Skip to content

Commit

Permalink
feat: create data save mode for mobile connections (#1128)
Browse files Browse the repository at this point in the history
Fixes #1112
  • Loading branch information
Feichtmeier authored Jan 21, 2025
1 parent 76df047 commit c3ddadc
Show file tree
Hide file tree
Showing 15 changed files with 412 additions and 30 deletions.
13 changes: 4 additions & 9 deletions lib/app/connectivity_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:safe_change_notifier/safe_change_notifier.dart';

import '../common/data/audio_type.dart';
import '../extensions/connectivity_x.dart';
import '../player/player_service.dart';

class ConnectivityModel extends SafeChangeNotifier {
Expand Down Expand Up @@ -34,6 +35,9 @@ class ConnectivityModel extends SafeChangeNotifier {

bool get isOnline => _connectivity.isOnline(_result);

bool get isMaybeLowBandWidth => _connectivity.isMaybeLowBandWidth(_result);

List<ConnectivityResult>? get result => _result;
List<ConnectivityResult>? _result;
void _updateConnectivity(List<ConnectivityResult> newResult) {
if (!_connectivity.isOnline(newResult) &&
Expand All @@ -50,12 +54,3 @@ class ConnectivityModel extends SafeChangeNotifier {
super.dispose();
}
}

extension _ConnectivityX on Connectivity {
bool isOnline(List<ConnectivityResult>? res) =>
res?.contains(ConnectivityResult.ethernet) == true ||
res?.contains(ConnectivityResult.bluetooth) == true ||
res?.contains(ConnectivityResult.mobile) == true ||
res?.contains(ConnectivityResult.vpn) == true ||
res?.contains(ConnectivityResult.wifi) == true;
}
57 changes: 55 additions & 2 deletions lib/app/view/mobile_page.dart
Original file line number Diff line number Diff line change
@@ -1,27 +1,53 @@
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart';
import 'package:watch_it/watch_it.dart';

import '../../common/view/bandwidth_dialog.dart';
import '../../common/view/snackbars.dart';
import '../../common/view/theme.dart';
import '../../extensions/build_context_x.dart';
import '../../extensions/connectivity_x.dart';
import '../../player/player_model.dart';
import '../../player/view/full_height_player.dart';
import '../../player/view/player_view.dart';
import '../../podcasts/download_model.dart';
import '../../podcasts/podcast_model.dart';
import '../../podcasts/podcast_search_state.dart';
import '../../podcasts/view/podcast_snackbar_contents.dart';
import '../../settings/settings_model.dart';
import '../app_model.dart';
import '../connectivity_model.dart';
import 'mobile_bottom_bar.dart';

class MobilePage extends StatelessWidget with WatchItMixin {
class MobilePage extends StatefulWidget with WatchItStatefulWidgetMixin {
const MobilePage({
super.key,
required this.page,
});

final Widget page;

@override
State<MobilePage> createState() => _MobilePageState();
}

class _MobilePageState extends State<MobilePage> {
@override
void initState() {
super.initState();

WidgetsBinding.instance.addPostFrameCallback((_) {
if (di<SettingsModel>().notifyDataSafeMode &&
di<ConnectivityModel>().isMaybeLowBandWidth &&
!di<PlayerModel>().dataSafeMode) {
showDialog(
context: context,
builder: (context) => const BandwidthDialog(),
);
}
});
}

@override
Widget build(BuildContext context) {
final fullWindowMode =
Expand All @@ -37,6 +63,33 @@ class MobilePage extends StatelessWidget with WatchItMixin {
},
);

final dataSafeMode = watchPropertyValue((PlayerModel m) => m.dataSafeMode);
final notifyDataSafeMode =
watchPropertyValue((SettingsModel m) => m.notifyDataSafeMode);

registerStreamHandler(
select: (Connectivity m) => m.onConnectivityChanged,
handler: (context, res, cancel) {
if (notifyDataSafeMode && res.hasData) {
if (di<Connectivity>().isMaybeLowBandWidth(res.data) &&
!dataSafeMode) {
showDialog(
context: context,
builder: (context) => const BandwidthDialog(),
);
} else if (!di<Connectivity>().isMaybeLowBandWidth(res.data) &&
dataSafeMode) {
showDialog(
context: context,
builder: (context) => const BandwidthDialog(
backOnBetterConnection: true,
),
);
}
}
},
);

registerStreamHandler(
select: (PodcastModel m) => m.stateStream,
initialValue: null,
Expand Down Expand Up @@ -73,7 +126,7 @@ class MobilePage extends StatelessWidget with WatchItMixin {
body: Stack(
fit: StackFit.expand,
children: [
page,
widget.page,
if (fullWindowMode)
Material(
color: context.theme.scaffoldBackgroundColor,
Expand Down
37 changes: 37 additions & 0 deletions lib/common/view/bandwidth_dialog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:watch_it/watch_it.dart';

import '../../l10n/l10n.dart';
import '../../player/player_model.dart';
import '../../settings/settings_model.dart';
import 'confirm.dart';

class BandwidthDialog extends StatelessWidget {
const BandwidthDialog({
super.key,
this.backOnBetterConnection = false,
});

final bool backOnBetterConnection;

@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return ConfirmationDialog(
title: Text(
backOnBetterConnection
? l10n.isBackInWifiDialogTitle
: l10n.isMaybeLowBandwidthDialogTitle,
),
content: Text(
backOnBetterConnection
? l10n.isBackInWifiDialogBody
: l10n.isMaybeLowBandwidthDialogBody,
),
onConfirm: () =>
di<PlayerModel>().setDataSafeMode(!backOnBetterConnection),
cancelLabel: l10n.stopToNotifyAboutDataSafeMode,
onCancel: () => di<SettingsModel>().setNotifyDataSafeMode(false),
);
}
}
4 changes: 3 additions & 1 deletion lib/common/view/confirm.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class ConfirmationDialog extends StatelessWidget {
this.scrollable = false,
this.confirmLabel,
this.enabled = true,
this.cancelLabel,
});

final dynamic Function()? onConfirm;
Expand All @@ -29,6 +30,7 @@ class ConfirmationDialog extends StatelessWidget {
final bool showCloseIcon;
final bool scrollable;
final String? confirmLabel;
final String? cancelLabel;
final bool enabled;

@override
Expand Down Expand Up @@ -65,7 +67,7 @@ class ConfirmationDialog extends StatelessWidget {
}
}
: null,
child: Text(l10n.cancel),
child: Text(cancelLabel ?? l10n.cancel),
),
ElevatedButton(
onPressed: enabled
Expand Down
1 change: 1 addition & 0 deletions lib/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,4 @@ const kShowPositionDuration = 'showPositionDuration';
const kSettingsPageId = 'settings';
const kHomePageId = 'homePage';
const kPlaybackRate = 'playbackRate';
const kNotifyDataSafeMode = 'notifyDataSafeMode';
14 changes: 14 additions & 0 deletions lib/extensions/connectivity_x.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import 'package:connectivity_plus/connectivity_plus.dart';

extension ConnectivityX on Connectivity {
bool isOnline(List<ConnectivityResult>? res) =>
res?.contains(ConnectivityResult.ethernet) == true ||
res?.contains(ConnectivityResult.bluetooth) == true ||
res?.contains(ConnectivityResult.mobile) == true ||
res?.contains(ConnectivityResult.vpn) == true ||
res?.contains(ConnectivityResult.wifi) == true;

bool isMaybeLowBandWidth(List<ConnectivityResult>? res) =>
res?.contains(ConnectivityResult.ethernet) == false &&
res?.contains(ConnectivityResult.wifi) == false;
}
10 changes: 10 additions & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,16 @@
"resetAllSettingsConfirm": "Are you absolutely sure to reset all settings, your podcast subscriptions, your podcast progress, your starred stations and your pinned albums? The app will be closed after and you need to re-open it.",
"confirm": "Confirm",
"confirmation": "Confirmation",
"isMaybeLowBandwidthDialogTitle": "No WIFI/Ethernet",
"isMaybeLowBandwidthDialogBody": "You are not connected to WIFI or Ethernet. Do you want to enable data safe mode?",
"isBackInWifiDialogTitle": "Back in WIFI/Ethernet",
"isBackInWifiDialogBody": "You are connected to WIFI or Ethernet. Do you want to disable data safe mode?",
"enableDataSafeModeSettingTitle": "Data safe mode",
"enableDataSafeModeSettingDescription": "When active the player will not try to download artwork of titles send from radio stations.",
"stopToNotifyAboutDataSafeMode": "Stop to notify me",
"notifyMeAboutDataSafeModeTitle": "Data safe mode notifications",
"notifyMeAboutDataSafeModeDescription": "Notify me about data safe mode",
"resourceSectionTitle": "Device resources",
"downloadsOnly": "Downloads only",
"downloadsDirectory": "Location of your downloads",
"downloadsDirectoryDescription": "Make sure MusicPod can access this directory!",
Expand Down
3 changes: 3 additions & 0 deletions lib/player/player_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ class PlayerModel extends SafeChangeNotifier {
notifyListeners();
}

void setDataSafeMode(bool value) => _playerService.setDataSafeMode(value);
bool get dataSafeMode => _playerService.dataSafeMode;

@override
Future<void> dispose() async {
await _propertiesChangedSub?.cancel();
Expand Down
11 changes: 10 additions & 1 deletion lib/player/player_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -759,6 +759,13 @@ class PlayerService {
//
// Everything related to radio stream icy-title information observed from MPV and digested here
//
bool _dataSafeMode = false;
bool get dataSafeMode => _dataSafeMode;
void setDataSafeMode(bool value) {
if (value == _dataSafeMode) return;
_dataSafeMode = value;
_propertiesChangedController.add(true);
}

MpvMetaData? _mpvMetaData;
MpvMetaData? get mpvMetaData => _mpvMetaData;
Expand All @@ -773,7 +780,9 @@ class PlayerService {
),
);

await _processParsedIcyTitle(mpvMetaData!.icyTitle);
if (!_dataSafeMode) {
await _processParsedIcyTitle(mpvMetaData!.icyTitle);
}
}
_propertiesChangedController.add(true);
}
Expand Down
4 changes: 4 additions & 0 deletions lib/settings/settings_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ class SettingsModel extends SafeChangeNotifier {
bool get useMoreAnimations => _service.useMoreAnimations;
void setUseMoreAnimations(bool value) => _service.setUseMoreAnimations(value);

bool get notifyDataSafeMode => _service.notifyDataSafeMode;
void setNotifyDataSafeMode(bool value) =>
_service.setNotifyDataSafeMode(value);

bool get usePodcastIndex => _service.usePodcastIndex;
Future<void> setUsePodcastIndex(bool value) async =>
_service.setUsePodcastIndex(value);
Expand Down
5 changes: 5 additions & 0 deletions lib/settings/settings_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ class SettingsService {
void setUseMoreAnimations(bool value) =>
_preferences.setBool(kUseMoreAnimations, value).then(notify);

bool get notifyDataSafeMode =>
_preferences.getBool(kNotifyDataSafeMode) ?? true;
void setNotifyDataSafeMode(bool value) =>
_preferences.setBool(kNotifyDataSafeMode, value).then(notify);

bool recentPatchNotesDisposed(String version) =>
_preferences.getString(kPatchNotesDisposed) == version;

Expand Down
56 changes: 56 additions & 0 deletions lib/settings/view/resource_section.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'package:watch_it/watch_it.dart';
import 'package:yaru/yaru.dart';

import '../../common/view/common_widgets.dart';
import '../../common/view/ui_constants.dart';
import '../../l10n/l10n.dart';
import '../../player/player_model.dart';
import '../settings_model.dart';

class ResourceSection extends StatelessWidget with WatchItMixin {
const ResourceSection({super.key});

@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return YaruSection(
margin: const EdgeInsets.only(
left: kLargestSpace,
top: kLargestSpace,
right: kLargestSpace,
),
headline: Text(l10n.resourceSectionTitle),
child: Column(
children: [
YaruTile(
title: Text(l10n.useMoreAnimationsTitle),
subtitle: Text(l10n.useMoreAnimationsDescription),
trailing: CommonSwitch(
onChanged: di<SettingsModel>().setUseMoreAnimations,
value:
watchPropertyValue((SettingsModel m) => m.useMoreAnimations),
),
),
YaruTile(
title: Text(l10n.enableDataSafeModeSettingTitle),
subtitle: Text(l10n.enableDataSafeModeSettingDescription),
trailing: CommonSwitch(
onChanged: di<PlayerModel>().setDataSafeMode,
value: watchPropertyValue((PlayerModel m) => m.dataSafeMode),
),
),
YaruTile(
title: Text(l10n.notifyMeAboutDataSafeModeTitle),
subtitle: Text(l10n.notifyMeAboutDataSafeModeDescription),
trailing: CommonSwitch(
onChanged: di<SettingsModel>().setNotifyDataSafeMode,
value:
watchPropertyValue((SettingsModel m) => m.notifyDataSafeMode),
),
),
],
),
);
}
}
2 changes: 2 additions & 0 deletions lib/settings/view/settings_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'expose_online_section.dart';
import 'local_audio_section.dart';
import 'podcast_section.dart';
import 'reset_section.dart';
import 'resource_section.dart';
import 'theme_section.dart';

class SettingsPage extends StatelessWidget {
Expand All @@ -25,6 +26,7 @@ class SettingsPage extends StatelessWidget {
PodcastSection(),
LocalAudioSection(),
ExposeOnlineSection(),
ResourceSection(),
ResetSection(),
AboutSection(),
],
Expand Down
9 changes: 0 additions & 9 deletions lib/settings/view/theme_section.dart
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,6 @@ class ThemeSection extends StatelessWidget with WatchItMixin {
),
),
),
YaruTile(
title: Text(l10n.useMoreAnimationsTitle),
subtitle: Text(l10n.useMoreAnimationsDescription),
trailing: CommonSwitch(
onChanged: di<SettingsModel>().setUseMoreAnimations,
value:
watchPropertyValue((SettingsModel m) => m.useMoreAnimations),
),
),
YaruTile(
title: Text(l10n.showPositionDurationTitle),
subtitle: Text(l10n.showPositionDurationDescription),
Expand Down
Loading

0 comments on commit c3ddadc

Please sign in to comment.