From 919048ea01897756f8d73522d26bfb6beec001d1 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Thu, 19 Oct 2023 13:42:11 -0300 Subject: [PATCH 1/8] chore: update README.md --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 73f744a8..95243b7a 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,8 @@ Let's say, we're adding French (`fr`) translation. 3. Add your translations to your new `app_fr.arb` file in correspondence to existing [English translations](https://github.com/bluecherrydvr/unity/tree/main/lib/l10n/app_en.arb). 4. Send us a new pull-request. 🎉 +When adding new strings, run `bin/l10n_organizer.dart`. This script will ensure that the new strings are added to all l10n files and that they are in the same location. It will also remove any unused strings. The base file is `app_en.arb`, so all strings must be added there first. + ## Bug-Reports Send us details about any issues you discover [in the issues](https://github.com/bluecherrydvr/unity/issues) or [in the forums](https://forums.bluecherrydvr.com/). @@ -120,7 +122,7 @@ lib │ ├───mobile_view_provider.dart [stores, provides & caches mobile camera layout etc.] │ ├───server_provider.dart [stores, provides & caches multiple DVR servers added by the user.] │ └───settings_provider.dart [stores, provides & caches various in-app configurations & settings.] -│ └───update_provider.dart [manages app updates and app status.] +│ └───update_provider.dart [manages app updates and app status.] │ ├───utils [constant values, helper functions & theme-related stuff.] │ ├───constants.dart @@ -155,7 +157,7 @@ flutter build [linux|windows|android|ios] The automated build process is done using GitHub Actions. You may find the workflow [here](.github/workflows/main.yml). The workflow builds the app for all supported platforms & uploads the artifacts to the release page. -On Linux, a Flutter executable with different environment variables is used to build the app for different distributions. This tells the app how the system is configured and how it should install updates. To run for Linux, you need to provide the following environment variables based on your system, where `[DISTRO_ENV]` can be `appimage`, `deb`, `rpm` or `tar.gz` (Tarball). +On Linux, a Flutter executable with different environment variables is used to build the app for different distributions. This tells the app how the system is configured and how it should install updates. To run for Linux, you need to provide the following environment variables based on your system, where `[DISTRO_ENV]` can be `appimage` (AppImage), `deb` (Debian), `rpm` (RedHat) or `tar.gz` (Tarball). ```bash flutter run --dart-define-from-file=linux/env/[DISTRO_ENV].json From 9e4ce373aa3275b7ba814544e3eee3204959e63d Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Thu, 19 Oct 2023 14:19:46 -0300 Subject: [PATCH 2/8] feat: wakelock support --- lib/l10n/app_en.arb | 2 + lib/l10n/app_fr.arb | 2 + lib/l10n/app_pl.arb | 2 + lib/l10n/app_pt.arb | 2 + lib/providers/settings_provider.dart | 135 +++++++----------- lib/utils/constants.dart | 1 + lib/widgets/misc.dart | 9 +- lib/widgets/settings/desktop/general.dart | 16 +++ lib/widgets/settings/mobile/settings.dart | 19 +++ lib/widgets/settings/settings.dart | 2 + .../lib/unity_video_player_main.dart | 1 + ...unity_video_player_platform_interface.dart | 3 + 12 files changed, 112 insertions(+), 82 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index d92386b7..9f674ed1 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -386,6 +386,8 @@ "@@MISC": {}, "general": "General", "miscellaneous": "Miscellaneous", + "wakelock": "Keep screen on", + "wakelockDescription": "Keep screen on while watching live streams or recordings.", "@Snoozing": {}, "snooze15": "15 minutes", "snooze30": "30 minutes", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 7a017f5a..e400c29d 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -362,6 +362,8 @@ "@@MISC": {}, "general": "General", "miscellaneous": "Divers", + "wakelock": "Keep screen on", + "wakelockDescription": "Keep screen on while watching live streams or recordings.", "@Snoozing": {}, "snooze15": "15 minutes", "snooze30": "30 minutes", diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 681c7eba..7b3a34cb 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -386,6 +386,8 @@ "@@MISC": {}, "general": "General", "miscellaneous": "Różne", + "wakelock": "Keep screen on", + "wakelockDescription": "Keep screen on while watching live streams or recordings.", "@Snoozing": {}, "snooze15": "15 minut", "snooze30": "30 minut", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 01d7392a..2804a429 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -386,6 +386,8 @@ "@@MISC": {}, "general": "Geral", "miscellaneous": "Outros", + "wakelock": "Deixar tela ligada", + "wakelockDescription": "Mantenha a tela ligada enquanto estiver assistindo a streams ao vivo ou gravações", "@Snoozing": {}, "snooze15": "15 minutos", "snooze30": "30 minutos", diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 1c63d86e..24caac1e 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -33,7 +33,6 @@ import 'package:unity_video_player/unity_video_player.dart'; /// This class manages & saves the settings inside the application. class SettingsProvider extends ChangeNotifier { - /// `late` initialized [SettingsProvider] instance. static late final SettingsProvider instance; static const kDefaultThemeMode = ThemeMode.system; @@ -50,6 +49,22 @@ class SettingsProvider extends ChangeNotifier { static const kDefaultStreamingType = StreamingType.rtsp; static const kDefaultRTSPProtocol = RTSPProtocol.tcp; static const kDefaultVideoQuality = RenderingQuality.automatic; + static const kDefaultWakelockEnabled = true; + + late Locale _locale; + late ThemeMode _themeMode; + late DateFormat _dateFormat; + late DateFormat _timeFormat; + late DateTime _snoozedUntil; + late NotificationClickBehavior _notificationClickBehavior; + late UnityVideoFit _cameraViewFit; + late String _downloadsDirectory; + late bool _layoutCyclingEnabled; + late Duration _layoutCyclingTogglePeriod; + late StreamingType _streamingType; + late RTSPProtocol _rtspProtocol; + late RenderingQuality _videoQuality; + late bool _wakelockEnabled; // Getters. Locale get locale => _locale; @@ -66,6 +81,7 @@ class SettingsProvider extends ChangeNotifier { StreamingType get streamingType => _streamingType; RTSPProtocol get rtspProtocol => _rtspProtocol; RenderingQuality get videoQuality => _videoQuality; + bool get wakelockEnabled => _wakelockEnabled; // Setters. set locale(Locale value) { @@ -142,19 +158,11 @@ class SettingsProvider extends ChangeNotifier { _save(); } - late Locale _locale; - late ThemeMode _themeMode; - late DateFormat _dateFormat; - late DateFormat _timeFormat; - late DateTime _snoozedUntil; - late NotificationClickBehavior _notificationClickBehavior; - late UnityVideoFit _cameraViewFit; - late String _downloadsDirectory; - late bool _layoutCyclingEnabled; - late Duration _layoutCyclingTogglePeriod; - late StreamingType _streamingType; - late RTSPProtocol _rtspProtocol; - late RenderingQuality _videoQuality; + set wakelockEnabled(bool value) { + _wakelockEnabled = value; + UnityVideoPlayerInterface.wakelockEnabled = value; + _save(); + } /// Initializes the [SettingsProvider] instance & fetches state from `async` /// `package:hive` method-calls. Called before [runApp]. @@ -185,6 +193,7 @@ class SettingsProvider extends ChangeNotifier { kHiveStreamingType: streamingType.index, kHiveStreamingProtocol: rtspProtocol.index, kHiveVideoQuality: videoQuality.index, + kWakelockEnabled: wakelockEnabled, }); if (notify) notifyListeners(); @@ -215,72 +224,36 @@ class SettingsProvider extends ChangeNotifier { final systemLocale = Intl.getCurrentLocale(); final timePattern = await format.getTimePattern(); - if (data.containsKey(kHiveDateFormat)) { - _dateFormat = DateFormat(data[kHiveDateFormat]!, systemLocale); - } else { - _dateFormat = DateFormat(kDefaultDateFormat, systemLocale); - } - if (data.containsKey(kHiveTimeFormat)) { - _timeFormat = DateFormat(data[kHiveTimeFormat]!, systemLocale); - } else { - _timeFormat = DateFormat(timePattern ?? kDefaultTimeFormat, systemLocale); - } - if (data.containsKey(kHiveSnoozedUntil)) { - _snoozedUntil = DateTime.parse( - data[kHiveSnoozedUntil]!, - ); - } else { - _snoozedUntil = defaultSnoozedUntil; - } - if (data.containsKey(kHiveNotificationClickBehavior)) { - _notificationClickBehavior = NotificationClickBehavior - .values[data[kHiveNotificationClickBehavior]!]; - } else { - _notificationClickBehavior = kDefaultNotificationClickBehavior; - } - if (data.containsKey(kHiveCameraViewFit)) { - _cameraViewFit = UnityVideoFit.values[data[kHiveCameraViewFit]!]; - } else { - _cameraViewFit = kDefaultCameraViewFit; - } - - if (data.containsKey(kHiveDownloadsDirectorySetting)) { - _downloadsDirectory = data[kHiveDownloadsDirectorySetting]; - } else { - _downloadsDirectory = (await kDefaultDownloadsDirectory).path; - } - - if (data.containsKey(kHiveLayoutCycling)) { - _layoutCyclingEnabled = data[kHiveLayoutCycling]; - } else { - _layoutCyclingEnabled = kDefaultLayoutCyclingEnabled; - } - - if (data.containsKey(kHiveLayoutCyclingPeriod)) { - _layoutCyclingTogglePeriod = Duration( - milliseconds: data[kHiveLayoutCyclingPeriod], - ); - } else { - _layoutCyclingTogglePeriod = kDefaultLayoutCyclingTogglePeriod; - } - - if (data.containsKey(kHiveStreamingType)) { - _streamingType = StreamingType.values[data[kHiveStreamingType]!]; - } else { - _streamingType = kDefaultStreamingType; - } - - if (data.containsKey(kHiveStreamingProtocol)) { - _rtspProtocol = RTSPProtocol.values[data[kHiveStreamingProtocol]!]; - } else { - _rtspProtocol = kDefaultRTSPProtocol; - } - - if (data.containsKey(kHiveVideoQuality)) { - _videoQuality = RenderingQuality.values[data[kHiveVideoQuality]!]; - } else { - _videoQuality = kDefaultVideoQuality; - } + _dateFormat = DateFormat( + data[kHiveDateFormat] ?? kDefaultDateFormat, + systemLocale, + ); + _timeFormat = DateFormat( + data[kHiveTimeFormat] ?? timePattern ?? kDefaultTimeFormat, + systemLocale, + ); + _snoozedUntil = + DateTime.tryParse(data[kHiveSnoozedUntil]) ?? defaultSnoozedUntil; + _notificationClickBehavior = NotificationClickBehavior.values[ + data[kHiveNotificationClickBehavior] ?? + kDefaultNotificationClickBehavior.index]; + _cameraViewFit = UnityVideoFit + .values[data[kHiveCameraViewFit] ?? kDefaultCameraViewFit.index]; + _downloadsDirectory = data[kHiveDownloadsDirectorySetting] ?? + ((await kDefaultDownloadsDirectory).path); + _layoutCyclingEnabled = + data[kHiveLayoutCycling] ?? kDefaultLayoutCyclingEnabled; + _layoutCyclingTogglePeriod = Duration( + milliseconds: data[kHiveLayoutCyclingPeriod] ?? + kDefaultLayoutCyclingTogglePeriod.inMilliseconds, + ); + _streamingType = StreamingType + .values[data[kHiveStreamingType] ?? kDefaultStreamingType.index]; + _rtspProtocol = RTSPProtocol + .values[data[kHiveStreamingProtocol] ?? kDefaultRTSPProtocol.index]; + _videoQuality = RenderingQuality + .values[data[kHiveVideoQuality] ?? kDefaultVideoQuality.index]; + _wakelockEnabled = data[kWakelockEnabled] ?? kDefaultWakelockEnabled; notifyListeners(); } @@ -355,5 +328,5 @@ enum RenderingQuality { enum StreamingType { rtsp, hls, - mjpeg, + mjpeg; } diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index 10e2ddd9..50a000e4 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -58,6 +58,7 @@ const kHiveLastCheck = 'last_update_check'; const kHiveStreamingType = 'streaming_type'; const kHiveStreamingProtocol = 'streaming_protocol'; const kHiveVideoQuality = 'video_quality'; +const kWakelockEnabled = 'wakelock_enabled'; /// Used as frame buffer size in [DeviceTile], and calculating aspect ratio. Only relevant on desktop. const kDeviceTileWidth = 640.0; diff --git a/lib/widgets/misc.dart b/lib/widgets/misc.dart index 5476b8b2..827bf059 100644 --- a/lib/widgets/misc.dart +++ b/lib/widgets/misc.dart @@ -104,6 +104,7 @@ class CorrectedListTile extends StatelessWidget { final String? subtitle; final double? height; final IconData? trailing; + final Widget? trailingWidget; const CorrectedListTile({ super.key, @@ -113,6 +114,7 @@ class CorrectedListTile extends StatelessWidget { this.onTap, this.height, this.trailing, + this.trailingWidget, }); @override @@ -156,7 +158,12 @@ class CorrectedListTile extends StatelessWidget { ], ), ), - if (trailing != null) + if (trailingWidget != null) + Padding( + padding: const EdgeInsetsDirectional.only(start: 12.0), + child: trailingWidget!, + ) + else if (trailing != null) Container( margin: const EdgeInsetsDirectional.only(start: 12.0), alignment: AlignmentDirectional.center, diff --git a/lib/widgets/settings/desktop/general.dart b/lib/widgets/settings/desktop/general.dart index 722d71cc..279a896a 100644 --- a/lib/widgets/settings/desktop/general.dart +++ b/lib/widgets/settings/desktop/general.dart @@ -196,6 +196,22 @@ class GeneralSettings extends StatelessWidget { ); }).toList(), ), + CorrectedListTile( + iconData: Icons.monitor, + trailingWidget: Padding( + padding: const EdgeInsetsDirectional.only(end: 4.0), + child: IgnorePointer( + child: Checkbox( + value: settings.wakelockEnabled, + onChanged: (v) {}, + ), + ), + ), + title: loc.wakelock, + subtitle: loc.wakelockDescription, + height: 72.0, + onTap: () => settings.wakelockEnabled = !settings.wakelockEnabled, + ), ]); } } diff --git a/lib/widgets/settings/mobile/settings.dart b/lib/widgets/settings/mobile/settings.dart index bc09f58c..9ce275d0 100644 --- a/lib/widgets/settings/mobile/settings.dart +++ b/lib/widgets/settings/mobile/settings.dart @@ -304,6 +304,25 @@ class _MobileSettingsState extends State { }, ), ), + SliverToBoxAdapter( + child: CorrectedListTile( + iconData: Icons.monitor, + trailingWidget: Padding( + padding: const EdgeInsetsDirectional.only(end: 4.0), + child: IgnorePointer( + child: Checkbox( + value: settings.wakelockEnabled, + onChanged: (v) {}, + ), + ), + ), + title: loc.wakelock, + subtitle: loc.wakelockDescription, + height: 72.0, + onTap: () => + settings.wakelockEnabled = !settings.wakelockEnabled, + ), + ), if (update.isUpdatingSupported) ...[ SliverToBoxAdapter( child: SubHeader( diff --git a/lib/widgets/settings/settings.dart b/lib/widgets/settings/settings.dart index a784003d..d0c28dec 100644 --- a/lib/widgets/settings/settings.dart +++ b/lib/widgets/settings/settings.dart @@ -22,6 +22,8 @@ import 'package:bluecherry_client/widgets/settings/desktop/settings.dart'; import 'package:bluecherry_client/widgets/settings/mobile/settings.dart'; import 'package:flutter/material.dart'; +// TODO(bdlukaa): update shared widgets + class Settings extends StatelessWidget { const Settings({super.key}); diff --git a/packages/unity_video_player/unity_video_player_main/lib/unity_video_player_main.dart b/packages/unity_video_player/unity_video_player_main/lib/unity_video_player_main.dart index a2700704..3cec7a58 100644 --- a/packages/unity_video_player/unity_video_player_main/lib/unity_video_player_main.dart +++ b/packages/unity_video_player/unity_video_player_main/lib/unity_video_player_main.dart @@ -100,6 +100,7 @@ class _MKVideo extends StatelessWidget { fill: color, fit: fit, controls: NoVideoControls, + wakelock: UnityVideoPlayerInterface.wakelockEnabled, ); } } diff --git a/packages/unity_video_player/unity_video_player_platform_interface/lib/unity_video_player_platform_interface.dart b/packages/unity_video_player/unity_video_player_platform_interface/lib/unity_video_player_platform_interface.dart index 0b1cf79d..b29695b7 100644 --- a/packages/unity_video_player/unity_video_player_platform_interface/lib/unity_video_player_platform_interface.dart +++ b/packages/unity_video_player/unity_video_player_platform_interface/lib/unity_video_player_platform_interface.dart @@ -50,6 +50,9 @@ abstract class UnityVideoPlayerInterface extends PlatformInterface { _instance = instance; } + /// Whether the app should be kept awake while playing videos. + static bool wakelockEnabled = true; + /// Called to initialize any resources before using it Future initialize(); From 29f05c7e798ab7912f00bda3f2f50f63ff6dc0cb Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Thu, 19 Oct 2023 18:29:03 -0300 Subject: [PATCH 3/8] chore: reorganize settings folder --- lib/widgets/settings/desktop/general.dart | 184 +------------ lib/widgets/settings/desktop/settings.dart | 2 +- lib/widgets/settings/desktop/updates.dart | 2 +- lib/widgets/settings/mobile/date_time.dart | 113 -------- lib/widgets/settings/mobile/settings.dart | 189 +------------- .../{desktop => shared}/date_language.dart | 96 ++++++- lib/widgets/settings/shared/tiles.dart | 247 ++++++++++++++++++ .../settings/{mobile => shared}/update.dart | 0 8 files changed, 363 insertions(+), 470 deletions(-) delete mode 100644 lib/widgets/settings/mobile/date_time.dart rename lib/widgets/settings/{desktop => shared}/date_language.dart (61%) create mode 100644 lib/widgets/settings/shared/tiles.dart rename lib/widgets/settings/{mobile => shared}/update.dart (100%) diff --git a/lib/widgets/settings/desktop/general.dart b/lib/widgets/settings/desktop/general.dart index 279a896a..6fc76be6 100644 --- a/lib/widgets/settings/desktop/general.dart +++ b/lib/widgets/settings/desktop/general.dart @@ -17,201 +17,31 @@ * along with this program. If not, see . */ -import 'dart:io'; - -import 'package:bluecherry_client/providers/settings_provider.dart'; -import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/widgets/misc.dart'; import 'package:bluecherry_client/widgets/settings/desktop/settings.dart'; -import 'package:file_picker/file_picker.dart'; +import 'package:bluecherry_client/widgets/settings/shared/tiles.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:provider/provider.dart'; class GeneralSettings extends StatelessWidget { const GeneralSettings({super.key}); @override Widget build(BuildContext context) { - final theme = Theme.of(context); final loc = AppLocalizations.of(context); - final settings = context.watch(); return ListView(padding: DesktopSettings.verticalPadding, children: [ SubHeader( loc.theme, subtext: loc.themeDescription, padding: DesktopSettings.horizontalPadding, ), - ...ThemeMode.values.map((e) { - return ListTile( - leading: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: Icon(switch (e) { - ThemeMode.system => Icons.brightness_auto, - ThemeMode.light => Icons.light_mode, - ThemeMode.dark => Icons.dark_mode, - }), - ), - onTap: () { - settings.themeMode = e; - }, - trailing: Radio( - value: e, - groupValue: settings.themeMode, - onChanged: (_) => settings.themeMode = e, - ), - title: Text(switch (e) { - ThemeMode.system => loc.system, - ThemeMode.light => loc.light, - ThemeMode.dark => loc.dark, - }), - subtitle: e == ThemeMode.system - ? Text(switch (MediaQuery.platformBrightnessOf(context)) { - Brightness.dark => loc.dark, - Brightness.light => loc.light, - }) - : null, - ); - }), + ...ThemeMode.values.map((mode) => ThemeTile(themeMode: mode)), SubHeader(loc.miscellaneous, padding: DesktopSettings.horizontalPadding), - CorrectedListTile( - iconData: Icons.notifications_paused, - onTap: () async { - if (settings.snoozedUntil.isAfter(DateTime.now())) { - settings.snoozedUntil = SettingsProvider.defaultSnoozedUntil; - } else { - final timeOfDay = await showTimePicker( - context: context, - helpText: loc.snoozeNotificationsUntil.toUpperCase(), - initialTime: TimeOfDay.fromDateTime(DateTime.now()), - useRootNavigator: false, - ); - if (timeOfDay != null) { - settings.snoozedUntil = DateTime( - DateTime.now().year, - DateTime.now().month, - DateTime.now().day, - timeOfDay.hour, - timeOfDay.minute, - ); - } - } - }, - title: loc.snoozeNotifications, - height: 72.0, - subtitle: settings.snoozedUntil.isAfter(DateTime.now()) - ? loc.snoozedUntil( - [ - if (settings.snoozedUntil.difference(DateTime.now()) > - const Duration(hours: 24)) - settings.formatDate(settings.snoozedUntil), - settings.formatTime(settings.snoozedUntil), - ].join(' '), - ) - : loc.notSnoozed, - ), - ExpansionTile( - leading: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.beenhere_rounded), - ), - title: Text(loc.notificationClickBehavior), - textColor: theme.textTheme.bodyLarge?.color, - subtitle: Text( - settings.notificationClickBehavior.locale(context), - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.textTheme.bodySmall?.color, - ), - ), - children: NotificationClickBehavior.values.map((behavior) { - return RadioListTile( - contentPadding: const EdgeInsetsDirectional.only( - start: 68.0, - end: 16.0, - ), - value: behavior, - groupValue: settings.notificationClickBehavior, - onChanged: (value) { - settings.notificationClickBehavior = behavior; - }, - secondary: Icon(behavior.icon), - controlAffinity: ListTileControlAffinity.trailing, - title: Padding( - padding: const EdgeInsetsDirectional.only(start: 16.0), - child: Text(behavior.locale(context)), - ), - ); - }).toList(), - ), - CorrectedListTile( - iconData: Icons.folder, - trailing: Icons.navigate_next, - title: loc.downloadPath, - subtitle: settings.downloadsDirectory, - height: 72.0, - onTap: () async { - final selectedDirectory = await FilePicker.platform.getDirectoryPath( - dialogTitle: loc.downloadPath, - initialDirectory: settings.downloadsDirectory, - lockParentWindow: true, - ); - - if (selectedDirectory != null) { - settings.downloadsDirectory = Directory(selectedDirectory).path; - } - }, - ), - ExpansionTile( - leading: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.timelapse), - ), - title: Text(loc.cycleTogglePeriod), - textColor: theme.textTheme.bodyLarge?.color, - subtitle: Text( - settings.layoutCyclingTogglePeriod.humanReadable(context), - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.textTheme.bodySmall?.color, - ), - ), - children: [5, 10, 30, 60, 60 * 5].map((e) { - final dur = Duration(seconds: e); - return RadioListTile( - value: dur, - groupValue: settings.layoutCyclingTogglePeriod, - onChanged: (value) { - settings.layoutCyclingTogglePeriod = dur; - }, - secondary: const Icon(null), - controlAffinity: ListTileControlAffinity.trailing, - title: Padding( - padding: const EdgeInsetsDirectional.only(start: 16.0), - child: Text( - dur.humanReadable(context), - ), - ), - ); - }).toList(), - ), - CorrectedListTile( - iconData: Icons.monitor, - trailingWidget: Padding( - padding: const EdgeInsetsDirectional.only(end: 4.0), - child: IgnorePointer( - child: Checkbox( - value: settings.wakelockEnabled, - onChanged: (v) {}, - ), - ), - ), - title: loc.wakelock, - subtitle: loc.wakelockDescription, - height: 72.0, - onTap: () => settings.wakelockEnabled = !settings.wakelockEnabled, - ), + const SnoozeNotificationsTile(), + const NavigationClickBehaviorTile(), + const DirectoryChooseTile(), + const CyclePeriodTile(), + const WakelockTile(), ]); } } diff --git a/lib/widgets/settings/desktop/settings.dart b/lib/widgets/settings/desktop/settings.dart index d301b0e3..231a626f 100644 --- a/lib/widgets/settings/desktop/settings.dart +++ b/lib/widgets/settings/desktop/settings.dart @@ -18,7 +18,7 @@ */ import 'package:bluecherry_client/utils/constants.dart'; -import 'package:bluecherry_client/widgets/settings/desktop/date_language.dart'; +import 'package:bluecherry_client/widgets/settings/shared/date_language.dart'; import 'package:bluecherry_client/widgets/settings/desktop/general.dart'; import 'package:bluecherry_client/widgets/settings/desktop/server.dart'; import 'package:bluecherry_client/widgets/settings/desktop/updates.dart'; diff --git a/lib/widgets/settings/desktop/updates.dart b/lib/widgets/settings/desktop/updates.dart index e16faf81..e8adf5ec 100644 --- a/lib/widgets/settings/desktop/updates.dart +++ b/lib/widgets/settings/desktop/updates.dart @@ -21,7 +21,7 @@ import 'dart:io'; import 'package:bluecherry_client/providers/update_provider.dart'; import 'package:bluecherry_client/widgets/settings/desktop/settings.dart'; -import 'package:bluecherry_client/widgets/settings/mobile/update.dart'; +import 'package:bluecherry_client/widgets/settings/shared/update.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; diff --git a/lib/widgets/settings/mobile/date_time.dart b/lib/widgets/settings/mobile/date_time.dart deleted file mode 100644 index f9db8a4a..00000000 --- a/lib/widgets/settings/mobile/date_time.dart +++ /dev/null @@ -1,113 +0,0 @@ -/* - * This file is a part of Bluecherry Client (https://github.com/bluecherrydvr/unity). - * - * Copyright 2022 Bluecherry, LLC - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation; either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -part of 'settings.dart'; - -class DateFormatSection extends StatelessWidget { - const DateFormatSection({super.key}); - - @override - Widget build(BuildContext context) { - final settings = context.watch(); - final locale = Localizations.localeOf(context).toLanguageTag(); - return LayoutBuilder(builder: (context, consts) { - final formats = [ - 'dd MMMM yyyy', - 'EEEE, dd MMMM yyyy', - 'EE, dd MMMM yyyy', - 'MM/dd/yyyy', - 'dd/MM/yyyy', - 'MM-dd-yyyy', - 'dd-MM-yyyy', - 'yyyy-MM-dd' - ].map((e) => DateFormat(e, locale)); - - if (consts.maxWidth >= 800) { - final crossAxisCount = consts.maxWidth >= 870 ? 4 : 3; - return Wrap( - children: formats.map((format) { - return SizedBox( - width: consts.maxWidth / crossAxisCount, - child: RadioListTile( - value: format.pattern, - groupValue: settings.dateFormat.pattern, - onChanged: (value) { - settings.dateFormat = format; - }, - controlAffinity: ListTileControlAffinity.trailing, - title: AutoSizeText( - format.format(DateTime.utc(1969, 7, 20, 14, 18, 04)), - maxLines: 1, - softWrap: false, - ), - subtitle: Text(format.pattern ?? ''), - ), - ); - }).toList(), - ); - } else { - return Column( - children: formats.map((format) { - return RadioListTile( - value: format.pattern, - groupValue: settings.dateFormat.pattern, - onChanged: (value) { - settings.dateFormat = format; - }, - controlAffinity: ListTileControlAffinity.trailing, - title: Text( - format.format(DateTime.utc(1969, 7, 20, 14, 18, 04)), - ), - subtitle: Text(format.pattern ?? ''), - ); - }).toList(), - ); - } - }); - } -} - -class TimeFormatSection extends StatelessWidget { - const TimeFormatSection({super.key}); - - @override - Widget build(BuildContext context) { - final settings = context.watch(); - final locale = Localizations.localeOf(context).toLanguageTag(); - - return LayoutBuilder(builder: (context, constraints) { - final patterns = ['HH:mm', 'hh:mm a'].map((e) => DateFormat(e, locale)); - final date = DateTime.utc(1969, 7, 20, 14, 18, 04); - return Column( - children: patterns.map((format) { - return ListTile( - onTap: () => settings.timeFormat = format, - trailing: Radio( - value: format.pattern, - groupValue: settings.timeFormat.pattern, - onChanged: (value) => settings.timeFormat = format, - ), - title: Text(format.format(date)), - subtitle: Text(format.pattern ?? ''), - ); - }).toList(), - ); - }); - } -} diff --git a/lib/widgets/settings/mobile/settings.dart b/lib/widgets/settings/mobile/settings.dart index 9ce275d0..57e39edb 100644 --- a/lib/widgets/settings/mobile/settings.dart +++ b/lib/widgets/settings/mobile/settings.dart @@ -19,7 +19,6 @@ import 'dart:io'; -import 'package:auto_size_text/auto_size_text.dart'; import 'package:bluecherry_client/models/server.dart'; import 'package:bluecherry_client/providers/home_provider.dart'; import 'package:bluecherry_client/providers/server_provider.dart'; @@ -29,20 +28,18 @@ import 'package:bluecherry_client/utils/constants.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/widgets/misc.dart'; import 'package:bluecherry_client/widgets/servers/edit_server.dart'; -import 'package:bluecherry_client/widgets/settings/desktop/date_language.dart'; -import 'package:bluecherry_client/widgets/settings/mobile/update.dart'; -import 'package:file_picker/file_picker.dart'; +import 'package:bluecherry_client/widgets/settings/shared/date_language.dart'; +import 'package:bluecherry_client/widgets/settings/shared/tiles.dart'; +import 'package:bluecherry_client/widgets/settings/shared/update.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_localized_locales/flutter_localized_locales.dart'; -import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:unity_video_player/unity_video_player.dart'; import 'package:url_launcher/url_launcher.dart'; part '../shared/server_tile.dart'; -part 'date_time.dart'; class MobileSettings extends StatefulWidget { const MobileSettings({super.key}); @@ -88,109 +85,14 @@ class _MobileSettingsState extends State { child: SubHeader(loc.theme, subtext: loc.themeDescription), ), SliverList.list( - children: ThemeMode.values.map((e) { - return ListTile( - leading: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: Icon(switch (e) { - ThemeMode.system => Icons.brightness_auto, - ThemeMode.light => Icons.light_mode, - ThemeMode.dark => Icons.dark_mode, - }), - ), - onTap: () => settings.themeMode = e, - trailing: Radio( - value: e, - groupValue: settings.themeMode, - onChanged: (value) { - settings.themeMode = e; - }, - ), - title: Text(switch (e) { - ThemeMode.system => loc.system, - ThemeMode.light => loc.light, - ThemeMode.dark => loc.dark, - }), - subtitle: e == ThemeMode.system - ? Text(switch (MediaQuery.platformBrightnessOf(context)) { - Brightness.dark => loc.dark, - Brightness.light => loc.light, - }) - : null, - ); - }).toList()), + children: ThemeMode.values + .map((mode) => ThemeTile(themeMode: mode)) + .toList(), + ), SliverToBoxAdapter(child: SubHeader(loc.miscellaneous)), SliverList.list(children: [ - CorrectedListTile( - iconData: Icons.notifications_paused, - onTap: () async { - if (settings.snoozedUntil.isAfter(DateTime.now())) { - settings.snoozedUntil = - SettingsProvider.defaultSnoozedUntil; - } else { - final timeOfDay = await showTimePicker( - context: context, - helpText: loc.snoozeNotificationsUntil.toUpperCase(), - initialTime: TimeOfDay.fromDateTime(DateTime.now()), - useRootNavigator: false, - ); - if (timeOfDay != null) { - settings.snoozedUntil = DateTime( - DateTime.now().year, - DateTime.now().month, - DateTime.now().day, - timeOfDay.hour, - timeOfDay.minute, - ); - } - } - }, - title: loc.snoozeNotifications, - height: 72.0, - subtitle: settings.snoozedUntil.isAfter(DateTime.now()) - ? loc.snoozedUntil([ - if (settings.snoozedUntil.difference(DateTime.now()) > - const Duration(hours: 24)) - settings.formatDate(settings.snoozedUntil), - settings.formatTime(settings.snoozedUntil), - ].join(' ')) - : loc.notSnoozed, - ), - ExpansionTile( - leading: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.beenhere_rounded), - ), - title: Text(loc.notificationClickBehavior), - textColor: theme.textTheme.bodyLarge?.color, - subtitle: Text( - settings.notificationClickBehavior.locale(context), - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.textTheme.bodySmall?.color, - ), - ), - children: NotificationClickBehavior.values.map((behavior) { - return RadioListTile( - contentPadding: const EdgeInsetsDirectional.only( - start: 68.0, - end: 16.0, - ), - value: behavior, - groupValue: settings.notificationClickBehavior, - onChanged: (value) { - settings.notificationClickBehavior = behavior; - }, - secondary: Icon(behavior.icon), - controlAffinity: ListTileControlAffinity.trailing, - title: Padding( - padding: const EdgeInsetsDirectional.only(start: 16.0), - child: Text(behavior.locale(context)), - ), - ); - }).toList(), - ), + const SnoozeNotificationsTile(), + const NavigationClickBehaviorTile(), ExpansionTile( leading: CircleAvatar( backgroundColor: const Color.fromRGBO(0, 0, 0, 0), @@ -223,57 +125,8 @@ class _MobileSettingsState extends State { ); }).toList(), ), - CorrectedListTile( - iconData: Icons.folder, - trailing: Icons.navigate_next, - title: loc.downloadPath, - subtitle: settings.downloadsDirectory, - height: 72.0, - onTap: () async { - final selectedDirectory = - await FilePicker.platform.getDirectoryPath( - dialogTitle: loc.downloadPath, - initialDirectory: settings.downloadsDirectory, - lockParentWindow: true, - ); - - if (selectedDirectory != null) { - settings.downloadsDirectory = - Directory(selectedDirectory).path; - } - }, - ), - ExpansionTile( - leading: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.timelapse), - ), - title: Text(loc.cycleTogglePeriod), - textColor: theme.textTheme.bodyLarge?.color, - subtitle: Text( - settings.layoutCyclingTogglePeriod.humanReadable(context), - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.textTheme.bodySmall?.color, - ), - ), - children: [5, 10, 30, 60, 60 * 5].map((e) { - final dur = Duration(seconds: e); - return RadioListTile( - value: dur, - groupValue: settings.layoutCyclingTogglePeriod, - onChanged: (value) { - settings.layoutCyclingTogglePeriod = dur; - }, - secondary: const Icon(null), - controlAffinity: ListTileControlAffinity.trailing, - title: Padding( - padding: const EdgeInsetsDirectional.only(start: 16.0), - child: Text(dur.humanReadable(context)), - ), - ); - }).toList(), - ), + const DirectoryChooseTile(), + const CyclePeriodTile(), ]), SliverToBoxAdapter( child: CorrectedListTile( @@ -304,25 +157,7 @@ class _MobileSettingsState extends State { }, ), ), - SliverToBoxAdapter( - child: CorrectedListTile( - iconData: Icons.monitor, - trailingWidget: Padding( - padding: const EdgeInsetsDirectional.only(end: 4.0), - child: IgnorePointer( - child: Checkbox( - value: settings.wakelockEnabled, - onChanged: (v) {}, - ), - ), - ), - title: loc.wakelock, - subtitle: loc.wakelockDescription, - height: 72.0, - onTap: () => - settings.wakelockEnabled = !settings.wakelockEnabled, - ), - ), + const SliverToBoxAdapter(child: WakelockTile()), if (update.isUpdatingSupported) ...[ SliverToBoxAdapter( child: SubHeader( diff --git a/lib/widgets/settings/desktop/date_language.dart b/lib/widgets/settings/shared/date_language.dart similarity index 61% rename from lib/widgets/settings/desktop/date_language.dart rename to lib/widgets/settings/shared/date_language.dart index c8b8bdeb..645f5ab1 100644 --- a/lib/widgets/settings/desktop/date_language.dart +++ b/lib/widgets/settings/shared/date_language.dart @@ -17,13 +17,14 @@ * along with this program. If not, see . */ +import 'package:auto_size_text/auto_size_text.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/widgets/settings/desktop/settings.dart'; -import 'package:bluecherry_client/widgets/settings/mobile/settings.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_localized_locales/flutter_localized_locales.dart'; +import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; class LocalizationSettings extends StatelessWidget { @@ -138,3 +139,96 @@ class LanguageSection extends StatelessWidget { ); } } + +class DateFormatSection extends StatelessWidget { + const DateFormatSection({super.key}); + + @override + Widget build(BuildContext context) { + final settings = context.watch(); + final locale = Localizations.localeOf(context).toLanguageTag(); + return LayoutBuilder(builder: (context, consts) { + final formats = [ + 'dd MMMM yyyy', + 'EEEE, dd MMMM yyyy', + 'EE, dd MMMM yyyy', + 'MM/dd/yyyy', + 'dd/MM/yyyy', + 'MM-dd-yyyy', + 'dd-MM-yyyy', + 'yyyy-MM-dd' + ].map((e) => DateFormat(e, locale)); + + if (consts.maxWidth >= 800) { + final crossAxisCount = consts.maxWidth >= 870 ? 4 : 3; + return Wrap( + children: formats.map((format) { + return SizedBox( + width: consts.maxWidth / crossAxisCount, + child: RadioListTile( + value: format.pattern, + groupValue: settings.dateFormat.pattern, + onChanged: (value) { + settings.dateFormat = format; + }, + controlAffinity: ListTileControlAffinity.trailing, + title: AutoSizeText( + format.format(DateTime.utc(1969, 7, 20, 14, 18, 04)), + maxLines: 1, + softWrap: false, + ), + subtitle: Text(format.pattern ?? ''), + ), + ); + }).toList(), + ); + } else { + return Column( + children: formats.map((format) { + return RadioListTile( + value: format.pattern, + groupValue: settings.dateFormat.pattern, + onChanged: (value) { + settings.dateFormat = format; + }, + controlAffinity: ListTileControlAffinity.trailing, + title: Text( + format.format(DateTime.utc(1969, 7, 20, 14, 18, 04)), + ), + subtitle: Text(format.pattern ?? ''), + ); + }).toList(), + ); + } + }); + } +} + +class TimeFormatSection extends StatelessWidget { + const TimeFormatSection({super.key}); + + @override + Widget build(BuildContext context) { + final settings = context.watch(); + final locale = Localizations.localeOf(context).toLanguageTag(); + + return LayoutBuilder(builder: (context, constraints) { + final patterns = ['HH:mm', 'hh:mm a'].map((e) => DateFormat(e, locale)); + final date = DateTime.utc(1969, 7, 20, 14, 18, 04); + return Column( + children: patterns.map((format) { + return ListTile( + onTap: () => settings.timeFormat = format, + trailing: Radio( + value: format.pattern, + groupValue: settings.timeFormat.pattern, + onChanged: (value) => settings.timeFormat = format, + ), + title: Text(format.format(date)), + subtitle: Text(format.pattern ?? ''), + ); + }).toList(), + ); + }); + } +} diff --git a/lib/widgets/settings/shared/tiles.dart b/lib/widgets/settings/shared/tiles.dart new file mode 100644 index 00000000..9a9ffb0b --- /dev/null +++ b/lib/widgets/settings/shared/tiles.dart @@ -0,0 +1,247 @@ +import 'dart:io'; + +import 'package:bluecherry_client/providers/settings_provider.dart'; +import 'package:bluecherry_client/utils/extensions.dart'; +import 'package:bluecherry_client/widgets/misc.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:provider/provider.dart'; + +class ThemeTile extends StatelessWidget { + final ThemeMode themeMode; + + const ThemeTile({super.key, required this.themeMode}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final settings = context.watch(); + final loc = AppLocalizations.of(context); + + return ListTile( + leading: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: Icon(switch (themeMode) { + ThemeMode.system => Icons.brightness_auto, + ThemeMode.light => Icons.light_mode, + ThemeMode.dark => Icons.dark_mode, + }), + ), + onTap: () => settings.themeMode = themeMode, + trailing: Radio( + value: themeMode, + groupValue: settings.themeMode, + onChanged: (value) { + settings.themeMode = themeMode; + }, + ), + title: Text(switch (themeMode) { + ThemeMode.system => loc.system, + ThemeMode.light => loc.light, + ThemeMode.dark => loc.dark, + }), + subtitle: themeMode == ThemeMode.system + ? Text(switch (MediaQuery.platformBrightnessOf(context)) { + Brightness.dark => loc.dark, + Brightness.light => loc.light, + }) + : null, + ); + } +} + +class DirectoryChooseTile extends StatelessWidget { + const DirectoryChooseTile({super.key}); + + @override + Widget build(BuildContext context) { + final settings = context.watch(); + final loc = AppLocalizations.of(context); + + return CorrectedListTile( + iconData: Icons.folder, + trailing: Icons.navigate_next, + title: loc.downloadPath, + subtitle: settings.downloadsDirectory, + height: 72.0, + onTap: () async { + final selectedDirectory = await FilePicker.platform.getDirectoryPath( + dialogTitle: loc.downloadPath, + initialDirectory: settings.downloadsDirectory, + lockParentWindow: true, + ); + + if (selectedDirectory != null) { + settings.downloadsDirectory = Directory(selectedDirectory).path; + } + }, + ); + } +} + +class SnoozeNotificationsTile extends StatelessWidget { + const SnoozeNotificationsTile({super.key}); + + @override + Widget build(BuildContext context) { + final settings = context.watch(); + final loc = AppLocalizations.of(context); + + return CorrectedListTile( + iconData: Icons.notifications_paused, + onTap: () async { + if (settings.snoozedUntil.isAfter(DateTime.now())) { + settings.snoozedUntil = SettingsProvider.defaultSnoozedUntil; + } else { + final timeOfDay = await showTimePicker( + context: context, + helpText: loc.snoozeNotificationsUntil.toUpperCase(), + initialTime: TimeOfDay.fromDateTime(DateTime.now()), + useRootNavigator: false, + ); + if (timeOfDay != null) { + settings.snoozedUntil = DateTime( + DateTime.now().year, + DateTime.now().month, + DateTime.now().day, + timeOfDay.hour, + timeOfDay.minute, + ); + } + } + }, + title: loc.snoozeNotifications, + height: 72.0, + subtitle: settings.snoozedUntil.isAfter(DateTime.now()) + ? loc.snoozedUntil( + [ + if (settings.snoozedUntil.difference(DateTime.now()) > + const Duration(hours: 24)) + settings.formatDate(settings.snoozedUntil), + settings.formatTime(settings.snoozedUntil), + ].join(' '), + ) + : loc.notSnoozed, + ); + } +} + +class NavigationClickBehaviorTile extends StatelessWidget { + const NavigationClickBehaviorTile({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final settings = context.watch(); + final loc = AppLocalizations.of(context); + + return ExpansionTile( + leading: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.beenhere_rounded), + ), + title: Text(loc.notificationClickBehavior), + textColor: theme.textTheme.bodyLarge?.color, + subtitle: Text( + settings.notificationClickBehavior.locale(context), + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.textTheme.bodySmall?.color, + ), + ), + children: NotificationClickBehavior.values.map((behavior) { + return RadioListTile( + contentPadding: const EdgeInsetsDirectional.only( + start: 68.0, + end: 16.0, + ), + value: behavior, + groupValue: settings.notificationClickBehavior, + onChanged: (value) { + settings.notificationClickBehavior = behavior; + }, + secondary: Icon(behavior.icon), + controlAffinity: ListTileControlAffinity.trailing, + title: Padding( + padding: const EdgeInsetsDirectional.only(start: 16.0), + child: Text(behavior.locale(context)), + ), + ); + }).toList(), + ); + } +} + +class CyclePeriodTile extends StatelessWidget { + const CyclePeriodTile({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final settings = context.watch(); + final loc = AppLocalizations.of(context); + + return ExpansionTile( + leading: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.timelapse), + ), + title: Text(loc.cycleTogglePeriod), + textColor: theme.textTheme.bodyLarge?.color, + subtitle: Text( + settings.layoutCyclingTogglePeriod.humanReadable(context), + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.textTheme.bodySmall?.color, + ), + ), + children: [5, 10, 30, 60, 60 * 5].map((e) { + final dur = Duration(seconds: e); + return RadioListTile( + value: dur, + groupValue: settings.layoutCyclingTogglePeriod, + onChanged: (value) { + settings.layoutCyclingTogglePeriod = dur; + }, + secondary: const Icon(null), + controlAffinity: ListTileControlAffinity.trailing, + title: Padding( + padding: const EdgeInsetsDirectional.only(start: 16.0), + child: Text( + dur.humanReadable(context), + ), + ), + ); + }).toList(), + ); + } +} + +class WakelockTile extends StatelessWidget { + const WakelockTile({super.key}); + + @override + Widget build(BuildContext context) { + final settings = context.watch(); + final loc = AppLocalizations.of(context); + + return CorrectedListTile( + iconData: Icons.monitor, + trailingWidget: Padding( + padding: const EdgeInsetsDirectional.only(end: 4.0), + child: IgnorePointer( + child: Checkbox( + value: settings.wakelockEnabled, + onChanged: (v) {}, + ), + ), + ), + title: loc.wakelock, + subtitle: loc.wakelockDescription, + height: 72.0, + onTap: () => settings.wakelockEnabled = !settings.wakelockEnabled, + ); + } +} diff --git a/lib/widgets/settings/mobile/update.dart b/lib/widgets/settings/shared/update.dart similarity index 100% rename from lib/widgets/settings/mobile/update.dart rename to lib/widgets/settings/shared/update.dart From 8300318d3d99eb41b6b1fa19026d154697bee47c Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Thu, 19 Oct 2023 19:07:25 -0300 Subject: [PATCH 4/8] chore: upgrade dependencies --- android/app/build.gradle | 2 +- lib/widgets/settings/desktop/settings.dart | 2 +- lib/widgets/settings/settings.dart | 2 - .../unity_multi_window/example/pubspec.lock | 14 ++--- pubspec.lock | 58 +++++++++++-------- pubspec.yaml | 16 ++--- 6 files changed, 50 insertions(+), 44 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 4a6559d3..9d549f4f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -27,7 +27,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 33 + compileSdkVersion 34 ndkVersion flutter.ndkVersion compileOptions { diff --git a/lib/widgets/settings/desktop/settings.dart b/lib/widgets/settings/desktop/settings.dart index 231a626f..7660edd4 100644 --- a/lib/widgets/settings/desktop/settings.dart +++ b/lib/widgets/settings/desktop/settings.dart @@ -18,10 +18,10 @@ */ import 'package:bluecherry_client/utils/constants.dart'; -import 'package:bluecherry_client/widgets/settings/shared/date_language.dart'; import 'package:bluecherry_client/widgets/settings/desktop/general.dart'; import 'package:bluecherry_client/widgets/settings/desktop/server.dart'; import 'package:bluecherry_client/widgets/settings/desktop/updates.dart'; +import 'package:bluecherry_client/widgets/settings/shared/date_language.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; diff --git a/lib/widgets/settings/settings.dart b/lib/widgets/settings/settings.dart index d0c28dec..a784003d 100644 --- a/lib/widgets/settings/settings.dart +++ b/lib/widgets/settings/settings.dart @@ -22,8 +22,6 @@ import 'package:bluecherry_client/widgets/settings/desktop/settings.dart'; import 'package:bluecherry_client/widgets/settings/mobile/settings.dart'; import 'package:flutter/material.dart'; -// TODO(bdlukaa): update shared widgets - class Settings extends StatelessWidget { const Settings({super.key}); diff --git a/packages/unity_multi_window/example/pubspec.lock b/packages/unity_multi_window/example/pubspec.lock index ec963e1c..c6518327 100644 --- a/packages/unity_multi_window/example/pubspec.lock +++ b/packages/unity_multi_window/example/pubspec.lock @@ -87,18 +87,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.11.0" path: dependency: transitive description: @@ -179,10 +179,10 @@ packages: dependency: transitive description: name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 url: "https://pub.dev" source: hosted - version: "0.1.4-beta" + version: "0.3.0" sdks: - dart: ">=3.1.0-185.0.dev <4.0.0" + dart: ">=3.2.0-194.0.dev <4.0.0" flutter: ">=1.17.0" diff --git a/pubspec.lock b/pubspec.lock index 85695598..7c4be9d4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -53,10 +53,10 @@ packages: dependency: "direct main" description: name: awesome_notifications - sha256: "6ba98d73553c8a54e7b77f8dd8b95ce9d32a7839ef182b28cf2ad54ec28b1821" + sha256: "65f730f9c0e73a346039ef746384bcff1773f9f03821b859705a7ab8db977b23" url: "https://pub.dev" source: hosted - version: "0.7.5-dev.3" + version: "0.8.2" boolean_selector: dependency: transitive description: @@ -109,10 +109,10 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: "77a180d6938f78ca7d2382d2240eb626c0f6a735d0bfdce227d8ffb80f95c48b" + sha256: b502a681ba415272ecc41400bd04fe543ed1a62632137dc84d25a91e7746f55f url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "5.0.1" connectivity_plus_platform_interface: dependency: transitive description: @@ -157,10 +157,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: "86add5ef97215562d2e090535b0a16f197902b10c369c558a100e74ea06e8659" + sha256: "7035152271ff67b072a211152846e9f1259cf1be41e34cd3e0b5463d2d6b8419" url: "https://pub.dev" source: hosted - version: "9.0.3" + version: "9.1.0" device_info_plus_platform_interface: dependency: transitive description: @@ -173,10 +173,10 @@ packages: dependency: "direct main" description: name: dio - sha256: ce75a1b40947fea0a0e16ce73337122a86762e38b982e1ccb909daa3b9bc4197 + sha256: "417e2a6f9d83ab396ec38ff4ea5da6c254da71e4db765ad737a42af6930140b7" url: "https://pub.dev" source: hosted - version: "5.3.2" + version: "5.3.3" duration: dependency: "direct main" description: @@ -213,10 +213,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: be325344c1f3070354a1d84a231a1ba75ea85d413774ec4bdf444c023342e030 + sha256: "903dd4ba13eae7cef64acc480e91bf54c3ddd23b5b90b639c170f3911e489620" url: "https://pub.dev" source: hosted - version: "5.5.0" + version: "6.0.0" firebase_core: dependency: "direct main" description: @@ -282,10 +282,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + sha256: ad76540d21c066228ee3f9d1dad64a9f7e46530e8bb7c85011a88bc1fd874bc5 url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "3.0.0" flutter_localizations: dependency: "direct main" description: flutter @@ -401,10 +401,10 @@ packages: dependency: transitive description: name: lints - sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "3.0.0" matcher: dependency: transitive description: @@ -417,10 +417,10 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" media_kit: dependency: transitive description: @@ -497,10 +497,10 @@ packages: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.11.0" msix: dependency: "direct dev" description: @@ -537,10 +537,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "6ff267fcd9d48cb61c8df74a82680e8b82e940231bb5f68356672fde0397334a" + sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017" url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.2.0" package_info_plus_platform_interface: dependency: transitive description: @@ -786,6 +786,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: @@ -962,10 +970,10 @@ packages: dependency: "direct main" description: name: uuid - sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + sha256: b715b8d3858b6fa9f68f87d20d98830283628014750c2b09b6f516c1da4af2a7 url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "4.1.0" vector_math: dependency: transitive description: @@ -1010,10 +1018,10 @@ packages: dependency: transitive description: name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 url: "https://pub.dev" source: hosted - version: "0.1.4-beta" + version: "0.3.0" win32: dependency: transitive description: @@ -1071,5 +1079,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.1.0-185.0.dev <4.0.0" + dart: ">=3.2.0-194.0.dev <4.0.0" flutter: ">=3.10.0" diff --git a/pubspec.yaml b/pubspec.yaml index 7031c48f..a0072b69 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,7 +19,7 @@ dependencies: http: ^1.1.0 # Dio is used by DownloadManager to show the donwload progress of the file. This is currently not possible by using http # TODO: no longer use dio and find a solution to show donwload progress using the http package - dio: ^5.3.2 + dio: ^5.3.3 xml2json: ^6.2.0 auto_size_text: ^3.0.0 @@ -34,17 +34,17 @@ dependencies: duration: ^3.0.12 firebase_core: 2.10.0 firebase_messaging: ^14.4.1 - awesome_notifications: ^0.7.5-dev.3 + awesome_notifications: ^0.8.2 system_date_time_format: ^0.7.0 - device_info_plus: ^9.0.3 - package_info_plus: ^4.1.0 - connectivity_plus: ^4.0.2 + device_info_plus: ^9.1.0 + package_info_plus: ^4.2.0 + connectivity_plus: ^5.0.1 version: ^3.0.2 url_launcher: ^6.1.10 path_provider: ^2.0.14 - file_picker: ^5.3.3 + file_picker: ^6.0.0 safe_local_storage: ^1.0.0 # Hive is just used in terms of migration @@ -52,7 +52,7 @@ dependencies: hive_flutter: ^1.1.0 permission_handler: ^11.0.0 - uuid: ^3.0.7 + uuid: ^4.1.0 # Desktop window_manager: ^0.3.2 @@ -62,7 +62,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^2.0.1 + flutter_lints: ^3.0.0 msix: ^3.7.0 flutter_launcher_icons: ^0.13.0 From 03964f854eb06156f6ee5e1c686db7cf31d68b38 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Thu, 19 Oct 2023 19:09:09 -0300 Subject: [PATCH 5/8] fix: correctly release devices on direct camera --- lib/providers/desktop_view_provider.dart | 2 +- lib/utils/widgets/tree_view.dart | 2 +- lib/widgets/direct_camera.dart | 2 +- lib/widgets/multi_window/window.dart | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/providers/desktop_view_provider.dart b/lib/providers/desktop_view_provider.dart index 3659244a..405ebdd0 100644 --- a/lib/providers/desktop_view_provider.dart +++ b/lib/providers/desktop_view_provider.dart @@ -130,7 +130,7 @@ class DesktopViewProvider extends ChangeNotifier { /// Releases a device if no layout is using it void _releaseDevice(Device device) { - if (!UnityPlayers.players.containsKey(device)) return; + if (!UnityPlayers.players.containsKey(device.uuid)) return; if (!layouts .any((layout) => layout.devices.any((d) => d.id == device.id))) { UnityPlayers.releaseDevice(device.uuid); diff --git a/lib/utils/widgets/tree_view.dart b/lib/utils/widgets/tree_view.dart index 281fbb96..a48eaf45 100644 --- a/lib/utils/widgets/tree_view.dart +++ b/lib/utils/widgets/tree_view.dart @@ -149,7 +149,7 @@ List? _copyNodesRecursively( } class _TreeNodeKey extends ValueKey { - const _TreeNodeKey(dynamic value) : super(value); + const _TreeNodeKey(super.value); } /// Provides unique keys and verifies duplicates. diff --git a/lib/widgets/direct_camera.dart b/lib/widgets/direct_camera.dart index 82fc6f5f..289f04ad 100644 --- a/lib/widgets/direct_camera.dart +++ b/lib/widgets/direct_camera.dart @@ -220,6 +220,6 @@ class _DevicesForServer extends StatelessWidget { arguments: {'device': device, 'player': player}, ); - if (!UnityPlayers.players.containsKey(device)) await player.release(); + if (!UnityPlayers.players.containsKey(device.uuid)) await player.release(); } } diff --git a/lib/widgets/multi_window/window.dart b/lib/widgets/multi_window/window.dart index f6d560ac..af45efdc 100644 --- a/lib/widgets/multi_window/window.dart +++ b/lib/widgets/multi_window/window.dart @@ -37,10 +37,10 @@ class AlternativeWindow extends StatefulWidget { /// Creates a new [AlternativeWindow] instance. const AlternativeWindow({ - Key? key, + super.key, required this.mode, required this.child, - }) : super(key: key); + }); static AlternativeWindowState? maybeOf(BuildContext context) { return context.findAncestorStateOfType(); From 6547f3a20772b3eac68178851580898b60db11ca Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Thu, 19 Oct 2023 19:17:22 -0300 Subject: [PATCH 6/8] feat: implement wakelock --- lib/providers/home_provider.dart | 23 ++++++++++++++++++++++- pubspec.lock | 6 +++--- pubspec.yaml | 1 + 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/lib/providers/home_provider.dart b/lib/providers/home_provider.dart index ee3eeeee..0306e264 100644 --- a/lib/providers/home_provider.dart +++ b/lib/providers/home_provider.dart @@ -19,11 +19,13 @@ import 'package:bluecherry_client/main.dart'; import 'package:bluecherry_client/providers/server_provider.dart'; +import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/utils/methods.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; enum UnityTab { deviceGrid, @@ -85,8 +87,9 @@ class HomeProvider extends ChangeNotifier { automaticallyGoToAddServersScreen = false; } - notifyListeners(); refreshDeviceOrientation(context); + updateWakelock(context); + notifyListeners(); } bool automaticallyGoToAddServersScreen = false; @@ -136,6 +139,24 @@ class HomeProvider extends ChangeNotifier { } } + void updateWakelock(BuildContext context) { + final settings = context.read(); + + if (!settings.wakelockEnabled) { + WakelockPlus.disable(); + } else { + switch (tab) { + case UnityTab.deviceGrid: + case UnityTab.eventsPlayback: + WakelockPlus.enable(); + break; + default: + WakelockPlus.disable(); + break; + } + } + } + /// Whether something in the app is loading bool get isLoading => loadReasons.isNotEmpty; void loading(UnityLoadingReason reason, {bool notify = true}) { diff --git a/pubspec.lock b/pubspec.lock index 7c4be9d4..815eeb3b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -999,13 +999,13 @@ packages: source: hosted version: "2.0.7" wakelock_plus: - dependency: transitive + dependency: "direct main" description: name: wakelock_plus - sha256: aac3f3258f01781ec9212df94eecef1eb9ba9350e106728def405baa096ba413 + sha256: f45a6c03aa3f8322e0a9d7f4a0482721c8789cb41d555407367650b8f9c26018 url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.3" wakelock_plus_platform_interface: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a0072b69..5e61d6c7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,6 +35,7 @@ dependencies: firebase_core: 2.10.0 firebase_messaging: ^14.4.1 awesome_notifications: ^0.8.2 + wakelock_plus: ^1.1.3 system_date_time_format: ^0.7.0 device_info_plus: ^9.1.0 From 779e1e7caae48c59abce4dfe28fee7124f9bbb5f Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Thu, 19 Oct 2023 19:28:25 -0300 Subject: [PATCH 7/8] feat: log file --- lib/utils/storage.dart | 34 +++++++++++++++++++ .../device_grid/mobile/device_view.dart | 2 +- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index 0a374b83..3582d361 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -17,6 +17,8 @@ * along with this program. If not, see . */ +import 'dart:io'; + import 'package:bluecherry_client/utils/constants.dart'; import 'package:flutter/foundation.dart'; import 'package:hive_flutter/hive_flutter.dart'; @@ -106,3 +108,35 @@ extension SafeLocalStorageExtension on SafeLocalStorage { return Future.value(); } } + +enum LogType { video, network } + +Future errorLogDirectory() async { + final documentsDir = await getApplicationDocumentsDirectory(); + final logsDir = Directory(path.join(documentsDir.path, 'logs')); + await logsDir.create(recursive: true); + return logsDir; +} + +Future errorLogFile(LogType type) { + return errorLogDirectory().then((dir) async { + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final filename = '${today.toIso8601String()}.log'; + final file = File(path.join(dir.path, type.name, filename)); + await file.create(recursive: true); + return file; + }); +} + +Future errorLog(LogType type, String message) async { + final file = await errorLogFile(type); + + final now = DateTime.now(); + final timestamp = now.toIso8601String(); + final log = '$timestamp: $message\n'; + + await file.writeAsString(log, mode: FileMode.append); + + return file; +} diff --git a/lib/widgets/device_grid/mobile/device_view.dart b/lib/widgets/device_grid/mobile/device_view.dart index c2c7348f..f4538805 100644 --- a/lib/widgets/device_grid/mobile/device_view.dart +++ b/lib/widgets/device_grid/mobile/device_view.dart @@ -256,7 +256,7 @@ class DeviceTileState extends State { const Center( child: CircularProgressIndicator.adaptive( valueColor: AlwaysStoppedAnimation(Colors.white), - strokeWidth: 4.4, + strokeWidth: 1.5, ), ), if (video.lastImageUpdate != null) From 2ed35833918278e024b871b314a1c861b160514e Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Thu, 19 Oct 2023 20:24:07 -0300 Subject: [PATCH 8/8] fix: fullscreen --- lib/providers/home_provider.dart | 2 + lib/widgets/player/live_player.dart | 115 +++++++++++++--------------- 2 files changed, 56 insertions(+), 61 deletions(-) diff --git a/lib/providers/home_provider.dart b/lib/providers/home_provider.dart index 0306e264..7008e512 100644 --- a/lib/providers/home_provider.dart +++ b/lib/providers/home_provider.dart @@ -118,6 +118,8 @@ class HomeProvider extends ChangeNotifier { Future refreshDeviceOrientation(BuildContext context) async { if (isMobile) { + if (Navigator.of(context).canPop()) return; + final home = context.read(); final tab = home.tab; diff --git a/lib/widgets/player/live_player.dart b/lib/widgets/player/live_player.dart index 83e40f5b..d70c3b1e 100644 --- a/lib/widgets/player/live_player.dart +++ b/lib/widgets/player/live_player.dart @@ -22,7 +22,6 @@ import 'dart:async'; import 'package:bluecherry_client/models/device.dart'; import 'package:bluecherry_client/providers/home_provider.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; -import 'package:bluecherry_client/utils/constants.dart'; import 'package:bluecherry_client/utils/methods.dart'; import 'package:bluecherry_client/utils/window.dart'; import 'package:bluecherry_client/widgets/desktop_buttons.dart'; @@ -65,17 +64,39 @@ class LivePlayer extends StatefulWidget { } class _LivePlayerState extends State { + @override + void initState() { + super.initState(); + if (isMobile) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); + HomeProvider.setDefaultStatusBarStyle(); + DeviceOrientations.instance.set([ + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, + ]); + } + } + + @override + void dispose() { + if (isMobile) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + DeviceOrientations.instance.restoreLast(); + } + super.dispose(); + } + @override Widget build(BuildContext context) { return LayoutBuilder(builder: (context, consts) { - if (consts.maxWidth >= kMobileBreakpoint.width) { - return _DesktopLivePlayer( + if (isMobilePlatform) { + return _MobileLivePlayer( player: widget.player, device: widget.device, ptzEnabled: widget.ptzEnabled, ); } else { - return _MobileLivePlayer( + return _DesktopLivePlayer( player: widget.player, device: widget.device, ptzEnabled: widget.ptzEnabled, @@ -109,31 +130,12 @@ class __MobileLivePlayerState extends State<_MobileLivePlayer> { @override void initState() { super.initState(); - if (isMobile) { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); - HomeProvider.setDefaultStatusBarStyle(); - DeviceOrientations.instance.set([ - DeviceOrientation.landscapeLeft, - DeviceOrientation.landscapeRight, - ]); - } - - // Hide the title overlay after 750ms WidgetsBinding.instance.addPostFrameCallback((_) async { await Future.delayed(const Duration(milliseconds: 750)); if (mounted) setState(() => overlay = false); }); } - @override - void dispose() { - if (isMobile) { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - DeviceOrientations.instance.restoreLast(); - } - super.dispose(); - } - void toggleOverlay([PointerDeviceKind? kind]) { if (kind != null && kind != PointerDeviceKind.touch) return; if (mounted) setState(() => overlay = !overlay); @@ -143,40 +145,31 @@ class __MobileLivePlayerState extends State<_MobileLivePlayer> { Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, - body: MouseRegion( - onEnter: (_) { - if (mounted) setState(() => overlay = true); - }, - onHover: (_) { - if (mounted && !overlay) setState(() => overlay = true); - }, - onExit: (_) { - if (mounted) setState(() => overlay = false); - }, - child: GestureDetector( - onTapUp: (event) => toggleOverlay(event.kind), - onLongPressUp: toggleOverlay, - onDoubleTapDown: (event) => toggleOverlay(event.kind), - child: PTZController( - device: widget.device, - enabled: !overlay && ptzEnabled, - builder: (context, commands, constraints) { - return UnityVideoView( - heroTag: widget.device.streamURL, - player: widget.player, - fit: fit, - videoBuilder: (context, child) { - return AnimatedOpacity( - duration: const Duration(milliseconds: 320), - curve: Curves.easeInOut, - opacity: overlay ? 0.75 : 1.0, - child: child, - ); - }, - paneBuilder: (context, controller) { - final error = UnityVideoView.of(context).error; + body: SafeArea( + child: PTZController( + device: widget.device, + enabled: !overlay && ptzEnabled, + builder: (context, commands, constraints) { + return UnityVideoView( + heroTag: widget.device.streamURL, + player: widget.player, + fit: fit, + videoBuilder: (context, child) { + return AnimatedOpacity( + duration: const Duration(milliseconds: 320), + curve: Curves.easeInOut, + opacity: overlay ? 0.75 : 1.0, + child: child, + ); + }, + paneBuilder: (context, controller) { + final error = UnityVideoView.of(context).error; - return Stack(children: [ + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTapDown: (event) => toggleOverlay(event.kind), + child: Stack(children: [ + const Positioned.fill(child: SizedBox.expand()), if (error != null) ErrorWarning(message: error) else if (!controller.isSeekable || @@ -263,11 +256,11 @@ class __MobileLivePlayerState extends State<_MobileLivePlayer> { video: UnityVideoView.of(context), ), ), - ]); - }, - ); - }, - ), + ]), + ); + }, + ); + }, ), ), );