From 94b4de1592c0698fd5f3d4e8dc531ed966536ed2 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Sun, 18 Feb 2024 11:45:06 -0300 Subject: [PATCH 01/26] feat: Add all settings options --- .../settings/desktop/advanced_options.dart | 18 +++++++++++ .../desktop/events_and_downloads.dart | 18 +++++++++++ .../{server.dart => server_and_devices.dart} | 0 lib/screens/settings/desktop/settings.dart | 30 ++++++++++++------- .../{updates.dart => updates_and_help.dart} | 0 5 files changed, 55 insertions(+), 11 deletions(-) create mode 100644 lib/screens/settings/desktop/advanced_options.dart create mode 100644 lib/screens/settings/desktop/events_and_downloads.dart rename lib/screens/settings/desktop/{server.dart => server_and_devices.dart} (100%) rename lib/screens/settings/desktop/{updates.dart => updates_and_help.dart} (100%) diff --git a/lib/screens/settings/desktop/advanced_options.dart b/lib/screens/settings/desktop/advanced_options.dart new file mode 100644 index 00000000..5facc078 --- /dev/null +++ b/lib/screens/settings/desktop/advanced_options.dart @@ -0,0 +1,18 @@ +/* + * 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 . + */ \ No newline at end of file diff --git a/lib/screens/settings/desktop/events_and_downloads.dart b/lib/screens/settings/desktop/events_and_downloads.dart new file mode 100644 index 00000000..5facc078 --- /dev/null +++ b/lib/screens/settings/desktop/events_and_downloads.dart @@ -0,0 +1,18 @@ +/* + * 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 . + */ \ No newline at end of file diff --git a/lib/screens/settings/desktop/server.dart b/lib/screens/settings/desktop/server_and_devices.dart similarity index 100% rename from lib/screens/settings/desktop/server.dart rename to lib/screens/settings/desktop/server_and_devices.dart diff --git a/lib/screens/settings/desktop/settings.dart b/lib/screens/settings/desktop/settings.dart index 96b805a4..a652306f 100644 --- a/lib/screens/settings/desktop/settings.dart +++ b/lib/screens/settings/desktop/settings.dart @@ -18,8 +18,8 @@ */ import 'package:bluecherry_client/screens/settings/desktop/general.dart'; -import 'package:bluecherry_client/screens/settings/desktop/server.dart'; -import 'package:bluecherry_client/screens/settings/desktop/updates.dart'; +import 'package:bluecherry_client/screens/settings/desktop/server_and_devices.dart'; +import 'package:bluecherry_client/screens/settings/desktop/updates_and_help.dart'; import 'package:bluecherry_client/screens/settings/shared/date_language.dart'; import 'package:bluecherry_client/utils/constants.dart'; import 'package:flutter/material.dart'; @@ -55,17 +55,25 @@ class _DesktopSettingsState extends State { icon: const Icon(Icons.dashboard), label: Text(loc.general), ), - NavigationRailDestination( - icon: const Icon(Icons.dns), - label: Text(loc.servers), + const NavigationRailDestination( + icon: Icon(Icons.dns), + label: Text('Servers and Devices'), ), - NavigationRailDestination( - icon: const Icon(Icons.update), - label: Text(loc.updates), + const NavigationRailDestination( + icon: Icon(Icons.event), + label: Text('Events and Downloads'), ), - NavigationRailDestination( - icon: const Icon(Icons.language), - label: Text(loc.dateLanguage), + const NavigationRailDestination( + icon: Icon(Icons.style), + label: Text('Application'), + ), + const NavigationRailDestination( + icon: Icon(Icons.update), + label: Text('Updates and Help'), + ), + const NavigationRailDestination( + icon: Icon(Icons.code), + label: Text('Advanced Options'), ), ], selectedIndex: currentIndex, diff --git a/lib/screens/settings/desktop/updates.dart b/lib/screens/settings/desktop/updates_and_help.dart similarity index 100% rename from lib/screens/settings/desktop/updates.dart rename to lib/screens/settings/desktop/updates_and_help.dart From 0c6b5b966cf39738b1894db6b08a7435f607ced8 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Sun, 18 Feb 2024 12:03:00 -0300 Subject: [PATCH 02/26] feat: Organize General tab --- lib/screens/settings/desktop/general.dart | 70 +++++++++++++--- lib/screens/settings/mobile/settings.dart | 2 +- .../settings/shared/options_chooser_tile.dart | 80 +++++++++++++++++++ lib/screens/settings/shared/tiles.dart | 4 +- 4 files changed, 143 insertions(+), 13 deletions(-) create mode 100644 lib/screens/settings/shared/options_chooser_tile.dart diff --git a/lib/screens/settings/desktop/general.dart b/lib/screens/settings/desktop/general.dart index 6c2ea942..51804530 100644 --- a/lib/screens/settings/desktop/general.dart +++ b/lib/screens/settings/desktop/general.dart @@ -18,6 +18,7 @@ */ import 'package:bluecherry_client/screens/settings/desktop/settings.dart'; +import 'package:bluecherry_client/screens/settings/shared/options_chooser_tile.dart'; import 'package:bluecherry_client/screens/settings/shared/tiles.dart'; import 'package:bluecherry_client/widgets/misc.dart'; import 'package:flutter/material.dart'; @@ -28,21 +29,70 @@ class GeneralSettings extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = Theme.of(context); final loc = AppLocalizations.of(context); return ListView(padding: DesktopSettings.verticalPadding, children: [ - SubHeader( - loc.theme, - subtext: loc.themeDescription, + const CyclePeriodTile(), + const WakelockTile(), + const SubHeader( + 'Notifications', + // subtext: , padding: DesktopSettings.horizontalPadding, ), - ...ThemeMode.values.map((mode) => ThemeTile(themeMode: mode)), - SubHeader(loc.miscellaneous, padding: DesktopSettings.horizontalPadding), + CheckboxListTile.adaptive( + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.crop), + ), + title: const Text('Notifications enabled'), + // subtitle: Text(loc.matrixedViewZoomDescription), + value: true, + onChanged: (value) {}, + ), const SnoozeNotificationsTile(), - const NavigationClickBehaviorTile(), - const DirectoryChooseTile(), - const CyclePeriodTile(), - const CameraReloadPeriodTile(), - const WakelockTile(), + const NotificationClickBehaviorTile(), + const SubHeader( + 'Data Usage', + // subtext: , + padding: DesktopSettings.horizontalPadding, + ), + OptionsChooserTile( + icon: Icons.data_usage, + title: 'Automatic streaming', + description: 'When to stream videos automatically on startup', + selected: '...', + value: '', + values: [ + Option(value: '', icon: Icons.insights, text: 'Auto'), + Option(value: '', icon: Icons.wifi, text: 'Wifi only'), + Option(value: '', icon: Icons.not_interested, text: 'Never'), + ], + onChanged: (value) {}, + ), + OptionsChooserTile( + icon: Icons.cloud_done, + title: 'Keep streams playing on background', + description: + 'When to keep streams playing when the app is in background', + selected: '...', + value: '', + values: [ + Option(value: '', icon: Icons.insights, text: 'Auto'), + Option(value: '', icon: Icons.wifi, text: 'Wifi only'), + Option(value: '', icon: Icons.not_interested, text: 'Never'), + ], + onChanged: (value) {}, + ), + ListTile( + leading: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.show_chart), + ), + title: const Text('View previous data usage'), + trailing: const Icon(Icons.navigate_next), + ), ]); } } diff --git a/lib/screens/settings/mobile/settings.dart b/lib/screens/settings/mobile/settings.dart index 584b4b51..1b7983af 100644 --- a/lib/screens/settings/mobile/settings.dart +++ b/lib/screens/settings/mobile/settings.dart @@ -95,7 +95,7 @@ class _MobileSettingsState extends State { SliverToBoxAdapter(child: SubHeader(loc.miscellaneous)), SliverList.list(children: [ const SnoozeNotificationsTile(), - const NavigationClickBehaviorTile(), + const NotificationClickBehaviorTile(), ExpansionTile( leading: CircleAvatar( backgroundColor: const Color.fromRGBO(0, 0, 0, 0), diff --git a/lib/screens/settings/shared/options_chooser_tile.dart b/lib/screens/settings/shared/options_chooser_tile.dart new file mode 100644 index 00000000..9455fdd2 --- /dev/null +++ b/lib/screens/settings/shared/options_chooser_tile.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; + +class Option { + final T value; + final IconData icon; + final String text; + + Option({ + required this.value, + required this.icon, + required this.text, + }); +} + +class OptionsChooserTile extends StatelessWidget { + final String title; + final String description; + final IconData icon; + + final String selected; + final T value; + final List> values; + final ValueChanged onChanged; + + const OptionsChooserTile({ + super.key, + required this.title, + required this.description, + required this.icon, + required this.selected, + required this.value, + required this.values, + required this.onChanged, + }); + + @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: Icon(icon), + ), + title: Text(title), + textColor: theme.textTheme.bodyLarge?.color, + subtitle: Text( + description, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.textTheme.bodySmall?.color, + ), + ), + trailing: Text(selected), + children: values.map((option) { + return RadioListTile.adaptive( + contentPadding: const EdgeInsetsDirectional.only( + start: 68.0, + end: 16.0, + ), + value: option.value, + groupValue: value, + onChanged: (value) { + if (value != null) { + onChanged(value); + } + }, + secondary: Icon(option.icon), + controlAffinity: ListTileControlAffinity.trailing, + title: Padding( + padding: const EdgeInsetsDirectional.only(start: 16.0), + child: Text(option.text), + ), + ); + }).toList(), + ); + } +} diff --git a/lib/screens/settings/shared/tiles.dart b/lib/screens/settings/shared/tiles.dart index 2da87d94..22993fa0 100644 --- a/lib/screens/settings/shared/tiles.dart +++ b/lib/screens/settings/shared/tiles.dart @@ -131,8 +131,8 @@ class SnoozeNotificationsTile extends StatelessWidget { } } -class NavigationClickBehaviorTile extends StatelessWidget { - const NavigationClickBehaviorTile({super.key}); +class NotificationClickBehaviorTile extends StatelessWidget { + const NotificationClickBehaviorTile({super.key}); @override Widget build(BuildContext context) { From bc452e6c445f8ad1053c4e9c8b7e37e6fa15e9df Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Sun, 18 Feb 2024 19:28:05 -0300 Subject: [PATCH 03/26] feat: Add all options to server and devices --- .../settings/desktop/server_and_devices.dart | 59 ++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/lib/screens/settings/desktop/server_and_devices.dart b/lib/screens/settings/desktop/server_and_devices.dart index 434bd43e..bf0a6f54 100644 --- a/lib/screens/settings/desktop/server_and_devices.dart +++ b/lib/screens/settings/desktop/server_and_devices.dart @@ -54,7 +54,7 @@ class ServerSettings extends StatelessWidget { const SizedBox(height: 12.0), Padding( padding: DesktopSettings.horizontalPadding, - child: Text(loc.camerasSettings, style: theme.textTheme.titleMedium), + child: Text('Devices Settings', style: theme.textTheme.titleMedium), ), const SizedBox(height: 8.0), const Padding( @@ -182,6 +182,33 @@ class CamerasSettings extends StatelessWidget { ), ), const SizedBox(height: 8.0), + Material( + borderRadius: BorderRadius.circular(6.0), + child: ListTile( + title: const Text('Refresh Period'), + subtitle: const Text('How often to refresh the cameras'), + trailing: DropdownButton( + value: Duration.zero, + onChanged: (v) {}, + items: const [ + Duration.zero, + Duration(seconds: 30), + Duration(minutes: 2), + Duration(minutes: 5), + ].map((q) { + return DropdownMenuItem( + value: q, + child: Row(children: [ + // Icon(q.icon), + // const SizedBox(width: 8.0), + Text(q.humanReadableCompact(context)), + ]), + ); + }).toList(), + ), + ), + ), + const SizedBox(height: 8.0), Material( borderRadius: BorderRadius.circular(6.0), child: ListTile( @@ -255,6 +282,36 @@ class CamerasSettings extends StatelessWidget { ), ), ), + const SizedBox(height: 8.0), + Material( + borderRadius: BorderRadius.circular(6.0), + child: CheckboxListTile.adaptive( + title: const Text('Automatically reload timed out streams'), + subtitle: + const Text('When to reload timed out streams automatically'), + value: true, + onChanged: (v) {}, + ), + ), + const SizedBox(height: 8.0), + Material( + borderRadius: BorderRadius.circular(6.0), + child: CheckboxListTile.adaptive( + title: const Text('Hardware rendering'), + subtitle: const Text('Use hardware rendering when available'), + value: true, + onChanged: (v) {}, + ), + ), + const SizedBox(height: 8.0), + Material( + borderRadius: BorderRadius.circular(6.0), + child: ListTile( + title: const Text('Run a video test'), + trailing: const Icon(Icons.play_arrow), + onTap: () {}, + ), + ), ]); } } From a0dcb85935d48f2e1563120a7987e64620bdccdb Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Sun, 18 Feb 2024 19:49:57 -0300 Subject: [PATCH 04/26] feat: Events and downloads settings --- .../desktop/events_and_downloads.dart | 136 +++++++++++++++++- lib/screens/settings/desktop/general.dart | 2 - lib/screens/settings/desktop/settings.dart | 4 +- .../settings/shared/options_chooser_tile.dart | 13 +- lib/widgets/misc.dart | 3 +- 5 files changed, 149 insertions(+), 9 deletions(-) diff --git a/lib/screens/settings/desktop/events_and_downloads.dart b/lib/screens/settings/desktop/events_and_downloads.dart index 5facc078..f9ce6bb9 100644 --- a/lib/screens/settings/desktop/events_and_downloads.dart +++ b/lib/screens/settings/desktop/events_and_downloads.dart @@ -15,4 +15,138 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . - */ \ No newline at end of file + */ + +import 'package:bluecherry_client/screens/settings/desktop/settings.dart'; +import 'package:bluecherry_client/screens/settings/shared/options_chooser_tile.dart'; +import 'package:bluecherry_client/screens/settings/shared/tiles.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class EventsAndDownloadsSettings extends StatelessWidget { + const EventsAndDownloadsSettings({super.key}); + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + final theme = Theme.of(context); + return ListView(padding: DesktopSettings.verticalPadding, children: [ + Padding( + padding: DesktopSettings.horizontalPadding, + child: Text(loc.downloads, style: theme.textTheme.titleMedium), + ), + CheckboxListTile.adaptive( + value: false, + onChanged: (v) {}, + contentPadding: DesktopSettings.horizontalPadding, + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.create_new_folder), + ), + title: const Text('Choose location for each download'), + ), + const DirectoryChooseTile(), + CheckboxListTile.adaptive( + value: false, + onChanged: (v) {}, + contentPadding: DesktopSettings.horizontalPadding, + title: const Text( + 'Block the app from closing when there are ongoing downloads'), + ), + Padding( + padding: DesktopSettings.horizontalPadding, + child: Text('Events', style: theme.textTheme.titleMedium), + ), + CheckboxListTile.adaptive( + value: false, + onChanged: (v) {}, + contentPadding: DesktopSettings.horizontalPadding, + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.picture_in_picture), + ), + title: const Text( + 'Move to picture-in-picture mode when the app moves to background'), + ), + ListTile( + leading: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.speed), + ), + contentPadding: DesktopSettings.horizontalPadding, + title: const Text('Default speed'), + trailing: SizedBox( + width: 200.0, + child: Slider( + value: 1.0, + onChanged: (v) {}, + ), + ), + ), + ListTile( + leading: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.equalizer), + ), + contentPadding: DesktopSettings.horizontalPadding, + title: const Text('Default volume'), + trailing: SizedBox( + width: 200.0, + child: Slider( + value: 1.0, + onChanged: (v) {}, + ), + ), + ), + Padding( + padding: DesktopSettings.horizontalPadding, + child: Text('Timeline of Events', style: theme.textTheme.titleMedium), + ), + CheckboxListTile.adaptive( + value: false, + onChanged: (v) {}, + contentPadding: DesktopSettings.horizontalPadding, + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.color_lens), + ), + title: const Text('Show different colors for events'), + subtitle: const Text( + 'Whether to show different colors for events in the timeline. ' + 'This will help you to easily identify the events.', + ), + ), + CheckboxListTile.adaptive( + value: false, + onChanged: (v) {}, + contentPadding: DesktopSettings.horizontalPadding, + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.pause_presentation), + ), + title: const Text('Pause to buffer'), + subtitle: const Text( + 'Whether the entire timeline should pause to buffer the events.', + ), + ), + OptionsChooserTile( + title: 'Initial point', + description: 'When the timeline should begin.', + icon: Icons.flag, + value: '', + values: [ + Option(value: '', icon: Icons.start, text: 'Beginning'), + Option(value: '', icon: Icons.first_page, text: 'First event'), + Option(value: '', icon: Icons.hourglass_bottom, text: 'An hour ago'), + ], + onChanged: (v) {}, + ), + ]); + } +} diff --git a/lib/screens/settings/desktop/general.dart b/lib/screens/settings/desktop/general.dart index 51804530..2fcd72b8 100644 --- a/lib/screens/settings/desktop/general.dart +++ b/lib/screens/settings/desktop/general.dart @@ -61,7 +61,6 @@ class GeneralSettings extends StatelessWidget { icon: Icons.data_usage, title: 'Automatic streaming', description: 'When to stream videos automatically on startup', - selected: '...', value: '', values: [ Option(value: '', icon: Icons.insights, text: 'Auto'), @@ -75,7 +74,6 @@ class GeneralSettings extends StatelessWidget { title: 'Keep streams playing on background', description: 'When to keep streams playing when the app is in background', - selected: '...', value: '', values: [ Option(value: '', icon: Icons.insights, text: 'Auto'), diff --git a/lib/screens/settings/desktop/settings.dart b/lib/screens/settings/desktop/settings.dart index a652306f..d38924ec 100644 --- a/lib/screens/settings/desktop/settings.dart +++ b/lib/screens/settings/desktop/settings.dart @@ -17,6 +17,7 @@ * along with this program. If not, see . */ +import 'package:bluecherry_client/screens/settings/desktop/events_and_downloads.dart'; import 'package:bluecherry_client/screens/settings/desktop/general.dart'; import 'package:bluecherry_client/screens/settings/desktop/server_and_devices.dart'; import 'package:bluecherry_client/screens/settings/desktop/updates_and_help.dart'; @@ -104,7 +105,8 @@ class _DesktopSettingsState extends State { child: switch (currentIndex) { 0 => const GeneralSettings(), 1 => const ServerSettings(), - 2 => const UpdatesSettings(), + 2 => const EventsAndDownloadsSettings(), + 4 => const UpdatesSettings(), 3 => const LocalizationSettings(), _ => const GeneralSettings(), }, diff --git a/lib/screens/settings/shared/options_chooser_tile.dart b/lib/screens/settings/shared/options_chooser_tile.dart index 9455fdd2..5d181dcd 100644 --- a/lib/screens/settings/shared/options_chooser_tile.dart +++ b/lib/screens/settings/shared/options_chooser_tile.dart @@ -1,3 +1,4 @@ +import 'package:bluecherry_client/screens/settings/desktop/settings.dart'; import 'package:flutter/material.dart'; class Option { @@ -17,7 +18,6 @@ class OptionsChooserTile extends StatelessWidget { final String description; final IconData icon; - final String selected; final T value; final List> values; final ValueChanged onChanged; @@ -27,7 +27,6 @@ class OptionsChooserTile extends StatelessWidget { required this.title, required this.description, required this.icon, - required this.selected, required this.value, required this.values, required this.onChanged, @@ -53,11 +52,17 @@ class OptionsChooserTile extends StatelessWidget { color: theme.textTheme.bodySmall?.color, ), ), - trailing: Text(selected), + tilePadding: DesktopSettings.horizontalPadding, + trailing: Text(values + .firstWhere( + (v) => v.text == value, + orElse: () => values.first, + ) + .text), children: values.map((option) { return RadioListTile.adaptive( contentPadding: const EdgeInsetsDirectional.only( - start: 68.0, + start: 76.0, end: 16.0, ), value: option.value, diff --git a/lib/widgets/misc.dart b/lib/widgets/misc.dart index df90e138..625e2674 100644 --- a/lib/widgets/misc.dart +++ b/lib/widgets/misc.dart @@ -19,6 +19,7 @@ import 'dart:async'; +import 'package:bluecherry_client/screens/settings/desktop/settings.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/utils/methods.dart'; import 'package:bluecherry_client/widgets/squared_icon_button.dart'; @@ -131,7 +132,7 @@ class CorrectedListTile extends StatelessWidget { minWidth: MediaQuery.sizeOf(context).width, maxWidth: MediaQuery.sizeOf(context).width, ), - padding: const EdgeInsetsDirectional.only(start: 16.0), + padding: DesktopSettings.horizontalPadding, child: Row(children: [ Container( margin: const EdgeInsetsDirectional.only(end: 12.0), From 6d3eff77c1d5335caa328f5a177e79bb90e769dd Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Sun, 18 Feb 2024 19:57:04 -0300 Subject: [PATCH 05/26] feat: Organize Application section --- lib/screens/settings/desktop/application.dart | 88 +++++++++++++++++++ lib/screens/settings/desktop/settings.dart | 3 +- 2 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 lib/screens/settings/desktop/application.dart diff --git a/lib/screens/settings/desktop/application.dart b/lib/screens/settings/desktop/application.dart new file mode 100644 index 00000000..b197a624 --- /dev/null +++ b/lib/screens/settings/desktop/application.dart @@ -0,0 +1,88 @@ +/* + * 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 . + */ + +import 'package:bluecherry_client/screens/settings/desktop/settings.dart'; +import 'package:bluecherry_client/screens/settings/shared/date_language.dart'; +import 'package:bluecherry_client/screens/settings/shared/options_chooser_tile.dart'; +import 'package:bluecherry_client/screens/settings/shared/tiles.dart'; +import 'package:bluecherry_client/widgets/misc.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class ApplicationSettings extends StatelessWidget { + const ApplicationSettings({super.key}); + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + final theme = Theme.of(context); + return ListView(padding: DesktopSettings.verticalPadding, children: [ + SubHeader( + loc.theme, + subtext: loc.themeDescription, + padding: DesktopSettings.horizontalPadding, + ), + ...ThemeMode.values.map((mode) => ThemeTile(themeMode: mode)), + const LanguageSection(), + Padding( + padding: DesktopSettings.horizontalPadding, + child: Text(loc.dateFormat, style: theme.textTheme.titleMedium), + ), + const DateFormatSection(), + Padding( + padding: DesktopSettings.horizontalPadding, + child: Text(loc.timeFormat, style: theme.textTheme.titleMedium), + ), + const TimeFormatSection(), + Padding( + padding: DesktopSettings.horizontalPadding, + child: Text('Window', style: theme.textTheme.titleMedium), + ), + CheckboxListTile.adaptive( + value: false, + onChanged: (v) {}, + contentPadding: DesktopSettings.horizontalPadding, + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.launch), + ), + title: const Text('Launch app on startup'), + subtitle: const Text( + 'Whether to launchthe app when the system starts', + ), + ), + CheckboxListTile.adaptive( + value: false, + onChanged: (v) {}, + contentPadding: DesktopSettings.horizontalPadding, + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.sensor_door), + ), + title: const Text('Close app to tray'), + subtitle: const Text( + 'Whether to close the app to the system tray when the window is closed. ' + 'This will keep the app running in the background.', + ), + ), + ]); + } +} diff --git a/lib/screens/settings/desktop/settings.dart b/lib/screens/settings/desktop/settings.dart index d38924ec..b1274dfd 100644 --- a/lib/screens/settings/desktop/settings.dart +++ b/lib/screens/settings/desktop/settings.dart @@ -17,6 +17,7 @@ * along with this program. If not, see . */ +import 'package:bluecherry_client/screens/settings/desktop/application.dart'; import 'package:bluecherry_client/screens/settings/desktop/events_and_downloads.dart'; import 'package:bluecherry_client/screens/settings/desktop/general.dart'; import 'package:bluecherry_client/screens/settings/desktop/server_and_devices.dart'; @@ -106,8 +107,8 @@ class _DesktopSettingsState extends State { 0 => const GeneralSettings(), 1 => const ServerSettings(), 2 => const EventsAndDownloadsSettings(), + 3 => const ApplicationSettings(), 4 => const UpdatesSettings(), - 3 => const LocalizationSettings(), _ => const GeneralSettings(), }, ), From a0aadd9f8fdc013ed5fad2a415f87cce73794da9 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Sun, 18 Feb 2024 20:28:53 -0300 Subject: [PATCH 06/26] feat: Organize sections --- lib/screens/settings/desktop/application.dart | 40 +- .../desktop/events_and_downloads.dart | 19 +- lib/screens/settings/desktop/general.dart | 5 +- .../settings/desktop/server_and_devices.dart | 410 +++++++++--------- .../settings/shared/date_language.dart | 68 +-- .../settings/shared/options_chooser_tile.dart | 26 +- lib/screens/settings/shared/tiles.dart | 3 + 7 files changed, 306 insertions(+), 265 deletions(-) diff --git a/lib/screens/settings/desktop/application.dart b/lib/screens/settings/desktop/application.dart index b197a624..6cc351b6 100644 --- a/lib/screens/settings/desktop/application.dart +++ b/lib/screens/settings/desktop/application.dart @@ -17,13 +17,13 @@ * along with this program. If not, see . */ +import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/screens/settings/desktop/settings.dart'; import 'package:bluecherry_client/screens/settings/shared/date_language.dart'; import 'package:bluecherry_client/screens/settings/shared/options_chooser_tile.dart'; -import 'package:bluecherry_client/screens/settings/shared/tiles.dart'; -import 'package:bluecherry_client/widgets/misc.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:provider/provider.dart'; class ApplicationSettings extends StatelessWidget { const ApplicationSettings({super.key}); @@ -32,23 +32,33 @@ class ApplicationSettings extends StatelessWidget { Widget build(BuildContext context) { final loc = AppLocalizations.of(context); final theme = Theme.of(context); + final settings = context.watch(); return ListView(padding: DesktopSettings.verticalPadding, children: [ - SubHeader( - loc.theme, - subtext: loc.themeDescription, - padding: DesktopSettings.horizontalPadding, + OptionsChooserTile( + title: 'Theme', + icon: Icons.contrast, + value: settings.themeMode, + values: ThemeMode.values.map((mode) { + return Option( + value: mode, + icon: switch (mode) { + ThemeMode.system => Icons.brightness_auto, + ThemeMode.light => Icons.light_mode, + ThemeMode.dark => Icons.dark_mode, + }, + text: switch (mode) { + ThemeMode.system => loc.system, + ThemeMode.light => loc.light, + ThemeMode.dark => loc.dark, + }, + ); + }), + onChanged: (v) { + settings.themeMode = v; + }, ), - ...ThemeMode.values.map((mode) => ThemeTile(themeMode: mode)), const LanguageSection(), - Padding( - padding: DesktopSettings.horizontalPadding, - child: Text(loc.dateFormat, style: theme.textTheme.titleMedium), - ), const DateFormatSection(), - Padding( - padding: DesktopSettings.horizontalPadding, - child: Text(loc.timeFormat, style: theme.textTheme.titleMedium), - ), const TimeFormatSection(), Padding( padding: DesktopSettings.horizontalPadding, diff --git a/lib/screens/settings/desktop/events_and_downloads.dart b/lib/screens/settings/desktop/events_and_downloads.dart index f9ce6bb9..7705b4e8 100644 --- a/lib/screens/settings/desktop/events_and_downloads.dart +++ b/lib/screens/settings/desktop/events_and_downloads.dart @@ -51,9 +51,15 @@ class EventsAndDownloadsSettings extends StatelessWidget { value: false, onChanged: (v) {}, contentPadding: DesktopSettings.horizontalPadding, + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.close), + ), title: const Text( 'Block the app from closing when there are ongoing downloads'), ), + const SizedBox(height: 20.0), Padding( padding: DesktopSettings.horizontalPadding, child: Text('Events', style: theme.textTheme.titleMedium), @@ -67,8 +73,10 @@ class EventsAndDownloadsSettings extends StatelessWidget { foregroundColor: theme.iconTheme.color, child: const Icon(Icons.picture_in_picture), ), - title: const Text( - 'Move to picture-in-picture mode when the app moves to background'), + title: const Text('Picture-in-picture'), + subtitle: const Text( + 'Move to picture-in-picture mode when the app moves to background.', + ), ), ListTile( leading: CircleAvatar( @@ -78,8 +86,9 @@ class EventsAndDownloadsSettings extends StatelessWidget { ), contentPadding: DesktopSettings.horizontalPadding, title: const Text('Default speed'), + subtitle: const Text('1.0'), trailing: SizedBox( - width: 200.0, + width: 160.0, child: Slider( value: 1.0, onChanged: (v) {}, @@ -94,14 +103,16 @@ class EventsAndDownloadsSettings extends StatelessWidget { ), contentPadding: DesktopSettings.horizontalPadding, title: const Text('Default volume'), + subtitle: const Text('1.0'), trailing: SizedBox( - width: 200.0, + width: 160.0, child: Slider( value: 1.0, onChanged: (v) {}, ), ), ), + const SizedBox(height: 20.0), Padding( padding: DesktopSettings.horizontalPadding, child: Text('Timeline of Events', style: theme.textTheme.titleMedium), diff --git a/lib/screens/settings/desktop/general.dart b/lib/screens/settings/desktop/general.dart index 2fcd72b8..f60ba930 100644 --- a/lib/screens/settings/desktop/general.dart +++ b/lib/screens/settings/desktop/general.dart @@ -36,7 +36,6 @@ class GeneralSettings extends StatelessWidget { const WakelockTile(), const SubHeader( 'Notifications', - // subtext: , padding: DesktopSettings.horizontalPadding, ), CheckboxListTile.adaptive( @@ -45,8 +44,8 @@ class GeneralSettings extends StatelessWidget { foregroundColor: theme.iconTheme.color, child: const Icon(Icons.crop), ), + contentPadding: DesktopSettings.horizontalPadding, title: const Text('Notifications enabled'), - // subtitle: Text(loc.matrixedViewZoomDescription), value: true, onChanged: (value) {}, ), @@ -54,7 +53,6 @@ class GeneralSettings extends StatelessWidget { const NotificationClickBehaviorTile(), const SubHeader( 'Data Usage', - // subtext: , padding: DesktopSettings.horizontalPadding, ), OptionsChooserTile( @@ -88,6 +86,7 @@ class GeneralSettings extends StatelessWidget { foregroundColor: theme.iconTheme.color, child: const Icon(Icons.show_chart), ), + contentPadding: DesktopSettings.horizontalPadding, title: const Text('View previous data usage'), trailing: const Icon(Icons.navigate_next), ), diff --git a/lib/screens/settings/desktop/server_and_devices.dart b/lib/screens/settings/desktop/server_and_devices.dart index bf0a6f54..a06f0083 100644 --- a/lib/screens/settings/desktop/server_and_devices.dart +++ b/lib/screens/settings/desktop/server_and_devices.dart @@ -47,20 +47,14 @@ class ServerSettings extends StatelessWidget { child: Text(loc.streamingSettings, style: theme.textTheme.titleMedium), ), const SizedBox(height: 8.0), - const Padding( - padding: DesktopSettings.horizontalPadding, - child: StreamingSettings(), - ), + const StreamingSettings(), const SizedBox(height: 12.0), Padding( padding: DesktopSettings.horizontalPadding, child: Text('Devices Settings', style: theme.textTheme.titleMedium), ), const SizedBox(height: 8.0), - const Padding( - padding: DesktopSettings.horizontalPadding, - child: CamerasSettings(), - ), + const CamerasSettings(), ]); } } @@ -72,52 +66,59 @@ class StreamingSettings extends StatelessWidget { Widget build(BuildContext context) { final settings = context.watch(); final loc = AppLocalizations.of(context); + final theme = Theme.of(context); return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Material( - borderRadius: BorderRadius.circular(6.0), - child: ListTile( - title: Text(loc.streamingType), - trailing: DropdownButton( - value: settings.streamingType, - onChanged: (v) { - if (v != null) { - settings.streamingType = v; - } - }, - items: StreamingType.values.map((value) { - return DropdownMenuItem( - value: value, - // Disable RTSP on web - enabled: !kIsWeb || value != StreamingType.rtsp, - child: Text(value.name.toUpperCase()), - ); - }).toList(), - ), + ListTile( + leading: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.sensors), + ), + title: Text(loc.streamingType), + contentPadding: DesktopSettings.horizontalPadding, + trailing: DropdownButton( + value: settings.streamingType, + onChanged: (v) { + if (v != null) { + settings.streamingType = v; + } + }, + items: StreamingType.values.map((value) { + return DropdownMenuItem( + value: value, + // Disable RTSP on web + enabled: !kIsWeb || value != StreamingType.rtsp, + child: Text(value.name.toUpperCase()), + ); + }).toList(), ), ), const SizedBox(height: 8.0), - Material( - borderRadius: BorderRadius.circular(6.0), - child: ListTile( - enabled: settings.streamingType == StreamingType.rtsp, - title: Text(loc.rtspProtocol), - trailing: DropdownButton( - value: settings.rtspProtocol, - onChanged: !kIsWeb && settings.streamingType == StreamingType.rtsp - ? (v) { - if (v != null) { - settings.rtspProtocol = v; - } + ListTile( + enabled: settings.streamingType == StreamingType.rtsp, + title: Text(loc.rtspProtocol), + leading: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.sensors), + ), + contentPadding: DesktopSettings.horizontalPadding, + trailing: DropdownButton( + value: settings.rtspProtocol, + onChanged: !kIsWeb && settings.streamingType == StreamingType.rtsp + ? (v) { + if (v != null) { + settings.rtspProtocol = v; } - : null, - items: RTSPProtocol.values.map((p) { - return DropdownMenuItem( - value: p, - child: Text(p.name.toUpperCase()), - ); - }).toList(), - ), + } + : null, + items: RTSPProtocol.values.map((p) { + return DropdownMenuItem( + value: p, + child: Text(p.name.toUpperCase()), + ); + }).toList(), ), ), ]); @@ -134,183 +135,198 @@ class CamerasSettings extends StatelessWidget { final loc = AppLocalizations.of(context); return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!kIsWeb) - Material( - borderRadius: BorderRadius.circular(6.0), - child: ListTile( - title: Text(loc.renderingQuality), - subtitle: Text(loc.renderingQualityDescription), - trailing: DropdownButton( - value: settings.videoQuality, - onChanged: (v) { - if (v != null) { - settings.videoQuality = v; - } - }, - items: RenderingQuality.values.map((q) { - return DropdownMenuItem( - value: q, - child: Text(q.locale(context)), - ); - }).toList(), - ), + ListTile( + leading: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.hd), ), - ), - const SizedBox(height: 8.0), - Material( - borderRadius: BorderRadius.circular(6.0), - child: ListTile( - title: Text(loc.cameraViewFit), - subtitle: Text(loc.cameraViewFitDescription), - trailing: DropdownButton( - value: settings.cameraViewFit, + title: Text(loc.renderingQuality), + subtitle: Text(loc.renderingQualityDescription), + contentPadding: DesktopSettings.horizontalPadding, + trailing: DropdownButton( + value: settings.videoQuality, onChanged: (v) { if (v != null) { - settings.cameraViewFit = v; + settings.videoQuality = v; } }, - items: UnityVideoFit.values.map((q) { + items: RenderingQuality.values.map((q) { return DropdownMenuItem( value: q, - child: Row(children: [ - Icon(q.icon), - const SizedBox(width: 8.0), - Text(q.locale(context)), - ]), + child: Text(q.locale(context)), ); }).toList(), ), ), + const SizedBox(height: 8.0), + ListTile( + leading: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.fit_screen), + ), + title: Text(loc.cameraViewFit), + subtitle: Text(loc.cameraViewFitDescription), + contentPadding: DesktopSettings.horizontalPadding, + trailing: DropdownButton( + value: settings.cameraViewFit, + onChanged: (v) { + if (v != null) { + settings.cameraViewFit = v; + } + }, + items: UnityVideoFit.values.map((q) { + return DropdownMenuItem( + value: q, + child: Row(children: [ + Icon(q.icon), + const SizedBox(width: 8.0), + Text(q.locale(context)), + ]), + ); + }).toList(), + ), ), const SizedBox(height: 8.0), - Material( - borderRadius: BorderRadius.circular(6.0), - child: ListTile( - title: const Text('Refresh Period'), - subtitle: const Text('How often to refresh the cameras'), - trailing: DropdownButton( - value: Duration.zero, - onChanged: (v) {}, - items: const [ - Duration.zero, - Duration(seconds: 30), - Duration(minutes: 2), - Duration(minutes: 5), - ].map((q) { - return DropdownMenuItem( - value: q, - child: Row(children: [ - // Icon(q.icon), - // const SizedBox(width: 8.0), - Text(q.humanReadableCompact(context)), - ]), - ); - }).toList(), - ), + ListTile( + leading: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.sync), + ), + title: const Text('Refresh Period'), + subtitle: const Text('How often to refresh the cameras'), + contentPadding: DesktopSettings.horizontalPadding, + trailing: DropdownButton( + value: Duration.zero, + onChanged: (v) {}, + items: const [ + Duration.zero, + Duration(seconds: 30), + Duration(minutes: 2), + Duration(minutes: 5), + ].map((q) { + return DropdownMenuItem( + value: q, + child: Row(children: [ + // Icon(q.icon), + // const SizedBox(width: 8.0), + Text(q.humanReadableCompact(context)), + ]), + ); + }).toList(), ), ), const SizedBox(height: 8.0), - Material( - borderRadius: BorderRadius.circular(6.0), - child: ListTile( - title: Text(loc.lateStreamBehavior), - subtitle: RichText( - text: TextSpan( - text: loc.lateStreamBehaviorDescription, - style: theme.textTheme.bodyMedium, - children: [ - const TextSpan(text: '\n'), - switch (settings.lateVideoBehavior) { - LateVideoBehavior.automatic => TextSpan( - text: loc.automaticBehaviorDescription, - style: const TextStyle(fontWeight: FontWeight.w600), - ), - LateVideoBehavior.manual => TextSpan( - children: [ - ...() { - final list = loc - .manualBehaviorDescription( - 'manualBehaviorDescription', - ) - .split(' '); + ListTile( + leading: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.watch_later), + ), + title: Text(loc.lateStreamBehavior), + subtitle: RichText( + text: TextSpan( + text: loc.lateStreamBehaviorDescription, + style: theme.textTheme.bodyMedium, + children: [ + const TextSpan(text: '\n'), + switch (settings.lateVideoBehavior) { + LateVideoBehavior.automatic => TextSpan( + text: loc.automaticBehaviorDescription, + style: const TextStyle(fontWeight: FontWeight.w600), + ), + LateVideoBehavior.manual => TextSpan( + children: [ + ...() { + final list = loc + .manualBehaviorDescription( + 'manualBehaviorDescription', + ) + .split(' '); - return list.map((part) { - if (part == 'manualBehaviorDescription') { - return const WidgetSpan( - child: Padding( - padding: EdgeInsetsDirectional.only( - start: 2.0, - end: 6.0, - ), - child: VideoStatusLabelIndicator( - status: VideoLabel.late, - ), + return list.map((part) { + if (part == 'manualBehaviorDescription') { + return const WidgetSpan( + child: Padding( + padding: EdgeInsetsDirectional.only( + start: 2.0, + end: 6.0, ), - ); - } else { - return TextSpan(text: '$part '); - } - }); - }() - ], - style: const TextStyle(fontWeight: FontWeight.w600), - ), - LateVideoBehavior.never => TextSpan( - text: loc.neverBehaviorDescription, - style: const TextStyle(fontWeight: FontWeight.w600), - ) - }, - ], - ), - ), - trailing: DropdownButton( - value: settings.lateVideoBehavior, - onChanged: (v) { - if (v != null) { - settings.lateVideoBehavior = v; - } - }, - items: LateVideoBehavior.values.map((q) { - return DropdownMenuItem( - value: q, - child: Row(children: [ - Icon(q.icon), - const SizedBox(width: 8.0), - Text(q.locale(context)), - ]), - ); - }).toList(), + child: VideoStatusLabelIndicator( + status: VideoLabel.late, + ), + ), + ); + } else { + return TextSpan(text: '$part '); + } + }); + }() + ], + style: const TextStyle(fontWeight: FontWeight.w600), + ), + LateVideoBehavior.never => TextSpan( + text: loc.neverBehaviorDescription, + style: const TextStyle(fontWeight: FontWeight.w600), + ) + }, + ], ), ), + contentPadding: DesktopSettings.horizontalPadding, + trailing: DropdownButton( + value: settings.lateVideoBehavior, + onChanged: (v) { + if (v != null) { + settings.lateVideoBehavior = v; + } + }, + items: LateVideoBehavior.values.map((q) { + return DropdownMenuItem( + value: q, + child: Row(children: [ + Icon(q.icon), + const SizedBox(width: 8.0), + Text(q.locale(context)), + ]), + ); + }).toList(), + ), ), const SizedBox(height: 8.0), - Material( - borderRadius: BorderRadius.circular(6.0), - child: CheckboxListTile.adaptive( - title: const Text('Automatically reload timed out streams'), - subtitle: - const Text('When to reload timed out streams automatically'), - value: true, - onChanged: (v) {}, + CheckboxListTile.adaptive( + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.history), ), + title: const Text('Automatically reload timed out streams'), + subtitle: const Text('When to reload timed out streams automatically'), + contentPadding: DesktopSettings.horizontalPadding, + value: true, + onChanged: (v) {}, ), const SizedBox(height: 8.0), - Material( - borderRadius: BorderRadius.circular(6.0), - child: CheckboxListTile.adaptive( - title: const Text('Hardware rendering'), - subtitle: const Text('Use hardware rendering when available'), - value: true, - onChanged: (v) {}, + CheckboxListTile.adaptive( + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.memory), ), + title: const Text('Hardware rendering'), + subtitle: const Text('Use hardware rendering when available'), + contentPadding: DesktopSettings.horizontalPadding, + value: true, + onChanged: (v) {}, ), const SizedBox(height: 8.0), - Material( - borderRadius: BorderRadius.circular(6.0), - child: ListTile( - title: const Text('Run a video test'), - trailing: const Icon(Icons.play_arrow), - onTap: () {}, - ), + ListTile( + title: const Text('Run a video test'), + trailing: const Icon(Icons.play_arrow), + contentPadding: DesktopSettings.horizontalPadding, + onTap: () {}, ), ]); } diff --git a/lib/screens/settings/shared/date_language.dart b/lib/screens/settings/shared/date_language.dart index f971186e..e418219d 100644 --- a/lib/screens/settings/shared/date_language.dart +++ b/lib/screens/settings/shared/date_language.dart @@ -17,9 +17,9 @@ * 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/screens/settings/desktop/settings.dart'; +import 'package:bluecherry_client/screens/settings/shared/options_chooser_tile.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -101,7 +101,13 @@ class LanguageSection extends StatelessWidget { return DropdownButtonHideUnderline( child: ListTile( - title: Text(loc.language, style: theme.textTheme.titleMedium), + contentPadding: DesktopSettings.horizontalPadding, + leading: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.language), + ), + title: Text(loc.language), trailing: DropdownButton( value: currentLocale, onChanged: (value) => settings.locale = value!, @@ -160,27 +166,20 @@ class DateFormatSection extends StatelessWidget { ].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.adaptive( - 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 ?? ''), - ), + return OptionsChooserTile( + title: 'Date Format', + description: 'What format to use for displaying dates', + icon: Icons.calendar_month, + value: '', + values: formats.map((format) { + return Option( + value: format.pattern, + text: format.format(DateTime.utc(1969, 7, 20, 14, 18, 04)), ); - }).toList(), + }), + onChanged: (v) { + settings.dateFormat = DateFormat(v!, locale); + }, ); } else { return Column( @@ -215,19 +214,20 @@ class TimeFormatSection extends StatelessWidget { 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 ?? ''), + return OptionsChooserTile( + title: 'Time Format', + description: 'What format to use for displaying time', + icon: Icons.hourglass_empty, + value: '', + values: patterns.map((pattern) { + return Option( + value: pattern.pattern, + text: pattern.format(date), ); - }).toList(), + }), + onChanged: (v) { + settings.timeFormat = DateFormat(v!, locale); + }, ); }); } diff --git a/lib/screens/settings/shared/options_chooser_tile.dart b/lib/screens/settings/shared/options_chooser_tile.dart index 5d181dcd..ae873c83 100644 --- a/lib/screens/settings/shared/options_chooser_tile.dart +++ b/lib/screens/settings/shared/options_chooser_tile.dart @@ -3,29 +3,29 @@ import 'package:flutter/material.dart'; class Option { final T value; - final IconData icon; + final IconData? icon; final String text; Option({ required this.value, - required this.icon, required this.text, + this.icon, }); } class OptionsChooserTile extends StatelessWidget { final String title; - final String description; + final String? description; final IconData icon; final T value; - final List> values; + final Iterable> values; final ValueChanged onChanged; const OptionsChooserTile({ super.key, required this.title, - required this.description, + this.description, required this.icon, required this.value, required this.values, @@ -46,12 +46,14 @@ class OptionsChooserTile extends StatelessWidget { ), title: Text(title), textColor: theme.textTheme.bodyLarge?.color, - subtitle: Text( - description, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.textTheme.bodySmall?.color, - ), - ), + subtitle: description == null + ? null + : Text( + description!, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.textTheme.bodySmall?.color, + ), + ), tilePadding: DesktopSettings.horizontalPadding, trailing: Text(values .firstWhere( @@ -72,7 +74,7 @@ class OptionsChooserTile extends StatelessWidget { onChanged(value); } }, - secondary: Icon(option.icon), + secondary: option.icon == null ? null : Icon(option.icon), controlAffinity: ListTileControlAffinity.trailing, title: Padding( padding: const EdgeInsetsDirectional.only(start: 16.0), diff --git a/lib/screens/settings/shared/tiles.dart b/lib/screens/settings/shared/tiles.dart index 22993fa0..d6f32f99 100644 --- a/lib/screens/settings/shared/tiles.dart +++ b/lib/screens/settings/shared/tiles.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/screens/layouts/desktop/stream_data.dart'; +import 'package:bluecherry_client/screens/settings/desktop/settings.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/widgets/misc.dart'; import 'package:file_picker/file_picker.dart'; @@ -154,6 +155,7 @@ class NotificationClickBehaviorTile extends StatelessWidget { color: theme.textTheme.bodySmall?.color, ), ), + tilePadding: DesktopSettings.horizontalPadding, trailing: Text(settings.notificationClickBehavior.locale(context)), children: NotificationClickBehavior.values.map((behavior) { return RadioListTile.adaptive( @@ -207,6 +209,7 @@ class CyclePeriodTile extends StatelessWidget { trailing: Text( settings.layoutCyclingTogglePeriod.humanReadableCompact(context), ), + tilePadding: DesktopSettings.horizontalPadding, childrenPadding: const EdgeInsetsDirectional.all(12.0), children: [ ToggleButtons( From a20422aa9534a4786a9f35b6291cd79b9f7ecc61 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Mon, 19 Feb 2024 11:57:07 -0300 Subject: [PATCH 07/26] feat: Update Updates section --- .../settings/desktop/updates_and_help.dart | 10 +-- lib/screens/settings/shared/update.dart | 78 ++++++++++++++++--- 2 files changed, 71 insertions(+), 17 deletions(-) diff --git a/lib/screens/settings/desktop/updates_and_help.dart b/lib/screens/settings/desktop/updates_and_help.dart index ccdb3948..c8d15fa6 100644 --- a/lib/screens/settings/desktop/updates_and_help.dart +++ b/lib/screens/settings/desktop/updates_and_help.dart @@ -69,11 +69,11 @@ class UpdatesSettings extends StatelessWidget { ], // TODO(bdlukaa): Show option to downlaod the native client when running // on the web. - Padding( - padding: DesktopSettings.horizontalPadding, - child: Text('Beta Features', style: theme.textTheme.titleMedium), - ), - const BetaFeatures(), + // Padding( + // padding: DesktopSettings.horizontalPadding, + // child: Text('Beta Features', style: theme.textTheme.titleMedium), + // ), + // const BetaFeatures(), const Divider(), const About(), ]); diff --git a/lib/screens/settings/shared/update.dart b/lib/screens/settings/shared/update.dart index 00a7666d..ee295034 100644 --- a/lib/screens/settings/shared/update.dart +++ b/lib/screens/settings/shared/update.dart @@ -247,6 +247,7 @@ class AppUpdateOptions extends StatelessWidget { foregroundColor: theme.iconTheme.color, child: const Icon(Icons.podcasts), ), + contentPadding: DesktopSettings.horizontalPadding, title: Text(loc.automaticDownloadUpdates), subtitle: RichText( text: TextSpan( @@ -268,12 +269,27 @@ class AppUpdateOptions extends StatelessWidget { ), isThreeLine: true, ), + CheckboxListTile.adaptive( + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.memory), + ), + title: const Text('Show release notes'), + subtitle: const Text( + 'Display release notes when a new version is downloaded', + ), + contentPadding: DesktopSettings.horizontalPadding, + value: true, + onChanged: (v) {}, + ), ListTile( leading: CircleAvatar( backgroundColor: Colors.transparent, foregroundColor: theme.iconTheme.color, child: const Icon(Icons.history), ), + contentPadding: DesktopSettings.horizontalPadding, title: Text(loc.updateHistory), trailing: const Icon(Icons.navigate_next), onTap: () => showUpdateHistory(context), @@ -357,7 +373,7 @@ class About extends StatelessWidget { final update = context.watch(); return Padding( - padding: const EdgeInsetsDirectional.symmetric(horizontal: 24.0), + padding: DesktopSettings.horizontalPadding, child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 8.0), if (update.packageInfo != null) ...[ @@ -369,22 +385,60 @@ class About extends StatelessWidget { ), ], const SizedBox(height: 8.0), - Link( - uri: Uri.https('www.bluecherrydvr.com', '/'), - builder: (context, open) { - return MaterialButton( - onPressed: open, - padding: EdgeInsetsDirectional.zero, - minWidth: 0.0, + Row( + children: [ + Link( + uri: Uri.https('www.bluecherrydvr.com', '/'), + builder: (context, open) { + return TextButton( + onPressed: open, + child: Text( + loc.website, + semanticsLabel: 'www.bluecherrydvr.com', + style: TextStyle( + color: theme.colorScheme.primary, + ), + ), + ); + }, + ), + const SizedBox(width: 8.0), + Link( + uri: Uri.https('www.bluecherrydvr.com', '/contact'), + builder: (context, open) { + return TextButton( + onPressed: open, + child: Text( + 'Help', + semanticsLabel: 'www.bluecherrydvr.com/contact', + style: TextStyle( + color: theme.colorScheme.primary, + ), + ), + ); + }, + ), + const SizedBox(width: 8.0), + TextButton( + onPressed: () { + showLicensePage( + context: context, + applicationName: 'Bluecherry Client', + applicationIcon: Image.asset( + 'assets/images/icon.png', + ), + applicationVersion: update.packageInfo?.version, + applicationLegalese: '© 2022 Bluecherry, LLC', + ); + }, child: Text( - loc.website, - semanticsLabel: 'www.bluecherrydvr.com', + 'Licenses', style: TextStyle( color: theme.colorScheme.primary, ), ), - ); - }, + ), + ], ), ]), ); From baef9bbddb2180c47b5a5e17d4d4c60d9dacec59 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Mon, 19 Feb 2024 12:09:42 -0300 Subject: [PATCH 08/26] feat: Add Advanced Options section --- .../settings/desktop/advanced_options.dart | 134 +++++++++++++++++- lib/screens/settings/desktop/settings.dart | 3 +- 2 files changed, 135 insertions(+), 2 deletions(-) diff --git a/lib/screens/settings/desktop/advanced_options.dart b/lib/screens/settings/desktop/advanced_options.dart index 5facc078..dc523d34 100644 --- a/lib/screens/settings/desktop/advanced_options.dart +++ b/lib/screens/settings/desktop/advanced_options.dart @@ -15,4 +15,136 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . - */ \ No newline at end of file + */ + +import 'package:bluecherry_client/providers/settings_provider.dart'; +import 'package:bluecherry_client/screens/layouts/desktop/external_stream.dart'; +import 'package:bluecherry_client/screens/settings/desktop/settings.dart'; +import 'package:bluecherry_client/screens/settings/shared/options_chooser_tile.dart'; +import 'package:bluecherry_client/utils/logging.dart'; +import 'package:bluecherry_client/utils/window.dart'; +import 'package:bluecherry_client/widgets/misc.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:provider/provider.dart'; + +class AdvancedOptionsSettings extends StatelessWidget { + const AdvancedOptionsSettings({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final loc = AppLocalizations.of(context); + final settings = context.watch(); + return ListView(children: [ + const SubHeader('Matrix Zoom'), + CheckboxListTile.adaptive( + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.crop), + ), + contentPadding: DesktopSettings.horizontalPadding, + title: Text(loc.matrixedViewZoom), + subtitle: Text(loc.matrixedViewZoomDescription), + value: settings.betaMatrixedZoomEnabled, + onChanged: (value) { + if (value != null) { + settings.betaMatrixedZoomEnabled = value; + } + }, + ), + OptionsChooserTile( + title: 'Default Matrix Size', + icon: Icons.view_quilt, + value: MatrixType.t4, + values: MatrixType.values.map((size) { + return Option( + value: size, + text: size.toString(), + ); + }), + onChanged: (v) {}, + ), + const SubHeader('Developer Options'), + if (!kIsWeb) ...[ + FutureBuilder( + future: getLogFile(), + builder: (context, snapshot) { + return ListTile( + contentPadding: DesktopSettings.horizontalPadding, + leading: const Icon(Icons.bug_report), + title: const Text('Open log file'), + subtitle: Text(snapshot.data?.path ?? loc.loading), + trailing: const Icon(Icons.navigate_next), + dense: false, + onTap: snapshot.data == null + ? null + : () { + launchFileExplorer(snapshot.data!.path); + }, + ); + }, + ), + FutureBuilder( + future: getApplicationSupportDirectory(), + builder: (context, snapshot) { + return ListTile( + contentPadding: DesktopSettings.horizontalPadding, + leading: const Icon(Icons.home), + title: const Text('Open app data'), + subtitle: Text(snapshot.data?.path ?? loc.loading), + trailing: const Icon(Icons.navigate_next), + dense: false, + onTap: snapshot.data == null + ? null + : () { + launchFileExplorer(snapshot.data!.path); + }, + ); + }, + ), + CheckboxListTile( + contentPadding: DesktopSettings.horizontalPadding, + secondary: const Icon(Icons.adb), + title: const Text('Debug info'), + subtitle: const Text( + 'Display useful information for debugging, such as video metadata ' + 'and other useful information for debugging purposes.', + ), + value: settings.showDebugInfo, + onChanged: (v) { + if (v != null) { + settings.showDebugInfo = v; + } + }, + dense: false, + ), + CheckboxListTile( + contentPadding: DesktopSettings.horizontalPadding, + secondary: const Icon(Icons.network_check), + title: const Text('Network Usage'), + subtitle: const Text( + 'Display network usage information over playing videos.', + ), + value: false, + onChanged: (v) {}, + dense: false, + ), + ListTile( + contentPadding: DesktopSettings.horizontalPadding, + leading: const Icon(Icons.restore), + title: const Text('Restore Defaults'), + subtitle: const Text( + 'Restore all settings to their default values. This will not ' + 'affect your servers or any other data.', + ), + trailing: const Icon(Icons.navigate_next), + onTap: () {}, + ), + ], + ]); + } +} diff --git a/lib/screens/settings/desktop/settings.dart b/lib/screens/settings/desktop/settings.dart index b1274dfd..48b8bb50 100644 --- a/lib/screens/settings/desktop/settings.dart +++ b/lib/screens/settings/desktop/settings.dart @@ -17,12 +17,12 @@ * along with this program. If not, see . */ +import 'package:bluecherry_client/screens/settings/desktop/advanced_options.dart'; import 'package:bluecherry_client/screens/settings/desktop/application.dart'; import 'package:bluecherry_client/screens/settings/desktop/events_and_downloads.dart'; import 'package:bluecherry_client/screens/settings/desktop/general.dart'; import 'package:bluecherry_client/screens/settings/desktop/server_and_devices.dart'; import 'package:bluecherry_client/screens/settings/desktop/updates_and_help.dart'; -import 'package:bluecherry_client/screens/settings/shared/date_language.dart'; import 'package:bluecherry_client/utils/constants.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -109,6 +109,7 @@ class _DesktopSettingsState extends State { 2 => const EventsAndDownloadsSettings(), 3 => const ApplicationSettings(), 4 => const UpdatesSettings(), + 5 => const AdvancedOptionsSettings(), _ => const GeneralSettings(), }, ), From 65dfbef4c50fb0ce067e27d2b302c2c13ed5508f Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Mon, 19 Feb 2024 14:12:00 -0300 Subject: [PATCH 09/26] feat: Sections on small screens (mobile) --- lib/screens/settings/desktop/settings.dart | 8 +- lib/screens/settings/mobile/settings.dart | 241 +++++++++++---------- 2 files changed, 132 insertions(+), 117 deletions(-) diff --git a/lib/screens/settings/desktop/settings.dart b/lib/screens/settings/desktop/settings.dart index 48b8bb50..9cdf400d 100644 --- a/lib/screens/settings/desktop/settings.dart +++ b/lib/screens/settings/desktop/settings.dart @@ -69,6 +69,10 @@ class _DesktopSettingsState extends State { icon: Icon(Icons.style), label: Text('Application'), ), + const NavigationRailDestination( + icon: Icon(Icons.security), + label: Text('Privacy and Security'), + ), const NavigationRailDestination( icon: Icon(Icons.update), label: Text('Updates and Help'), @@ -108,8 +112,8 @@ class _DesktopSettingsState extends State { 1 => const ServerSettings(), 2 => const EventsAndDownloadsSettings(), 3 => const ApplicationSettings(), - 4 => const UpdatesSettings(), - 5 => const AdvancedOptionsSettings(), + 5 => const UpdatesSettings(), + 6 => const AdvancedOptionsSettings(), _ => const GeneralSettings(), }, ), diff --git a/lib/screens/settings/mobile/settings.dart b/lib/screens/settings/mobile/settings.dart index 1b7983af..fa999daf 100644 --- a/lib/screens/settings/mobile/settings.dart +++ b/lib/screens/settings/mobile/settings.dart @@ -17,30 +17,26 @@ * along with this program. If not, see . */ -import 'dart:io'; - import 'package:bluecherry_client/models/server.dart'; import 'package:bluecherry_client/providers/home_provider.dart'; import 'package:bluecherry_client/providers/server_provider.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; -import 'package:bluecherry_client/providers/update_provider.dart'; import 'package:bluecherry_client/screens/layouts/device_grid.dart'; import 'package:bluecherry_client/screens/servers/edit_server.dart'; import 'package:bluecherry_client/screens/servers/edit_server_settings.dart'; -import 'package:bluecherry_client/screens/settings/shared/date_language.dart'; -import 'package:bluecherry_client/screens/settings/shared/tiles.dart'; -import 'package:bluecherry_client/screens/settings/shared/update.dart'; +import 'package:bluecherry_client/screens/settings/desktop/advanced_options.dart'; +import 'package:bluecherry_client/screens/settings/desktop/application.dart'; +import 'package:bluecherry_client/screens/settings/desktop/events_and_downloads.dart'; +import 'package:bluecherry_client/screens/settings/desktop/general.dart'; +import 'package:bluecherry_client/screens/settings/desktop/server_and_devices.dart'; +import 'package:bluecherry_client/screens/settings/desktop/updates_and_help.dart'; import 'package:bluecherry_client/utils/constants.dart'; -import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/widgets/drawer_button.dart'; import 'package:bluecherry_client/widgets/misc.dart'; import 'package:bluecherry_client/widgets/squared_icon_button.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:provider/provider.dart'; -import 'package:unity_video_player/unity_video_player.dart'; import 'package:url_launcher/url_launcher.dart'; part '../shared/server_tile.dart'; @@ -63,8 +59,6 @@ class _MobileSettingsState extends State { Widget build(BuildContext context) { final loc = AppLocalizations.of(context); final theme = Theme.of(context); - final settings = context.watch(); - final servers = context.watch(); return Material( type: MaterialType.transparency, @@ -76,114 +70,131 @@ class _MobileSettingsState extends State { title: Text(loc.settings), ), Expanded( - child: CustomScrollView(slivers: [ - SliverToBoxAdapter( - child: SubHeader( - loc.servers, - subtext: loc.nServers(servers.servers.length), + child: ListTileTheme( + data: ListTileThemeData( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), ), + tileColor: theme.colorScheme.primaryContainer.withOpacity(0.42), ), - const SliverToBoxAdapter(child: ServersList()), - SliverToBoxAdapter( - child: SubHeader(loc.theme, subtext: loc.themeDescription), - ), - SliverList.list( - children: ThemeMode.values - .map((mode) => ThemeTile(themeMode: mode)) - .toList(), - ), - SliverToBoxAdapter(child: SubHeader(loc.miscellaneous)), - SliverList.list(children: [ - const SnoozeNotificationsTile(), - const NotificationClickBehaviorTile(), - ExpansionTile( - leading: CircleAvatar( - backgroundColor: const Color.fromRGBO(0, 0, 0, 0), - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.fit_screen), + child: ListView( + padding: const EdgeInsetsDirectional.all(8.0), + children: [ + ListTile( + leading: const Icon(Icons.dashboard), + title: Text(loc.general), + subtitle: + const Text('Notifications, Data Usage, Wakelock, etc'), + trailing: const Icon(Icons.chevron_right), + onTap: () { + showModalBottomSheet( + context: context, + showDragHandle: true, + scrollControlDisabledMaxHeightRatio: 0.9, + builder: (context) { + return const GeneralSettings(); + }, + ); + }, ), - title: Text(loc.cameraViewFit), - textColor: theme.textTheme.bodyLarge?.color, - subtitle: Text( - settings.cameraViewFit.locale(context), - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.textTheme.bodySmall?.color, - ), + ListTile( + leading: const Icon(Icons.dns), + title: const Text('Servers and Devices'), + subtitle: const Text('Servers, Devices, Streaming, etc'), + trailing: const Icon(Icons.chevron_right), + onTap: () { + showModalBottomSheet( + context: context, + showDragHandle: true, + scrollControlDisabledMaxHeightRatio: 0.9, + builder: (context) { + return const ServerSettings(); + }, + ); + }, ), - children: UnityVideoFit.values.map((e) { - return RadioListTile.adaptive( - contentPadding: const EdgeInsetsDirectional.only( - start: 68.0, - end: 16.0, - ), - value: e, - groupValue: settings.cameraViewFit, - onChanged: (_) => settings.cameraViewFit = e, - secondary: Icon(e.icon), - controlAffinity: ListTileControlAffinity.trailing, - title: Padding( - padding: const EdgeInsetsDirectional.only(start: 16.0), - child: Text(e.locale(context)), - ), - ); - }).toList(), - ), - const DirectoryChooseTile(), - const CyclePeriodTile(), - const CameraReloadPeriodTile(), - ]), - SliverToBoxAdapter( - child: CorrectedListTile( - iconData: Icons.language, - trailing: Icons.navigate_next, - title: loc.dateLanguage, - subtitle: '${settings.dateFormat.format(DateTime.now())} ' - '${settings.timeFormat.format(DateTime.now())}; ' - '${LocaleNames.of(context)!.nameOf(settings.locale.toLanguageTag())}', - height: 80.0, - onTap: () { - showModalBottomSheet( - context: context, - showDragHandle: true, - isScrollControlled: true, - builder: (context) { - return DraggableScrollableSheet( - expand: false, - maxChildSize: 0.8, - minChildSize: 0.8, - initialChildSize: 0.8, - builder: (context, controller) { - return LocalizationSettings(controller: controller); - }, - ); - }, - ); - }, - ), - ), - const SliverToBoxAdapter(child: WakelockTile()), - if (UpdateManager.isUpdatingSupported) ...[ - SliverToBoxAdapter( - child: SubHeader( - loc.updates, - subtext: loc.runningOn(() { - if (Platform.isLinux) { - return loc.linux(UpdateManager.linuxEnvironment.name); - } else if (Platform.isWindows) { - return loc.windows; - } - - return defaultTargetPlatform.name; - }()), + ListTile( + leading: const Icon(Icons.event), + title: const Text('Events and Downloads'), + subtitle: const Text('Downloads, Events, Timeline, etc'), + trailing: const Icon(Icons.chevron_right), + onTap: () { + showModalBottomSheet( + context: context, + showDragHandle: true, + scrollControlDisabledMaxHeightRatio: 0.9, + builder: (context) { + return const EventsAndDownloadsSettings(); + }, + ); + }, ), - ), - const SliverToBoxAdapter(child: AppUpdateCard()), - const SliverToBoxAdapter(child: AppUpdateOptions()), - ], - SliverToBoxAdapter(child: SubHeader(loc.about)), - const SliverToBoxAdapter(child: About()), - const SliverToBoxAdapter(child: SizedBox(height: 16.0)), - ]), + ListTile( + leading: const Icon(Icons.style), + title: const Text('Application'), + subtitle: const Text( + 'Theme, Language, Date and Time, Window, etc'), + trailing: const Icon(Icons.chevron_right), + onTap: () { + showModalBottomSheet( + context: context, + showDragHandle: true, + scrollControlDisabledMaxHeightRatio: 0.9, + builder: (context) { + return const ApplicationSettings(); + }, + ); + }, + ), + ListTile( + leading: const Icon(Icons.security), + title: const Text('Privacy and Security'), + subtitle: const Text('Diagnostics, Privacy, Security, etc'), + trailing: const Icon(Icons.chevron_right), + onTap: () {}, + ), + ListTile( + leading: const Icon(Icons.update), + title: const Text('Updates and Help'), + subtitle: const Text('Check for updates, Help, About, etc'), + trailing: const Icon(Icons.chevron_right), + onTap: () { + showModalBottomSheet( + context: context, + showDragHandle: true, + scrollControlDisabledMaxHeightRatio: 0.9, + builder: (context) { + return const UpdatesSettings(); + }, + ); + }, + ), + ListTile( + leading: const Icon(Icons.code), + title: const Text('Advanced Options'), + subtitle: + const Text('Beta Features, Developer Options, etc'), + trailing: const Icon(Icons.chevron_right), + onTap: () { + showModalBottomSheet( + context: context, + showDragHandle: true, + scrollControlDisabledMaxHeightRatio: 0.9, + builder: (context) { + return const AdvancedOptionsSettings(); + }, + ); + }, + ), + ] + .map((e) => Padding( + padding: + const EdgeInsetsDirectional.only(bottom: 8.0), + child: e, + )) + .toList(), + ), + ), ), ]), ), From 461d96d6819d6aa8e3aa71b3df6046c3ab755d88 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Mon, 19 Feb 2024 14:19:26 -0300 Subject: [PATCH 10/26] feat: Privacy and Security field --- .../desktop/privacy_and_security.dart | 70 +++++++++++++++++++ lib/screens/settings/desktop/settings.dart | 2 + lib/screens/settings/mobile/settings.dart | 12 +++- 3 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 lib/screens/settings/desktop/privacy_and_security.dart diff --git a/lib/screens/settings/desktop/privacy_and_security.dart b/lib/screens/settings/desktop/privacy_and_security.dart new file mode 100644 index 00000000..cc61d8d1 --- /dev/null +++ b/lib/screens/settings/desktop/privacy_and_security.dart @@ -0,0 +1,70 @@ +/* + * 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 . + */ + +import 'package:bluecherry_client/screens/settings/desktop/settings.dart'; +import 'package:bluecherry_client/screens/settings/shared/options_chooser_tile.dart'; +import 'package:flutter/material.dart'; + +class PrivacySecuritySettings extends StatelessWidget { + const PrivacySecuritySettings({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return ListView(children: [ + CheckboxListTile.adaptive( + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.crop), + ), + contentPadding: DesktopSettings.horizontalPadding, + title: const Text('Use data'), + subtitle: const Text( + 'Allow Bluecherry to collect data to improve the app and provide ' + 'better services. Data is collected anonymously and does not contain ' + 'any personal information.', + ), + isThreeLine: true, + value: true, + onChanged: (value) {}, + ), + OptionsChooserTile( + title: 'Automatically report errors', + icon: Icons.error, + value: 'On', + values: ['On', 'Ask', 'Error'].map((e) => Option(text: e, value: e)), + onChanged: (v) {}, + ), + const Divider(), + ListTile( + leading: const Icon(Icons.privacy_tip), + title: const Text('Privacy Policy'), + trailing: const Icon(Icons.chevron_right), + onTap: () {}, + ), + ListTile( + leading: const Icon(Icons.policy), + title: const Text('Terms of Service'), + trailing: const Icon(Icons.chevron_right), + onTap: () {}, + ), + ]); + } +} diff --git a/lib/screens/settings/desktop/settings.dart b/lib/screens/settings/desktop/settings.dart index 9cdf400d..9a138054 100644 --- a/lib/screens/settings/desktop/settings.dart +++ b/lib/screens/settings/desktop/settings.dart @@ -21,6 +21,7 @@ import 'package:bluecherry_client/screens/settings/desktop/advanced_options.dart import 'package:bluecherry_client/screens/settings/desktop/application.dart'; import 'package:bluecherry_client/screens/settings/desktop/events_and_downloads.dart'; import 'package:bluecherry_client/screens/settings/desktop/general.dart'; +import 'package:bluecherry_client/screens/settings/desktop/privacy_and_security.dart'; import 'package:bluecherry_client/screens/settings/desktop/server_and_devices.dart'; import 'package:bluecherry_client/screens/settings/desktop/updates_and_help.dart'; import 'package:bluecherry_client/utils/constants.dart'; @@ -112,6 +113,7 @@ class _DesktopSettingsState extends State { 1 => const ServerSettings(), 2 => const EventsAndDownloadsSettings(), 3 => const ApplicationSettings(), + 4 => const PrivacySecuritySettings(), 5 => const UpdatesSettings(), 6 => const AdvancedOptionsSettings(), _ => const GeneralSettings(), diff --git a/lib/screens/settings/mobile/settings.dart b/lib/screens/settings/mobile/settings.dart index fa999daf..6813e5bd 100644 --- a/lib/screens/settings/mobile/settings.dart +++ b/lib/screens/settings/mobile/settings.dart @@ -28,6 +28,7 @@ import 'package:bluecherry_client/screens/settings/desktop/advanced_options.dart import 'package:bluecherry_client/screens/settings/desktop/application.dart'; import 'package:bluecherry_client/screens/settings/desktop/events_and_downloads.dart'; import 'package:bluecherry_client/screens/settings/desktop/general.dart'; +import 'package:bluecherry_client/screens/settings/desktop/privacy_and_security.dart'; import 'package:bluecherry_client/screens/settings/desktop/server_and_devices.dart'; import 'package:bluecherry_client/screens/settings/desktop/updates_and_help.dart'; import 'package:bluecherry_client/utils/constants.dart'; @@ -151,7 +152,16 @@ class _MobileSettingsState extends State { title: const Text('Privacy and Security'), subtitle: const Text('Diagnostics, Privacy, Security, etc'), trailing: const Icon(Icons.chevron_right), - onTap: () {}, + onTap: () { + showModalBottomSheet( + context: context, + showDragHandle: true, + scrollControlDisabledMaxHeightRatio: 0.9, + builder: (context) { + return const PrivacySecuritySettings(); + }, + ); + }, ), ListTile( leading: const Icon(Icons.update), From 13504fc7889dc78dbaa611dd6c373806c19fc814 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Mon, 19 Feb 2024 14:25:56 -0300 Subject: [PATCH 11/26] feat: Add Acessibility section under Application section --- lib/screens/settings/desktop/application.dart | 49 +++++++- .../settings/shared/date_language.dart | 105 +++++++----------- .../settings/shared/options_chooser_tile.dart | 5 +- 3 files changed, 90 insertions(+), 69 deletions(-) diff --git a/lib/screens/settings/desktop/application.dart b/lib/screens/settings/desktop/application.dart index 6cc351b6..e00caf27 100644 --- a/lib/screens/settings/desktop/application.dart +++ b/lib/screens/settings/desktop/application.dart @@ -21,6 +21,7 @@ import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/screens/settings/desktop/settings.dart'; import 'package:bluecherry_client/screens/settings/shared/date_language.dart'; import 'package:bluecherry_client/screens/settings/shared/options_chooser_tile.dart'; +import 'package:bluecherry_client/widgets/misc.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; @@ -60,10 +61,7 @@ class ApplicationSettings extends StatelessWidget { const LanguageSection(), const DateFormatSection(), const TimeFormatSection(), - Padding( - padding: DesktopSettings.horizontalPadding, - child: Text('Window', style: theme.textTheme.titleMedium), - ), + const SubHeader('Window'), CheckboxListTile.adaptive( value: false, onChanged: (v) {}, @@ -93,6 +91,49 @@ class ApplicationSettings extends StatelessWidget { 'This will keep the app running in the background.', ), ), + const SubHeader('Acessibility'), + CheckboxListTile.adaptive( + value: true, + onChanged: (v) {}, + contentPadding: DesktopSettings.horizontalPadding, + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.animation), + ), + title: const Text('Animations'), + subtitle: const Text( + 'Disable animations on low-end devices to improve performance. ' + 'This will also disable some visual effects. '), + ), + CheckboxListTile.adaptive( + value: false, + onChanged: (v) {}, + contentPadding: DesktopSettings.horizontalPadding, + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.filter_b_and_w), + ), + title: const Text('High contrast mode'), + subtitle: const Text( + 'Enable high contrast mode to make the app easier to read and use.', + ), + ), + CheckboxListTile.adaptive( + value: false, + onChanged: (v) {}, + contentPadding: DesktopSettings.horizontalPadding, + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.accessibility_new), + ), + title: const Text('Large text'), + subtitle: const Text( + 'Increase the size of the text in the app to make it easier to read.', + ), + ), ]); } } diff --git a/lib/screens/settings/shared/date_language.dart b/lib/screens/settings/shared/date_language.dart index e418219d..7a5b7ea5 100644 --- a/lib/screens/settings/shared/date_language.dart +++ b/lib/screens/settings/shared/date_language.dart @@ -153,53 +153,32 @@ class DateFormatSection extends StatelessWidget { 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)); + 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) { - return OptionsChooserTile( - title: 'Date Format', - description: 'What format to use for displaying dates', - icon: Icons.calendar_month, - value: '', - values: formats.map((format) { - return Option( - value: format.pattern, - text: format.format(DateTime.utc(1969, 7, 20, 14, 18, 04)), - ); - }), - onChanged: (v) { - settings.dateFormat = DateFormat(v!, locale); - }, - ); - } else { - return Column( - children: formats.map((format) { - return RadioListTile.adaptive( - 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(), + return OptionsChooserTile( + title: 'Date Format', + description: 'What format to use for displaying dates', + icon: Icons.calendar_month, + value: '', + values: formats.map((format) { + return Option( + value: format.pattern, + text: format.format(DateTime.utc(1969, 7, 20, 14, 18, 04)), ); - } - }); + }), + onChanged: (v) { + settings.dateFormat = DateFormat(v!, locale); + }, + ); } } @@ -211,24 +190,22 @@ class TimeFormatSection extends StatelessWidget { 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 OptionsChooserTile( - title: 'Time Format', - description: 'What format to use for displaying time', - icon: Icons.hourglass_empty, - value: '', - values: patterns.map((pattern) { - return Option( - value: pattern.pattern, - text: pattern.format(date), - ); - }), - onChanged: (v) { - settings.timeFormat = DateFormat(v!, locale); - }, - ); - }); + final patterns = ['HH:mm', 'hh:mm a'].map((e) => DateFormat(e, locale)); + final date = DateTime.utc(1969, 7, 20, 14, 18, 04); + return OptionsChooserTile( + title: 'Time Format', + description: 'What format to use for displaying time', + icon: Icons.hourglass_empty, + value: '', + values: patterns.map((pattern) { + return Option( + value: pattern.pattern, + text: pattern.format(date), + ); + }), + onChanged: (v) { + settings.timeFormat = DateFormat(v!, locale); + }, + ); } } diff --git a/lib/screens/settings/shared/options_chooser_tile.dart b/lib/screens/settings/shared/options_chooser_tile.dart index ae873c83..7fbcc5b0 100644 --- a/lib/screens/settings/shared/options_chooser_tile.dart +++ b/lib/screens/settings/shared/options_chooser_tile.dart @@ -42,7 +42,10 @@ class OptionsChooserTile extends StatelessWidget { leading: CircleAvatar( backgroundColor: Colors.transparent, foregroundColor: theme.iconTheme.color, - child: Icon(icon), + child: Align( + alignment: AlignmentDirectional.topCenter, + child: Icon(icon), + ), ), title: Text(title), textColor: theme.textTheme.bodyLarge?.color, From 99d43a548439ba9dc8ca2d8869dad3df22191604 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Mon, 19 Feb 2024 14:42:02 -0300 Subject: [PATCH 12/26] chore: Move tiles to their respective section --- lib/screens/settings/desktop/application.dart | 127 ++++++- .../desktop/events_and_downloads.dart | 30 +- lib/screens/settings/desktop/general.dart | 99 +++++- .../settings/shared/date_language.dart | 211 ----------- .../settings/shared/options_chooser_tile.dart | 4 +- lib/screens/settings/shared/tiles.dart | 335 ------------------ 6 files changed, 251 insertions(+), 555 deletions(-) delete mode 100644 lib/screens/settings/shared/date_language.dart delete mode 100644 lib/screens/settings/shared/tiles.dart diff --git a/lib/screens/settings/desktop/application.dart b/lib/screens/settings/desktop/application.dart index e00caf27..fa5c2e19 100644 --- a/lib/screens/settings/desktop/application.dart +++ b/lib/screens/settings/desktop/application.dart @@ -19,11 +19,13 @@ import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/screens/settings/desktop/settings.dart'; -import 'package:bluecherry_client/screens/settings/shared/date_language.dart'; import 'package:bluecherry_client/screens/settings/shared/options_chooser_tile.dart'; +import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/widgets/misc.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 ApplicationSettings extends StatelessWidget { @@ -137,3 +139,126 @@ class ApplicationSettings extends StatelessWidget { ]); } } + +class LanguageSection extends StatelessWidget { + const LanguageSection({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final loc = AppLocalizations.of(context); + final settings = context.watch(); + final currentLocale = Localizations.localeOf(context); + const locales = AppLocalizations.supportedLocales; + final names = LocaleNames.of(context)!; + + return DropdownButtonHideUnderline( + child: ListTile( + contentPadding: DesktopSettings.horizontalPadding, + leading: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.language), + ), + title: Text(loc.language), + trailing: DropdownButton( + value: currentLocale, + onChanged: (value) => settings.locale = value!, + items: locales.map((locale) { + final name = + names.nameOf(locale.toLanguageTag()) ?? locale.toLanguageTag(); + final nativeName = LocaleNamesLocalizationsDelegate + .nativeLocaleNames[locale.toLanguageTag()] ?? + locale.toLanguageTag(); + return DropdownMenuItem( + value: locale, + child: Padding( + padding: const EdgeInsetsDirectional.only(end: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + name.uppercaseFirst, + maxLines: 1, + softWrap: false, + style: theme.textTheme.bodyMedium, + ), + Text( + nativeName.uppercaseFirst, + style: theme.textTheme.labelSmall, + ), + ], + ), + ), + ); + }).toList(), + ), + ), + ); + } +} + +class DateFormatSection extends StatelessWidget { + const DateFormatSection({super.key}); + + @override + Widget build(BuildContext context) { + final settings = context.watch(); + final locale = Localizations.localeOf(context).toLanguageTag(); + 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)); + + return OptionsChooserTile( + title: 'Date Format', + description: 'What format to use for displaying dates', + icon: Icons.calendar_month, + value: '', + values: formats.map((format) { + return Option( + value: format.pattern, + text: format.format(DateTime.utc(1969, 7, 20, 14, 18, 04)), + ); + }), + onChanged: (v) { + settings.dateFormat = DateFormat(v!, locale); + }, + ); + } +} + +class TimeFormatSection extends StatelessWidget { + const TimeFormatSection({super.key}); + + @override + Widget build(BuildContext context) { + final settings = context.watch(); + final locale = Localizations.localeOf(context).toLanguageTag(); + + final patterns = ['HH:mm', 'hh:mm a'].map((e) => DateFormat(e, locale)); + final date = DateTime.utc(1969, 7, 20, 14, 18, 04); + return OptionsChooserTile( + title: 'Time Format', + description: 'What format to use for displaying time', + icon: Icons.hourglass_empty, + value: '', + values: patterns.map((pattern) { + return Option( + value: pattern.pattern, + text: pattern.format(date), + ); + }), + onChanged: (v) { + settings.timeFormat = DateFormat(v!, locale); + }, + ); + } +} diff --git a/lib/screens/settings/desktop/events_and_downloads.dart b/lib/screens/settings/desktop/events_and_downloads.dart index 7705b4e8..f87cb468 100644 --- a/lib/screens/settings/desktop/events_and_downloads.dart +++ b/lib/screens/settings/desktop/events_and_downloads.dart @@ -17,11 +17,15 @@ * along with this program. If not, see . */ +import 'dart:io'; + +import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/screens/settings/desktop/settings.dart'; import 'package:bluecherry_client/screens/settings/shared/options_chooser_tile.dart'; -import 'package:bluecherry_client/screens/settings/shared/tiles.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 EventsAndDownloadsSettings extends StatelessWidget { const EventsAndDownloadsSettings({super.key}); @@ -30,6 +34,7 @@ class EventsAndDownloadsSettings extends StatelessWidget { Widget build(BuildContext context) { final loc = AppLocalizations.of(context); final theme = Theme.of(context); + final settings = context.watch(); return ListView(padding: DesktopSettings.verticalPadding, children: [ Padding( padding: DesktopSettings.horizontalPadding, @@ -46,7 +51,28 @@ class EventsAndDownloadsSettings extends StatelessWidget { ), title: const Text('Choose location for each download'), ), - const DirectoryChooseTile(), + ListTile( + contentPadding: DesktopSettings.horizontalPadding, + leading: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.notifications_paused), + ), + title: Text(loc.downloadPath), + subtitle: Text(settings.downloadsDirectory), + trailing: const Icon(Icons.navigate_next), + onTap: () async { + final selectedDirectory = await FilePicker.platform.getDirectoryPath( + dialogTitle: loc.downloadPath, + initialDirectory: settings.downloadsDirectory, + lockParentWindow: true, + ); + + if (selectedDirectory != null) { + settings.downloadsDirectory = Directory(selectedDirectory).path; + } + }, + ), CheckboxListTile.adaptive( value: false, onChanged: (v) {}, diff --git a/lib/screens/settings/desktop/general.dart b/lib/screens/settings/desktop/general.dart index f60ba930..3980ae00 100644 --- a/lib/screens/settings/desktop/general.dart +++ b/lib/screens/settings/desktop/general.dart @@ -17,12 +17,14 @@ * along with this program. If not, see . */ +import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/screens/settings/desktop/settings.dart'; import 'package:bluecherry_client/screens/settings/shared/options_chooser_tile.dart'; -import 'package:bluecherry_client/screens/settings/shared/tiles.dart'; +import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/widgets/misc.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}); @@ -31,9 +33,40 @@ class GeneralSettings extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); final loc = AppLocalizations.of(context); + final settings = context.watch(); return ListView(padding: DesktopSettings.verticalPadding, children: [ - const CyclePeriodTile(), - const WakelockTile(), + OptionsChooserTile( + title: loc.cycleTogglePeriod, + description: loc.cycleTogglePeriodDescription, + icon: Icons.timelapse, + value: settings.layoutCyclingTogglePeriod, + values: [5, 10, 30, 60, 60 * 5] + .map((seconds) => Duration(seconds: seconds)) + .map((duration) { + return Option( + text: duration.humanReadableCompact(context), + value: duration, + ); + }), + onChanged: (value) { + settings.layoutCyclingTogglePeriod = value; + }, + ), + CheckboxListTile.adaptive( + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.monitor), + ), + contentPadding: DesktopSettings.horizontalPadding, + title: Text(loc.wakelock), + subtitle: Text(loc.wakelockDescription), + isThreeLine: true, + value: settings.wakelockEnabled, + onChanged: (value) { + settings.wakelockEnabled = !settings.wakelockEnabled; + }, + ), const SubHeader( 'Notifications', padding: DesktopSettings.horizontalPadding, @@ -49,8 +82,64 @@ class GeneralSettings extends StatelessWidget { value: true, onChanged: (value) {}, ), - const SnoozeNotificationsTile(), - const NotificationClickBehaviorTile(), + ListTile( + contentPadding: DesktopSettings.horizontalPadding, + leading: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.notifications_paused), + ), + title: Text(loc.snoozeNotifications), + subtitle: Text( + 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, + ), + 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, + ); + } + } + }, + ), + OptionsChooserTile( + title: loc.notificationClickBehavior, + description: loc.notificationClickBehaviorDescription, + icon: Icons.beenhere_rounded, + value: settings.notificationClickBehavior, + values: NotificationClickBehavior.values + .map((behavior) => Option( + value: behavior, + icon: behavior.icon, + text: behavior.locale(context), + )) + .toList(), + onChanged: (v) { + settings.notificationClickBehavior = v; + }, + ), const SubHeader( 'Data Usage', padding: DesktopSettings.horizontalPadding, diff --git a/lib/screens/settings/shared/date_language.dart b/lib/screens/settings/shared/date_language.dart deleted file mode 100644 index 7a5b7ea5..00000000 --- a/lib/screens/settings/shared/date_language.dart +++ /dev/null @@ -1,211 +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 . - */ - -import 'package:bluecherry_client/providers/settings_provider.dart'; -import 'package:bluecherry_client/screens/settings/desktop/settings.dart'; -import 'package:bluecherry_client/screens/settings/shared/options_chooser_tile.dart'; -import 'package:bluecherry_client/utils/extensions.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 { - final ScrollController? controller; - - const LocalizationSettings({super.key, this.controller}); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final loc = AppLocalizations.of(context); - - return ListView( - padding: DesktopSettings.verticalPadding, - controller: controller, - children: [ - Padding( - padding: DesktopSettings.horizontalPadding, - child: Material( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - child: const LanguageSection(), - ), - ), - const SizedBox(height: 12.0), - Padding( - padding: DesktopSettings.horizontalPadding, - child: Text(loc.dateFormat, style: theme.textTheme.titleMedium), - ), - const SizedBox(height: 8.0), - Padding( - padding: DesktopSettings.horizontalPadding, - child: Material( - clipBehavior: Clip.hardEdge, - shape: RoundedRectangleBorder( - borderRadius: BorderRadiusDirectional.circular(8.0), - ), - child: const DateFormatSection(), - ), - ), - const SizedBox(height: 12.0), - Padding( - padding: DesktopSettings.horizontalPadding, - child: Text(loc.timeFormat, style: theme.textTheme.titleMedium), - ), - const SizedBox(height: 8.0), - Padding( - padding: DesktopSettings.horizontalPadding, - child: Material( - clipBehavior: Clip.hardEdge, - shape: RoundedRectangleBorder( - borderRadius: BorderRadiusDirectional.circular(8.0), - ), - child: const TimeFormatSection(), - ), - ), - ], - ); - } -} - -class LanguageSection extends StatelessWidget { - const LanguageSection({super.key}); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final loc = AppLocalizations.of(context); - final settings = context.watch(); - final currentLocale = Localizations.localeOf(context); - const locales = AppLocalizations.supportedLocales; - final names = LocaleNames.of(context)!; - - return DropdownButtonHideUnderline( - child: ListTile( - contentPadding: DesktopSettings.horizontalPadding, - leading: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.language), - ), - title: Text(loc.language), - trailing: DropdownButton( - value: currentLocale, - onChanged: (value) => settings.locale = value!, - items: locales.map((locale) { - final name = - names.nameOf(locale.toLanguageTag()) ?? locale.toLanguageTag(); - final nativeName = LocaleNamesLocalizationsDelegate - .nativeLocaleNames[locale.toLanguageTag()] ?? - locale.toLanguageTag(); - return DropdownMenuItem( - value: locale, - child: Padding( - padding: const EdgeInsetsDirectional.only(end: 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - name.uppercaseFirst, - maxLines: 1, - softWrap: false, - style: theme.textTheme.bodyMedium, - ), - Text( - nativeName.uppercaseFirst, - style: theme.textTheme.labelSmall, - ), - ], - ), - ), - ); - }).toList(), - ), - ), - ); - } -} - -class DateFormatSection extends StatelessWidget { - const DateFormatSection({super.key}); - - @override - Widget build(BuildContext context) { - final settings = context.watch(); - final locale = Localizations.localeOf(context).toLanguageTag(); - 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)); - - return OptionsChooserTile( - title: 'Date Format', - description: 'What format to use for displaying dates', - icon: Icons.calendar_month, - value: '', - values: formats.map((format) { - return Option( - value: format.pattern, - text: format.format(DateTime.utc(1969, 7, 20, 14, 18, 04)), - ); - }), - onChanged: (v) { - settings.dateFormat = DateFormat(v!, locale); - }, - ); - } -} - -class TimeFormatSection extends StatelessWidget { - const TimeFormatSection({super.key}); - - @override - Widget build(BuildContext context) { - final settings = context.watch(); - final locale = Localizations.localeOf(context).toLanguageTag(); - - final patterns = ['HH:mm', 'hh:mm a'].map((e) => DateFormat(e, locale)); - final date = DateTime.utc(1969, 7, 20, 14, 18, 04); - return OptionsChooserTile( - title: 'Time Format', - description: 'What format to use for displaying time', - icon: Icons.hourglass_empty, - value: '', - values: patterns.map((pattern) { - return Option( - value: pattern.pattern, - text: pattern.format(date), - ); - }), - onChanged: (v) { - settings.timeFormat = DateFormat(v!, locale); - }, - ); - } -} diff --git a/lib/screens/settings/shared/options_chooser_tile.dart b/lib/screens/settings/shared/options_chooser_tile.dart index 7fbcc5b0..6afcba6a 100644 --- a/lib/screens/settings/shared/options_chooser_tile.dart +++ b/lib/screens/settings/shared/options_chooser_tile.dart @@ -43,7 +43,9 @@ class OptionsChooserTile extends StatelessWidget { backgroundColor: Colors.transparent, foregroundColor: theme.iconTheme.color, child: Align( - alignment: AlignmentDirectional.topCenter, + alignment: description == null + ? Alignment.center + : AlignmentDirectional.topCenter, child: Icon(icon), ), ), diff --git a/lib/screens/settings/shared/tiles.dart b/lib/screens/settings/shared/tiles.dart deleted file mode 100644 index d6f32f99..00000000 --- a/lib/screens/settings/shared/tiles.dart +++ /dev/null @@ -1,335 +0,0 @@ -import 'dart:io'; - -import 'package:bluecherry_client/providers/settings_provider.dart'; -import 'package:bluecherry_client/screens/layouts/desktop/stream_data.dart'; -import 'package:bluecherry_client/screens/settings/desktop/settings.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/foundation.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.adaptive( - 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) { - if (kIsWeb) return const SizedBox.shrink(); - 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 NotificationClickBehaviorTile extends StatelessWidget { - const NotificationClickBehaviorTile({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( - loc.notificationClickBehaviorDescription, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.textTheme.bodySmall?.color, - ), - ), - tilePadding: DesktopSettings.horizontalPadding, - trailing: Text(settings.notificationClickBehavior.locale(context)), - children: NotificationClickBehavior.values.map((behavior) { - return RadioListTile.adaptive( - 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); - final periodList = [5, 10, 30, 60, 60 * 5].map((e) => Duration(seconds: e)); - - return LayoutBuilder(builder: (context, constraints) { - final isSmall = constraints.maxWidth < 600.0; - 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( - loc.cycleTogglePeriodDescription, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.textTheme.bodySmall?.color, - ), - ), - trailing: Text( - settings.layoutCyclingTogglePeriod.humanReadableCompact(context), - ), - tilePadding: DesktopSettings.horizontalPadding, - childrenPadding: const EdgeInsetsDirectional.all(12.0), - children: [ - ToggleButtons( - isSelected: periodList - .map((d) => d == settings.layoutCyclingTogglePeriod) - .toList(), - onPressed: (index) => settings.layoutCyclingTogglePeriod = - periodList.elementAt(index), - children: periodList.map((dur) { - return Padding( - padding: - const EdgeInsetsDirectional.symmetric(horizontal: 12.0), - child: Row(children: [ - Text( - isSmall - ? dur.humanReadableCompact(context) - : dur.humanReadable(context), - ), - if (dur == - SettingsProvider.kDefaultLayoutCyclingTogglePeriod) ...[ - const SizedBox(width: 8.0), - const DefaultValueIcon(), - ] - ]), - ); - }).toList(), - ), - ], - ); - }); - } -} - -class CameraReloadPeriodTile extends StatelessWidget { - const CameraReloadPeriodTile({super.key}); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final settings = context.watch(); - final loc = AppLocalizations.of(context); - final periodList = - [0, 30, 60 * 2, 60 * 5].map((e) => Duration(seconds: e)); - - return LayoutBuilder(builder: (context, constraints) { - final isSmall = constraints.maxWidth < 600.0; - return ExpansionTile( - leading: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.camera), - ), - title: Text(loc.cameraRefreshPeriod), - textColor: theme.textTheme.bodyLarge?.color, - subtitle: Text( - loc.cameraRefreshPeriodDescription, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.textTheme.bodySmall?.color, - ), - ), - trailing: settings.cameraRefreshPeriod > Duration.zero - ? Text(settings.cameraRefreshPeriod.humanReadableCompact(context)) - : null, - childrenPadding: const EdgeInsetsDirectional.all(12.0), - children: [ - ToggleButtons( - isSelected: periodList - .map((d) => d == settings.cameraRefreshPeriod) - .toList(), - onPressed: (index) => - settings.cameraRefreshPeriod = periodList.elementAt(index), - children: periodList.map((dur) { - return Padding( - padding: - const EdgeInsetsDirectional.symmetric(horizontal: 12.0), - child: Row(children: [ - Text( - dur == Duration.zero - ? loc.disabled - : isSmall - ? dur.humanReadableCompact(context) - : dur.humanReadable(context), - ), - if (dur == SettingsProvider.kDefaultCameraRefreshPeriod) ...[ - const SizedBox(width: 8.0), - const DefaultValueIcon(), - ] - ]), - ); - }).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.adaptive( - value: settings.wakelockEnabled, - onChanged: (v) {}, - ), - ), - ), - title: loc.wakelock, - subtitle: loc.wakelockDescription, - height: 72.0, - onTap: () => settings.wakelockEnabled = !settings.wakelockEnabled, - ); - } -} From 98ce912661405923c1d67fe0f2c25e6a25d99e12 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Mon, 19 Feb 2024 14:47:50 -0300 Subject: [PATCH 13/26] feat: Reorganize Settings folders --- .../{desktop => }/advanced_options.dart | 2 +- .../settings/{desktop => }/application.dart | 2 +- .../{desktop => }/events_and_downloads.dart | 2 +- .../settings/{desktop => }/general.dart | 2 +- .../{desktop => }/privacy_and_security.dart | 22 ++++++++++++++----- .../{desktop => }/server_and_devices.dart | 4 ++-- lib/screens/settings/settings.dart | 4 ++-- .../settings.dart => settings_desktop.dart} | 14 ++++++------ .../settings.dart => settings_mobile.dart} | 16 +++++++------- .../settings/shared/options_chooser_tile.dart | 2 +- lib/screens/settings/shared/server_tile.dart | 2 +- lib/screens/settings/shared/update.dart | 2 +- .../{desktop => }/updates_and_help.dart | 2 +- lib/widgets/misc.dart | 2 +- 14 files changed, 45 insertions(+), 33 deletions(-) rename lib/screens/settings/{desktop => }/advanced_options.dart (98%) rename lib/screens/settings/{desktop => }/application.dart (99%) rename lib/screens/settings/{desktop => }/events_and_downloads.dart (98%) rename lib/screens/settings/{desktop => }/general.dart (98%) rename lib/screens/settings/{desktop => }/privacy_and_security.dart (72%) rename lib/screens/settings/{desktop => }/server_and_devices.dart (98%) rename lib/screens/settings/{desktop/settings.dart => settings_desktop.dart} (88%) rename lib/screens/settings/{mobile/settings.dart => settings_mobile.dart} (93%) rename lib/screens/settings/{desktop => }/updates_and_help.dart (98%) diff --git a/lib/screens/settings/desktop/advanced_options.dart b/lib/screens/settings/advanced_options.dart similarity index 98% rename from lib/screens/settings/desktop/advanced_options.dart rename to lib/screens/settings/advanced_options.dart index dc523d34..c196f7c5 100644 --- a/lib/screens/settings/desktop/advanced_options.dart +++ b/lib/screens/settings/advanced_options.dart @@ -19,7 +19,7 @@ import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/screens/layouts/desktop/external_stream.dart'; -import 'package:bluecherry_client/screens/settings/desktop/settings.dart'; +import 'package:bluecherry_client/screens/settings/settings_desktop.dart'; import 'package:bluecherry_client/screens/settings/shared/options_chooser_tile.dart'; import 'package:bluecherry_client/utils/logging.dart'; import 'package:bluecherry_client/utils/window.dart'; diff --git a/lib/screens/settings/desktop/application.dart b/lib/screens/settings/application.dart similarity index 99% rename from lib/screens/settings/desktop/application.dart rename to lib/screens/settings/application.dart index fa5c2e19..1c7512ae 100644 --- a/lib/screens/settings/desktop/application.dart +++ b/lib/screens/settings/application.dart @@ -18,7 +18,7 @@ */ import 'package:bluecherry_client/providers/settings_provider.dart'; -import 'package:bluecherry_client/screens/settings/desktop/settings.dart'; +import 'package:bluecherry_client/screens/settings/settings_desktop.dart'; import 'package:bluecherry_client/screens/settings/shared/options_chooser_tile.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/widgets/misc.dart'; diff --git a/lib/screens/settings/desktop/events_and_downloads.dart b/lib/screens/settings/events_and_downloads.dart similarity index 98% rename from lib/screens/settings/desktop/events_and_downloads.dart rename to lib/screens/settings/events_and_downloads.dart index f87cb468..509d38a6 100644 --- a/lib/screens/settings/desktop/events_and_downloads.dart +++ b/lib/screens/settings/events_and_downloads.dart @@ -20,7 +20,7 @@ import 'dart:io'; import 'package:bluecherry_client/providers/settings_provider.dart'; -import 'package:bluecherry_client/screens/settings/desktop/settings.dart'; +import 'package:bluecherry_client/screens/settings/settings_desktop.dart'; import 'package:bluecherry_client/screens/settings/shared/options_chooser_tile.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; diff --git a/lib/screens/settings/desktop/general.dart b/lib/screens/settings/general.dart similarity index 98% rename from lib/screens/settings/desktop/general.dart rename to lib/screens/settings/general.dart index 3980ae00..b2625f12 100644 --- a/lib/screens/settings/desktop/general.dart +++ b/lib/screens/settings/general.dart @@ -18,7 +18,7 @@ */ import 'package:bluecherry_client/providers/settings_provider.dart'; -import 'package:bluecherry_client/screens/settings/desktop/settings.dart'; +import 'package:bluecherry_client/screens/settings/settings_desktop.dart'; import 'package:bluecherry_client/screens/settings/shared/options_chooser_tile.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/widgets/misc.dart'; diff --git a/lib/screens/settings/desktop/privacy_and_security.dart b/lib/screens/settings/privacy_and_security.dart similarity index 72% rename from lib/screens/settings/desktop/privacy_and_security.dart rename to lib/screens/settings/privacy_and_security.dart index cc61d8d1..55da33f4 100644 --- a/lib/screens/settings/desktop/privacy_and_security.dart +++ b/lib/screens/settings/privacy_and_security.dart @@ -17,7 +17,7 @@ * along with this program. If not, see . */ -import 'package:bluecherry_client/screens/settings/desktop/settings.dart'; +import 'package:bluecherry_client/screens/settings/settings_desktop.dart'; import 'package:bluecherry_client/screens/settings/shared/options_chooser_tile.dart'; import 'package:flutter/material.dart'; @@ -32,10 +32,10 @@ class PrivacySecuritySettings extends StatelessWidget { secondary: CircleAvatar( backgroundColor: Colors.transparent, foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.crop), + child: const Icon(Icons.analytics), ), contentPadding: DesktopSettings.horizontalPadding, - title: const Text('Use data'), + title: const Text('Allow Bluecherry to collect usage data'), subtitle: const Text( 'Allow Bluecherry to collect data to improve the app and provide ' 'better services. Data is collected anonymously and does not contain ' @@ -47,6 +47,8 @@ class PrivacySecuritySettings extends StatelessWidget { ), OptionsChooserTile( title: 'Automatically report errors', + description: 'Automatically report errors to Bluecherry to help us ' + 'improve the app. Error reports may contain personal information.', icon: Icons.error, value: 'On', values: ['On', 'Ask', 'Error'].map((e) => Option(text: e, value: e)), @@ -54,13 +56,23 @@ class PrivacySecuritySettings extends StatelessWidget { ), const Divider(), ListTile( - leading: const Icon(Icons.privacy_tip), + contentPadding: DesktopSettings.horizontalPadding, + leading: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.privacy_tip), + ), title: const Text('Privacy Policy'), trailing: const Icon(Icons.chevron_right), onTap: () {}, ), ListTile( - leading: const Icon(Icons.policy), + contentPadding: DesktopSettings.horizontalPadding, + leading: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.policy), + ), title: const Text('Terms of Service'), trailing: const Icon(Icons.chevron_right), onTap: () {}, diff --git a/lib/screens/settings/desktop/server_and_devices.dart b/lib/screens/settings/server_and_devices.dart similarity index 98% rename from lib/screens/settings/desktop/server_and_devices.dart rename to lib/screens/settings/server_and_devices.dart index a06f0083..ef17e930 100644 --- a/lib/screens/settings/desktop/server_and_devices.dart +++ b/lib/screens/settings/server_and_devices.dart @@ -19,8 +19,8 @@ import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/screens/layouts/video_status_label.dart'; -import 'package:bluecherry_client/screens/settings/desktop/settings.dart'; -import 'package:bluecherry_client/screens/settings/mobile/settings.dart'; +import 'package:bluecherry_client/screens/settings/settings_desktop.dart'; +import 'package:bluecherry_client/screens/settings/settings_mobile.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; diff --git a/lib/screens/settings/settings.dart b/lib/screens/settings/settings.dart index 3f91afd4..60c9e72c 100644 --- a/lib/screens/settings/settings.dart +++ b/lib/screens/settings/settings.dart @@ -17,8 +17,8 @@ * along with this program. If not, see . */ -import 'package:bluecherry_client/screens/settings/desktop/settings.dart'; -import 'package:bluecherry_client/screens/settings/mobile/settings.dart'; +import 'package:bluecherry_client/screens/settings/settings_desktop.dart'; +import 'package:bluecherry_client/screens/settings/settings_mobile.dart'; import 'package:bluecherry_client/utils/constants.dart'; import 'package:flutter/material.dart'; diff --git a/lib/screens/settings/desktop/settings.dart b/lib/screens/settings/settings_desktop.dart similarity index 88% rename from lib/screens/settings/desktop/settings.dart rename to lib/screens/settings/settings_desktop.dart index 9a138054..9b5977be 100644 --- a/lib/screens/settings/desktop/settings.dart +++ b/lib/screens/settings/settings_desktop.dart @@ -17,13 +17,13 @@ * along with this program. If not, see . */ -import 'package:bluecherry_client/screens/settings/desktop/advanced_options.dart'; -import 'package:bluecherry_client/screens/settings/desktop/application.dart'; -import 'package:bluecherry_client/screens/settings/desktop/events_and_downloads.dart'; -import 'package:bluecherry_client/screens/settings/desktop/general.dart'; -import 'package:bluecherry_client/screens/settings/desktop/privacy_and_security.dart'; -import 'package:bluecherry_client/screens/settings/desktop/server_and_devices.dart'; -import 'package:bluecherry_client/screens/settings/desktop/updates_and_help.dart'; +import 'package:bluecherry_client/screens/settings/advanced_options.dart'; +import 'package:bluecherry_client/screens/settings/application.dart'; +import 'package:bluecherry_client/screens/settings/events_and_downloads.dart'; +import 'package:bluecherry_client/screens/settings/general.dart'; +import 'package:bluecherry_client/screens/settings/privacy_and_security.dart'; +import 'package:bluecherry_client/screens/settings/server_and_devices.dart'; +import 'package:bluecherry_client/screens/settings/updates_and_help.dart'; import 'package:bluecherry_client/utils/constants.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; diff --git a/lib/screens/settings/mobile/settings.dart b/lib/screens/settings/settings_mobile.dart similarity index 93% rename from lib/screens/settings/mobile/settings.dart rename to lib/screens/settings/settings_mobile.dart index 6813e5bd..40e970e3 100644 --- a/lib/screens/settings/mobile/settings.dart +++ b/lib/screens/settings/settings_mobile.dart @@ -24,13 +24,13 @@ import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/screens/layouts/device_grid.dart'; import 'package:bluecherry_client/screens/servers/edit_server.dart'; import 'package:bluecherry_client/screens/servers/edit_server_settings.dart'; -import 'package:bluecherry_client/screens/settings/desktop/advanced_options.dart'; -import 'package:bluecherry_client/screens/settings/desktop/application.dart'; -import 'package:bluecherry_client/screens/settings/desktop/events_and_downloads.dart'; -import 'package:bluecherry_client/screens/settings/desktop/general.dart'; -import 'package:bluecherry_client/screens/settings/desktop/privacy_and_security.dart'; -import 'package:bluecherry_client/screens/settings/desktop/server_and_devices.dart'; -import 'package:bluecherry_client/screens/settings/desktop/updates_and_help.dart'; +import 'package:bluecherry_client/screens/settings/advanced_options.dart'; +import 'package:bluecherry_client/screens/settings/application.dart'; +import 'package:bluecherry_client/screens/settings/events_and_downloads.dart'; +import 'package:bluecherry_client/screens/settings/general.dart'; +import 'package:bluecherry_client/screens/settings/privacy_and_security.dart'; +import 'package:bluecherry_client/screens/settings/server_and_devices.dart'; +import 'package:bluecherry_client/screens/settings/updates_and_help.dart'; import 'package:bluecherry_client/utils/constants.dart'; import 'package:bluecherry_client/widgets/drawer_button.dart'; import 'package:bluecherry_client/widgets/misc.dart'; @@ -40,7 +40,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; -part '../shared/server_tile.dart'; +part 'shared/server_tile.dart'; class MobileSettings extends StatefulWidget { const MobileSettings({super.key}); diff --git a/lib/screens/settings/shared/options_chooser_tile.dart b/lib/screens/settings/shared/options_chooser_tile.dart index 6afcba6a..a442e8cd 100644 --- a/lib/screens/settings/shared/options_chooser_tile.dart +++ b/lib/screens/settings/shared/options_chooser_tile.dart @@ -1,4 +1,4 @@ -import 'package:bluecherry_client/screens/settings/desktop/settings.dart'; +import 'package:bluecherry_client/screens/settings/settings_desktop.dart'; import 'package:flutter/material.dart'; class Option { diff --git a/lib/screens/settings/shared/server_tile.dart b/lib/screens/settings/shared/server_tile.dart index 8a1f0f3e..d02c27ee 100644 --- a/lib/screens/settings/shared/server_tile.dart +++ b/lib/screens/settings/shared/server_tile.dart @@ -17,7 +17,7 @@ * along with this program. If not, see . */ -part of '../mobile/settings.dart'; +part of '../settings_mobile.dart'; typedef OnRemoveServer = void Function(BuildContext, Server); diff --git a/lib/screens/settings/shared/update.dart b/lib/screens/settings/shared/update.dart index ee295034..b9988554 100644 --- a/lib/screens/settings/shared/update.dart +++ b/lib/screens/settings/shared/update.dart @@ -19,7 +19,7 @@ import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/providers/update_provider.dart'; -import 'package:bluecherry_client/screens/settings/desktop/settings.dart'; +import 'package:bluecherry_client/screens/settings/settings_desktop.dart'; import 'package:bluecherry_client/utils/methods.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/gestures.dart'; diff --git a/lib/screens/settings/desktop/updates_and_help.dart b/lib/screens/settings/updates_and_help.dart similarity index 98% rename from lib/screens/settings/desktop/updates_and_help.dart rename to lib/screens/settings/updates_and_help.dart index c8d15fa6..c97f8616 100644 --- a/lib/screens/settings/desktop/updates_and_help.dart +++ b/lib/screens/settings/updates_and_help.dart @@ -21,7 +21,7 @@ import 'dart:io'; import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/providers/update_provider.dart'; -import 'package:bluecherry_client/screens/settings/desktop/settings.dart'; +import 'package:bluecherry_client/screens/settings/settings_desktop.dart'; import 'package:bluecherry_client/screens/settings/shared/update.dart'; import 'package:bluecherry_client/utils/logging.dart'; import 'package:bluecherry_client/utils/window.dart'; diff --git a/lib/widgets/misc.dart b/lib/widgets/misc.dart index 625e2674..7ddbc740 100644 --- a/lib/widgets/misc.dart +++ b/lib/widgets/misc.dart @@ -19,7 +19,7 @@ import 'dart:async'; -import 'package:bluecherry_client/screens/settings/desktop/settings.dart'; +import 'package:bluecherry_client/screens/settings/settings_desktop.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/utils/methods.dart'; import 'package:bluecherry_client/widgets/squared_icon_button.dart'; From 03cc41b131d7899c40d69cc6d19bc406029d5bf5 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Wed, 21 Feb 2024 15:57:35 -0300 Subject: [PATCH 14/26] fix: Make use of the `SubHeader` widget on the sections --- .../settings/events_and_downloads.dart | 20 +- lib/screens/settings/general.dart | 4 +- lib/screens/settings/server_and_devices.dart | 267 +++++++----------- .../settings/shared/options_chooser_tile.dart | 39 +-- lib/screens/settings/updates_and_help.dart | 43 ++- lib/widgets/misc.dart | 6 +- 6 files changed, 148 insertions(+), 231 deletions(-) diff --git a/lib/screens/settings/events_and_downloads.dart b/lib/screens/settings/events_and_downloads.dart index 509d38a6..5de3d6b8 100644 --- a/lib/screens/settings/events_and_downloads.dart +++ b/lib/screens/settings/events_and_downloads.dart @@ -22,6 +22,7 @@ import 'dart:io'; import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/screens/settings/settings_desktop.dart'; import 'package:bluecherry_client/screens/settings/shared/options_chooser_tile.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'; @@ -35,11 +36,8 @@ class EventsAndDownloadsSettings extends StatelessWidget { final loc = AppLocalizations.of(context); final theme = Theme.of(context); final settings = context.watch(); - return ListView(padding: DesktopSettings.verticalPadding, children: [ - Padding( - padding: DesktopSettings.horizontalPadding, - child: Text(loc.downloads, style: theme.textTheme.titleMedium), - ), + return ListView(children: [ + SubHeader(loc.downloads), CheckboxListTile.adaptive( value: false, onChanged: (v) {}, @@ -86,10 +84,7 @@ class EventsAndDownloadsSettings extends StatelessWidget { 'Block the app from closing when there are ongoing downloads'), ), const SizedBox(height: 20.0), - Padding( - padding: DesktopSettings.horizontalPadding, - child: Text('Events', style: theme.textTheme.titleMedium), - ), + const SubHeader('Events'), CheckboxListTile.adaptive( value: false, onChanged: (v) {}, @@ -139,10 +134,7 @@ class EventsAndDownloadsSettings extends StatelessWidget { ), ), const SizedBox(height: 20.0), - Padding( - padding: DesktopSettings.horizontalPadding, - child: Text('Timeline of Events', style: theme.textTheme.titleMedium), - ), + const SubHeader('Timeline of Events'), CheckboxListTile.adaptive( value: false, onChanged: (v) {}, @@ -177,7 +169,7 @@ class EventsAndDownloadsSettings extends StatelessWidget { description: 'When the timeline should begin.', icon: Icons.flag, value: '', - values: [ + values: const [ Option(value: '', icon: Icons.start, text: 'Beginning'), Option(value: '', icon: Icons.first_page, text: 'First event'), Option(value: '', icon: Icons.hourglass_bottom, text: 'An hour ago'), diff --git a/lib/screens/settings/general.dart b/lib/screens/settings/general.dart index b2625f12..bdb4116d 100644 --- a/lib/screens/settings/general.dart +++ b/lib/screens/settings/general.dart @@ -149,7 +149,7 @@ class GeneralSettings extends StatelessWidget { title: 'Automatic streaming', description: 'When to stream videos automatically on startup', value: '', - values: [ + values: const [ Option(value: '', icon: Icons.insights, text: 'Auto'), Option(value: '', icon: Icons.wifi, text: 'Wifi only'), Option(value: '', icon: Icons.not_interested, text: 'Never'), @@ -162,7 +162,7 @@ class GeneralSettings extends StatelessWidget { description: 'When to keep streams playing when the app is in background', value: '', - values: [ + values: const [ Option(value: '', icon: Icons.insights, text: 'Auto'), Option(value: '', icon: Icons.wifi, text: 'Wifi only'), Option(value: '', icon: Icons.not_interested, text: 'Never'), diff --git a/lib/screens/settings/server_and_devices.dart b/lib/screens/settings/server_and_devices.dart index ef17e930..11583bfe 100644 --- a/lib/screens/settings/server_and_devices.dart +++ b/lib/screens/settings/server_and_devices.dart @@ -21,7 +21,9 @@ import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/screens/layouts/video_status_label.dart'; import 'package:bluecherry_client/screens/settings/settings_desktop.dart'; import 'package:bluecherry_client/screens/settings/settings_mobile.dart'; +import 'package:bluecherry_client/screens/settings/shared/options_chooser_tile.dart'; import 'package:bluecherry_client/utils/extensions.dart'; +import 'package:bluecherry_client/widgets/misc.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -34,25 +36,15 @@ class ServerSettings extends StatelessWidget { @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context); - final theme = Theme.of(context); - return ListView(padding: DesktopSettings.verticalPadding, children: [ - Padding( - padding: DesktopSettings.horizontalPadding, - child: Text(loc.servers, style: theme.textTheme.titleMedium), - ), + return ListView(children: [ + SubHeader(loc.servers), const ServersList(), const SizedBox(height: 8.0), - Padding( - padding: DesktopSettings.horizontalPadding, - child: Text(loc.streamingSettings, style: theme.textTheme.titleMedium), - ), + SubHeader(loc.streamingSettings), const SizedBox(height: 8.0), const StreamingSettings(), const SizedBox(height: 12.0), - Padding( - padding: DesktopSettings.horizontalPadding, - child: Text('Devices Settings', style: theme.textTheme.titleMedium), - ), + const SubHeader('Devices Settings'), const SizedBox(height: 8.0), const CamerasSettings(), ]); @@ -66,60 +58,40 @@ class StreamingSettings extends StatelessWidget { Widget build(BuildContext context) { final settings = context.watch(); final loc = AppLocalizations.of(context); - final theme = Theme.of(context); return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - ListTile( - leading: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.sensors), - ), - title: Text(loc.streamingType), - contentPadding: DesktopSettings.horizontalPadding, - trailing: DropdownButton( - value: settings.streamingType, - onChanged: (v) { - if (v != null) { - settings.streamingType = v; - } - }, - items: StreamingType.values.map((value) { - return DropdownMenuItem( - value: value, - // Disable RTSP on web - enabled: !kIsWeb || value != StreamingType.rtsp, - child: Text(value.name.toUpperCase()), - ); - }).toList(), - ), + OptionsChooserTile( + title: loc.streamingType, + icon: Icons.sensors, + value: settings.streamingType, + values: StreamingType.values.map((value) { + return Option( + value: value, + // Disable RTSP on web + enabled: !kIsWeb || value != StreamingType.rtsp, + text: value.name.toUpperCase(), + ); + }), + onChanged: (v) { + settings.streamingType = v; + }, ), const SizedBox(height: 8.0), - ListTile( - enabled: settings.streamingType == StreamingType.rtsp, - title: Text(loc.rtspProtocol), - leading: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.sensors), - ), - contentPadding: DesktopSettings.horizontalPadding, - trailing: DropdownButton( - value: settings.rtspProtocol, - onChanged: !kIsWeb && settings.streamingType == StreamingType.rtsp - ? (v) { - if (v != null) { - settings.rtspProtocol = v; - } - } - : null, - items: RTSPProtocol.values.map((p) { - return DropdownMenuItem( - value: p, - child: Text(p.name.toUpperCase()), - ); - }).toList(), - ), + OptionsChooserTile( + title: loc.rtspProtocol, + icon: Icons.sensors, + value: settings.rtspProtocol, + values: RTSPProtocol.values.map((value) { + return Option( + value: value, + text: value.name.toUpperCase(), + ); + }), + onChanged: !kIsWeb && settings.streamingType == StreamingType.rtsp + ? (v) { + settings.rtspProtocol = v; + } + : null, ), ]); } @@ -135,97 +107,59 @@ class CamerasSettings extends StatelessWidget { final loc = AppLocalizations.of(context); return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!kIsWeb) - ListTile( - leading: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.hd), - ), - title: Text(loc.renderingQuality), - subtitle: Text(loc.renderingQualityDescription), - contentPadding: DesktopSettings.horizontalPadding, - trailing: DropdownButton( - value: settings.videoQuality, - onChanged: (v) { - if (v != null) { - settings.videoQuality = v; - } - }, - items: RenderingQuality.values.map((q) { - return DropdownMenuItem( - value: q, - child: Text(q.locale(context)), - ); - }).toList(), - ), - ), - const SizedBox(height: 8.0), - ListTile( - leading: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.fit_screen), - ), - title: Text(loc.cameraViewFit), - subtitle: Text(loc.cameraViewFitDescription), - contentPadding: DesktopSettings.horizontalPadding, - trailing: DropdownButton( - value: settings.cameraViewFit, + OptionsChooserTile( + title: loc.renderingQuality, + icon: Icons.hd, + value: settings.videoQuality, + values: RenderingQuality.values.map((value) { + return Option( + value: value, + text: value.locale(context), + ); + }), onChanged: (v) { - if (v != null) { - settings.cameraViewFit = v; - } + settings.videoQuality = v; }, - items: UnityVideoFit.values.map((q) { - return DropdownMenuItem( - value: q, - child: Row(children: [ - Icon(q.icon), - const SizedBox(width: 8.0), - Text(q.locale(context)), - ]), - ); - }).toList(), ), + const SizedBox(height: 8.0), + OptionsChooserTile( + title: loc.cameraViewFit, + description: loc.cameraViewFitDescription, + icon: Icons.fit_screen, + value: settings.cameraViewFit, + values: UnityVideoFit.values.map((value) { + return Option( + value: value, + text: value.locale(context), + ); + }), + onChanged: (v) { + settings.cameraViewFit = v; + }, ), const SizedBox(height: 8.0), - ListTile( - leading: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.sync), - ), - title: const Text('Refresh Period'), - subtitle: const Text('How often to refresh the cameras'), - contentPadding: DesktopSettings.horizontalPadding, - trailing: DropdownButton( - value: Duration.zero, - onChanged: (v) {}, - items: const [ - Duration.zero, - Duration(seconds: 30), - Duration(minutes: 2), - Duration(minutes: 5), - ].map((q) { - return DropdownMenuItem( - value: q, - child: Row(children: [ - // Icon(q.icon), - // const SizedBox(width: 8.0), - Text(q.humanReadableCompact(context)), - ]), - ); - }).toList(), - ), + OptionsChooserTile( + title: 'Refresh Period', + description: 'How often to refresh the cameras', + icon: Icons.sync, + value: Duration.zero, + values: const [ + Duration.zero, + Duration(seconds: 30), + Duration(minutes: 2), + Duration(minutes: 5), + ].map((q) { + return Option( + value: q, + text: q.humanReadableCompact(context), + ); + }), + onChanged: (v) {}, ), const SizedBox(height: 8.0), - ListTile( - leading: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.watch_later), - ), - title: Text(loc.lateStreamBehavior), + OptionsChooserTile( + title: loc.lateStreamBehavior, + description: loc.lateStreamBehaviorDescription, subtitle: RichText( text: TextSpan( text: loc.lateStreamBehaviorDescription, @@ -275,25 +209,17 @@ class CamerasSettings extends StatelessWidget { ], ), ), - contentPadding: DesktopSettings.horizontalPadding, - trailing: DropdownButton( - value: settings.lateVideoBehavior, - onChanged: (v) { - if (v != null) { - settings.lateVideoBehavior = v; - } - }, - items: LateVideoBehavior.values.map((q) { - return DropdownMenuItem( - value: q, - child: Row(children: [ - Icon(q.icon), - const SizedBox(width: 8.0), - Text(q.locale(context)), - ]), - ); - }).toList(), - ), + icon: Icons.watch_later, + value: settings.lateVideoBehavior, + values: LateVideoBehavior.values.map((value) { + return Option( + value: value, + text: value.locale(context), + ); + }), + onChanged: (v) { + settings.lateVideoBehavior = v; + }, ), const SizedBox(height: 8.0), CheckboxListTile.adaptive( @@ -316,7 +242,12 @@ class CamerasSettings extends StatelessWidget { child: const Icon(Icons.memory), ), title: const Text('Hardware rendering'), - subtitle: const Text('Use hardware rendering when available'), + subtitle: const Text( + 'Use hardware rendering when available. This will improve the ' + 'performance of the video streams and reduce the CPU usage. ' + 'If not supported, it will fall back to software rendering. ', + ), + isThreeLine: true, contentPadding: DesktopSettings.horizontalPadding, value: true, onChanged: (v) {}, diff --git a/lib/screens/settings/shared/options_chooser_tile.dart b/lib/screens/settings/shared/options_chooser_tile.dart index a442e8cd..4f2bb62a 100644 --- a/lib/screens/settings/shared/options_chooser_tile.dart +++ b/lib/screens/settings/shared/options_chooser_tile.dart @@ -5,27 +5,31 @@ class Option { final T value; final IconData? icon; final String text; + final bool enabled; - Option({ + const Option({ required this.value, required this.text, this.icon, + this.enabled = true, }); } class OptionsChooserTile extends StatelessWidget { final String title; final String? description; + final Widget? subtitle; final IconData icon; final T value; final Iterable> values; - final ValueChanged onChanged; + final ValueChanged? onChanged; const OptionsChooserTile({ super.key, required this.title, this.description, + this.subtitle, required this.icon, required this.value, required this.values, @@ -51,18 +55,19 @@ class OptionsChooserTile extends StatelessWidget { ), title: Text(title), textColor: theme.textTheme.bodyLarge?.color, - subtitle: description == null - ? null - : Text( - description!, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.textTheme.bodySmall?.color, - ), - ), + subtitle: subtitle ?? + (description == null + ? null + : Text( + description!, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.textTheme.bodySmall?.color, + ), + )), tilePadding: DesktopSettings.horizontalPadding, trailing: Text(values .firstWhere( - (v) => v.text == value, + (v) => v.value == value, orElse: () => values.first, ) .text), @@ -74,11 +79,13 @@ class OptionsChooserTile extends StatelessWidget { ), value: option.value, groupValue: value, - onChanged: (value) { - if (value != null) { - onChanged(value); - } - }, + onChanged: option.enabled && onChanged != null + ? (value) { + if (value != null) { + onChanged!(value); + } + } + : null, secondary: option.icon == null ? null : Icon(option.icon), controlAffinity: ListTileControlAffinity.trailing, title: Padding( diff --git a/lib/screens/settings/updates_and_help.dart b/lib/screens/settings/updates_and_help.dart index c97f8616..7e37363b 100644 --- a/lib/screens/settings/updates_and_help.dart +++ b/lib/screens/settings/updates_and_help.dart @@ -21,10 +21,10 @@ import 'dart:io'; import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/providers/update_provider.dart'; -import 'package:bluecherry_client/screens/settings/settings_desktop.dart'; import 'package:bluecherry_client/screens/settings/shared/update.dart'; import 'package:bluecherry_client/utils/logging.dart'; import 'package:bluecherry_client/utils/window.dart'; +import 'package:bluecherry_client/widgets/misc.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -35,35 +35,26 @@ class UpdatesSettings extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = Theme.of(context); final loc = AppLocalizations.of(context); - return ListView(padding: DesktopSettings.verticalPadding, children: [ + return ListView(children: [ if (!kIsWeb) ...[ - Padding( - padding: DesktopSettings.horizontalPadding, - child: - Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - loc.updates, - style: theme.textTheme.titleMedium, - ), - Text( - loc.runningOn(() { - if (kIsWeb) { - return 'WEB'; - } else if (Platform.isLinux) { - return loc.linux(UpdateManager.linuxEnvironment.name); - } else if (Platform.isWindows) { - return loc.windows; - } + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + SubHeader( + loc.updates, + subtext: loc.runningOn(() { + if (kIsWeb) { + return 'WEB'; + } else if (Platform.isLinux) { + return loc.linux(UpdateManager.linuxEnvironment.name); + } else if (Platform.isWindows) { + return loc.windows; + } - return defaultTargetPlatform.name; - }()), - style: theme.textTheme.labelSmall, - ), - ]), - ), + return defaultTargetPlatform.name; + }()), + ), + ]), const AppUpdateCard(), const AppUpdateOptions(), ], diff --git a/lib/widgets/misc.dart b/lib/widgets/misc.dart index 7ddbc740..569380a2 100644 --- a/lib/widgets/misc.dart +++ b/lib/widgets/misc.dart @@ -221,11 +221,7 @@ class SubHeader extends StatelessWidget { children: [ Text( text.toUpperCase(), - style: theme.textTheme.labelSmall?.copyWith( - color: theme.textTheme.displaySmall?.color, - fontSize: 12.0, - fontWeight: FontWeight.w600, - ), + style: theme.textTheme.labelMedium?.copyWith(), textAlign: textAlign, ), if (subtext != null) From 39a1af476f01b9db0daf19ed2b0081343cc7c329 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Wed, 28 Feb 2024 16:29:38 -0300 Subject: [PATCH 15/26] fix: Basic settings functionality --- ...firebase_messaging_background_handler.dart | 11 +- lib/main.dart | 4 +- lib/providers/home_provider.dart | 2 +- lib/providers/settings.dart | 482 ++++++++++++ lib/providers/settings_provider.dart | 722 +++++++++++------- .../events_timeline/desktop/timeline.dart | 4 +- .../desktop/timeline_card.dart | 10 +- .../mobile/timeline_device_view.dart | 6 +- .../layouts/desktop/desktop_device_grid.dart | 6 +- .../layouts/desktop/external_stream.dart | 3 +- .../layouts/desktop/layout_manager.dart | 2 +- .../layouts/desktop/multicast_view.dart | 2 +- lib/screens/layouts/desktop/stream_data.dart | 8 +- lib/screens/layouts/mobile/device_view.dart | 2 +- .../layouts/mobile/mobile_device_grid.dart | 2 +- lib/screens/layouts/video_status_label.dart | 2 +- .../multi_window/single_camera_window.dart | 2 +- lib/screens/players/event_player_desktop.dart | 4 +- lib/screens/players/event_player_mobile.dart | 4 +- lib/screens/players/live_player.dart | 8 +- .../servers/additional_server_settings.dart | 8 +- lib/screens/settings/advanced_options.dart | 8 +- lib/screens/settings/application.dart | 14 +- lib/screens/settings/general.dart | 31 +- lib/screens/settings/server_and_devices.dart | 53 +- lib/screens/settings/shared/update.dart | 3 +- lib/screens/settings/updates_and_help.dart | 8 +- lib/utils/video_player.dart | 8 +- lib/utils/window.dart | 4 +- 29 files changed, 1037 insertions(+), 386 deletions(-) create mode 100644 lib/providers/settings.dart diff --git a/lib/firebase_messaging_background_handler.dart b/lib/firebase_messaging_background_handler.dart index e1af5bb9..93ad36cf 100644 --- a/lib/firebase_messaging_background_handler.dart +++ b/lib/firebase_messaging_background_handler.dart @@ -52,7 +52,8 @@ Future _firebaseMessagingHandler(RemoteMessage message) async { await configureStorage(); await ServersProvider.ensureInitialized(); await SettingsProvider.ensureInitialized(); - if (SettingsProvider.instance.snoozedUntil.isAfter(DateTime.now())) { + if (SettingsProvider.instance.kSnoozeNotificationsUntil.value + .isAfter(DateTime.now())) { debugPrint( 'SettingsProvider.instance.snoozedUntil.isAfter(DateTime.now())', ); @@ -192,7 +193,7 @@ Future _backgroundClickAction(ReceivedAction action) async { if (action.buttonKeyPressed.isEmpty) { debugPrint('action.buttonKeyPressed.isEmpty'); // Fetch device & server details to show the [DeviceFullscreenViewer]. - if (SettingsProvider.instance.notificationClickBehavior == + if (SettingsProvider.instance.kNotificationClickBehavior.value == NotificationClickBehavior.showFullscreenCamera) { final eventType = action.payload!['eventType']; final serverUUID = action.payload!['serverId']; @@ -264,7 +265,8 @@ Future _backgroundClickAction(ReceivedAction action) async { }, ); debugPrint(DateTime.now().add(duration).toString()); - SettingsProvider.instance.snoozedUntil = DateTime.now().add(duration); + SettingsProvider.instance.kSnoozeNotificationsUntil.value = + DateTime.now().add(duration); if (action.id != null) { AwesomeNotifications().dismiss(action.id!); } @@ -282,7 +284,8 @@ abstract class FirebaseConfiguration { ); FirebaseMessaging.onBackgroundMessage(_firebaseMessagingHandler); FirebaseMessaging.onMessage.listen((RemoteMessage message) async { - if (SettingsProvider.instance.snoozedUntil.isAfter(DateTime.now())) { + if (SettingsProvider.instance.kSnoozeNotificationsUntil.value + .isAfter(DateTime.now())) { debugPrint( 'SettingsProvider.instance.snoozedUntil.isAfter(DateTime.now())', ); diff --git a/lib/main.dart b/lib/main.dart index 8bb6d838..b9269e78 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -306,7 +306,7 @@ class _UnityAppState extends State debugShowCheckedModeBanner: false, navigatorKey: navigatorKey, navigatorObservers: [NObserver()], - locale: settings.locale, + locale: settings.kLanguageCode.value, localizationsDelegates: const [ AppLocalizations.delegate, GlobalMaterialLocalizations.delegate, @@ -315,7 +315,7 @@ class _UnityAppState extends State LocaleNamesLocalizationsDelegate(), ], supportedLocales: AppLocalizations.supportedLocales, - themeMode: settings.themeMode, + themeMode: settings.kThemeMode.value, theme: createTheme(brightness: Brightness.light), darkTheme: createTheme(brightness: Brightness.dark), initialRoute: '/', diff --git a/lib/providers/home_provider.dart b/lib/providers/home_provider.dart index db94db93..3bc3bd7b 100644 --- a/lib/providers/home_provider.dart +++ b/lib/providers/home_provider.dart @@ -149,7 +149,7 @@ class HomeProvider extends ChangeNotifier { } final settings = context.read(); - if (!settings.wakelockEnabled) { + if (!settings.kWakelock.value) { WakelockPlus.disable(); } else { switch (tab) { diff --git a/lib/providers/settings.dart b/lib/providers/settings.dart new file mode 100644 index 00000000..62ac8ffd --- /dev/null +++ b/lib/providers/settings.dart @@ -0,0 +1,482 @@ +/* + * 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 . + */ + +import 'package:bluecherry_client/providers/app_provider_interface.dart'; +import 'package:bluecherry_client/providers/settings_provider.dart'; +import 'package:bluecherry_client/screens/layouts/desktop/external_stream.dart'; +import 'package:bluecherry_client/utils/storage.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:unity_video_player/unity_video_player.dart'; + +enum NetworkUsage { auto, wifiOnly, never } + +enum TimelineIntialPoint { beggining, firstEvent, lastEvent } + +enum EnabledPreference { on, ask, never } + +class SettingsOption { + final String key; + final T def; + + late final String Function(T value) saveAs; + late final T Function(String value) loadFrom; + + late T value; + + SettingsOption({ + required this.key, + required this.def, + String Function(T value)? saveAs, + T Function(String value)? loadFrom, + }) { + value = def; + + if (saveAs != null) { + this.saveAs = saveAs; + } else if (T == bool) { + this.saveAs = (value) => value.toString(); + } else if (T == Duration) { + this.saveAs = (value) => (value as Duration).inMilliseconds.toString(); + } else if (T == Enum) { + this.saveAs = (value) => (value as Enum).index.toString(); + } else { + this.saveAs = (value) => value.toString(); + } + + if (loadFrom != null) { + this.loadFrom = loadFrom; + } else if (T == bool) { + this.loadFrom = (value) => (bool.tryParse(value) ?? def) as T; + } else if (T == Duration) { + this.loadFrom = (value) => Duration(milliseconds: int.parse(value)) as T; + } else if (T == Enum) { + throw UnsupportedError('Enum type must provide a loadFrom function'); + } else { + this.loadFrom = (value) => value as T; + } + } + + String get defAsString => saveAs(def); +} + +class AppSettings extends UnityProvider { + AppSettings._(); + static late AppSettings instance; + + // General settings + final kLayoutCyclePeriod = SettingsOption( + def: const Duration(seconds: 5), + key: 'general.cycle_period', + ); + final kWakelock = SettingsOption( + def: true, + key: 'general.wakelock', + ); + + // Notifications + final kNotificationsEnabled = SettingsOption( + def: true, + key: 'notifications.enabled', + ); + final kSnoozeNotificationsUntil = SettingsOption( + def: null, + key: 'notifications.snooze_until', + ); + final kNotificationClickBehavior = SettingsOption( + def: NotificationClickBehavior.showEventsScreen, + key: 'notifications.click_behavior', + loadFrom: (value) => NotificationClickBehavior.values[int.parse(value)], + ); + + // Data usage + final kAutomaticStreaming = SettingsOption( + def: NetworkUsage.wifiOnly, + key: 'data_usage.automatic_streaming', + loadFrom: (value) => NetworkUsage.values[int.parse(value)], + ); + final kStreamOnBackground = SettingsOption( + def: NetworkUsage.wifiOnly, + key: 'data_usage.stream_on_background', + loadFrom: (value) => NetworkUsage.values[int.parse(value)], + ); + + // Streaming settings + final kStreamingType = SettingsOption( + def: kIsWeb ? StreamingType.hls : StreamingType.rtsp, + key: 'streaming.type', + loadFrom: (value) => StreamingType.values[int.parse(value)], + ); + final kRTSPProtocol = SettingsOption( + def: RTSPProtocol.tcp, + key: 'streaming.rtsp_protocol', + loadFrom: (value) => RTSPProtocol.values[int.parse(value)], + ); + final kRenderingQuality = SettingsOption( + def: RenderingQuality.automatic, + key: 'streaming.rendering_quality', + loadFrom: (value) => RenderingQuality.values[int.parse(value)], + ); + final kVideoFit = SettingsOption( + def: UnityVideoFit.contain, + key: 'streaming.video_fit', + loadFrom: (value) => UnityVideoFit.values[int.parse(value)], + ); + final kRefreshRate = SettingsOption( + def: const Duration(minutes: 5), + key: 'streaming.refresh_rate', + ); + final kLateStreamBehavior = SettingsOption( + def: LateVideoBehavior.automatic, + key: 'streaming.late_video_behavior', + loadFrom: (value) => LateVideoBehavior.values[int.parse(value)], + ); + final kReloadTimedOutStreams = SettingsOption( + def: true, + key: 'streaming.reload_timed_out_streams', + ); + final kUseHardwareDecoding = SettingsOption( + def: true, + key: 'streaming.use_hardware_decoding', + ); + + // Downloads + final kDownloadOnMobileData = SettingsOption( + def: false, + key: 'downloads.download_on_mobile_data', + ); + final kChooseLocationEveryTime = SettingsOption( + def: false, + key: 'downloads.choose_location_every_time', + ); + final kAllowAppCloseWhenDownloading = SettingsOption( + def: false, + key: 'downloads.allow_app_close_when_downloading', + ); + + // Events + final kPictureInPicture = SettingsOption( + def: false, + key: 'events.picture_in_picture', + ); + final kEventsSpeed = SettingsOption( + def: 1.0, + key: 'events.speed', + ); + final kEventsVolume = SettingsOption( + def: 1.0, + key: 'events.volume', + ); + + // Timeline of Events + final kShowDifferentColorsForEvents = SettingsOption( + def: false, + key: 'timeline.show_different_colors_for_events', + ); + final kPauseToBuffer = SettingsOption( + def: false, + key: 'timeline.pause_to_buffer', + ); + final kTimelineInitialPoint = SettingsOption( + def: TimelineIntialPoint.beggining, + key: 'timeline.initial_point', + loadFrom: (value) => TimelineIntialPoint.values[int.parse(value)], + ); + + // Application + final kThemeMode = SettingsOption( + def: ThemeMode.system, + key: 'application.theme_mode', + loadFrom: (value) => ThemeMode.values[int.parse(value)], + ); + final kLanguageCode = SettingsOption( + def: 'en', + key: 'application.language_code', + ); + final kDateFormat = SettingsOption( + def: 'EEEE, dd MMMM yyyy', + key: 'application.date_format', + ); + final kTimeFormat = SettingsOption( + def: 'hh:mm a', + key: 'application.time_format', + ); + + // Window + final kLaunchAppOnStartup = SettingsOption( + def: false, + key: 'window.launch_app_on_startup', + ); + final kMinimizeToTray = SettingsOption( + def: false, + key: 'window.minimize_to_tray', + ); + + // Acessibility + final kAnimationsEnabled = SettingsOption( + def: true, + key: 'accessibility.animations_enabled', + ); + final kHighContrast = SettingsOption( + def: false, + key: 'accessibility.high_contrast', + ); + final kLargeFont = SettingsOption( + def: false, + key: 'accessibility.large_font', + ); + + // Privacy and Security + final kAllowDataCollection = SettingsOption( + def: true, + key: 'privacy.allow_data_collection', + ); + final kAllowCrashReports = SettingsOption( + def: true, + key: 'privacy.allow_crash_reports', + ); + + // Updates + final kAutoUpdate = SettingsOption( + def: true, + key: 'updates.auto_update', + ); + final kShowReleaseNotes = SettingsOption( + def: true, + key: 'updates.show_release_notes', + ); + + // Other + final kDefaultBetaMatrixedZoomEnabled = SettingsOption( + def: false, + key: 'other.matrixed_zoom_enabled', + ); + final kMatrixSize = SettingsOption( + def: MatrixType.t16, + key: 'other.matrix_size', + loadFrom: (value) => MatrixType.values[int.parse(value)], + ); + final kShowDebugInfo = SettingsOption( + def: false, + key: 'other.show_debug_info', + ); + final kShowNetworkUsage = SettingsOption( + def: false, + key: 'other.show_network_usage', + ); + + /// Initializes the [AppSettings] instance & fetches state from `async` + /// `package:hive` method-calls. Called before [runApp]. + static Future ensureInitialized() async { + instance = AppSettings._(); + await instance.initialize(); + return instance; + } + + @override + Future initialize() async { + final data = await tryReadStorage(() => settings.read()); + + kLayoutCyclePeriod.value = kLayoutCyclePeriod.loadFrom( + data[kLayoutCyclePeriod.key] ?? kLayoutCyclePeriod.defAsString, + ); + kWakelock.value = kWakelock.loadFrom( + data[kWakelock.key] ?? kWakelock.defAsString, + ); + kNotificationsEnabled.value = kNotificationsEnabled.loadFrom( + data[kNotificationsEnabled.key] ?? kNotificationsEnabled.defAsString, + ); + kSnoozeNotificationsUntil.value = kSnoozeNotificationsUntil.loadFrom( + data[kSnoozeNotificationsUntil.key] ?? + kSnoozeNotificationsUntil.defAsString, + ); + kNotificationClickBehavior.value = kNotificationClickBehavior.loadFrom( + data[kNotificationClickBehavior.key] ?? + kNotificationClickBehavior.defAsString, + ); + kAutomaticStreaming.value = kAutomaticStreaming.loadFrom( + data[kAutomaticStreaming.key] ?? kAutomaticStreaming.defAsString, + ); + kStreamOnBackground.value = kStreamOnBackground.loadFrom( + data[kStreamOnBackground.key] ?? kStreamOnBackground.defAsString, + ); + kStreamingType.value = kStreamingType.loadFrom( + data[kStreamingType.key] ?? kStreamingType.defAsString, + ); + kRTSPProtocol.value = kRTSPProtocol.loadFrom( + data[kRTSPProtocol.key] ?? kRTSPProtocol.defAsString, + ); + kRenderingQuality.value = kRenderingQuality.loadFrom( + data[kRenderingQuality.key] ?? kRenderingQuality.defAsString, + ); + kVideoFit.value = kVideoFit.loadFrom( + data[kVideoFit.key] ?? kVideoFit.defAsString, + ); + kRefreshRate.value = kRefreshRate.loadFrom( + data[kRefreshRate.key] ?? kRefreshRate.defAsString, + ); + kLateStreamBehavior.value = kLateStreamBehavior.loadFrom( + data[kLateStreamBehavior.key] ?? kLateStreamBehavior.defAsString, + ); + kReloadTimedOutStreams.value = kReloadTimedOutStreams.loadFrom( + data[kReloadTimedOutStreams.key] ?? kReloadTimedOutStreams.defAsString, + ); + kUseHardwareDecoding.value = kUseHardwareDecoding.loadFrom( + data[kUseHardwareDecoding.key] ?? kUseHardwareDecoding.defAsString, + ); + kDownloadOnMobileData.value = kDownloadOnMobileData.loadFrom( + data[kDownloadOnMobileData.key] ?? kDownloadOnMobileData.defAsString, + ); + kChooseLocationEveryTime.value = kChooseLocationEveryTime.loadFrom( + data[kChooseLocationEveryTime.key] ?? + kChooseLocationEveryTime.defAsString, + ); + kAllowAppCloseWhenDownloading.value = + kAllowAppCloseWhenDownloading.loadFrom( + data[kAllowAppCloseWhenDownloading.key] ?? + kAllowAppCloseWhenDownloading.defAsString, + ); + kPictureInPicture.value = kPictureInPicture.loadFrom( + data[kPictureInPicture.key] ?? kPictureInPicture.defAsString, + ); + kEventsSpeed.value = kEventsSpeed.loadFrom( + data[kEventsSpeed.key] ?? kEventsSpeed.defAsString, + ); + kEventsVolume.value = kEventsVolume.loadFrom( + data[kEventsVolume.key] ?? kEventsVolume.defAsString, + ); + kShowDifferentColorsForEvents.value = + kShowDifferentColorsForEvents.loadFrom( + data[kShowDifferentColorsForEvents.key] ?? + kShowDifferentColorsForEvents.defAsString, + ); + kPauseToBuffer.value = kPauseToBuffer.loadFrom( + data[kPauseToBuffer.key] ?? kPauseToBuffer.defAsString, + ); + kTimelineInitialPoint.value = kTimelineInitialPoint.loadFrom( + data[kTimelineInitialPoint.key] ?? kTimelineInitialPoint.defAsString, + ); + kThemeMode.value = kThemeMode.loadFrom( + data[kThemeMode.key] ?? kThemeMode.defAsString, + ); + kLanguageCode.value = kLanguageCode.loadFrom( + data[kLanguageCode.key] ?? kLanguageCode.defAsString, + ); + kDateFormat.value = kDateFormat.loadFrom( + data[kDateFormat.key] ?? kDateFormat.defAsString, + ); + kTimeFormat.value = kTimeFormat.loadFrom( + data[kTimeFormat.key] ?? kTimeFormat.defAsString, + ); + kLaunchAppOnStartup.value = kLaunchAppOnStartup.loadFrom( + data[kLaunchAppOnStartup.key] ?? kLaunchAppOnStartup.defAsString, + ); + kMinimizeToTray.value = kMinimizeToTray.loadFrom( + data[kMinimizeToTray.key] ?? kMinimizeToTray.defAsString, + ); + kAnimationsEnabled.value = kAnimationsEnabled.loadFrom( + data[kAnimationsEnabled.key] ?? kAnimationsEnabled.defAsString, + ); + kHighContrast.value = kHighContrast.loadFrom( + data[kHighContrast.key] ?? kHighContrast.defAsString, + ); + kLargeFont.value = kLargeFont.loadFrom( + data[kLargeFont.key] ?? kLargeFont.defAsString, + ); + kAllowDataCollection.value = kAllowDataCollection.loadFrom( + data[kAllowDataCollection.key] ?? kAllowDataCollection.defAsString, + ); + kAllowCrashReports.value = kAllowCrashReports.loadFrom( + data[kAllowCrashReports.key] ?? kAllowCrashReports.defAsString, + ); + kAutoUpdate.value = kAutoUpdate.loadFrom( + data[kAutoUpdate.key] ?? kAutoUpdate.defAsString, + ); + kShowReleaseNotes.value = kShowReleaseNotes.loadFrom( + data[kShowReleaseNotes.key] ?? kShowReleaseNotes.defAsString, + ); + kDefaultBetaMatrixedZoomEnabled.value = + kDefaultBetaMatrixedZoomEnabled.loadFrom( + data[kDefaultBetaMatrixedZoomEnabled.key] ?? + kDefaultBetaMatrixedZoomEnabled.defAsString, + ); + kMatrixSize.value = kMatrixSize.loadFrom( + data[kMatrixSize.key] ?? kMatrixSize.defAsString, + ); + kShowDebugInfo.value = kShowDebugInfo.loadFrom( + data[kShowDebugInfo.key] ?? kShowDebugInfo.defAsString, + ); + kShowNetworkUsage.value = kShowNetworkUsage.loadFrom( + data[kShowNetworkUsage.key] ?? kShowNetworkUsage.defAsString, + ); + } + + @override + Future save({bool notifyListeners = true}) async { + try { + await settings.write({ + kLayoutCyclePeriod.key: + kLayoutCyclePeriod.saveAs(kLayoutCyclePeriod.value), + kWakelock.key: kWakelock.saveAs(kWakelock.value), + kNotificationsEnabled.key: + kNotificationsEnabled.saveAs(kNotificationsEnabled.value), + kSnoozeNotificationsUntil.key: + kSnoozeNotificationsUntil.saveAs(kSnoozeNotificationsUntil.value), + kNotificationClickBehavior.key: + kNotificationClickBehavior.saveAs(kNotificationClickBehavior.value), + kAutomaticStreaming.key: + kAutomaticStreaming.saveAs(kAutomaticStreaming.value), + kStreamOnBackground.key: + kStreamOnBackground.saveAs(kStreamOnBackground.value), + kStreamingType.key: kStreamingType.saveAs(kStreamingType.value), + kRTSPProtocol.key: kRTSPProtocol.saveAs(kRTSPProtocol.value), + kRenderingQuality.key: + kRenderingQuality.saveAs(kRenderingQuality.value), + kVideoFit.key: kVideoFit.saveAs(kVideoFit.value), + kRefreshRate.key: kRefreshRate.saveAs(kRefreshRate.value), + kLateStreamBehavior.key: + kLateStreamBehavior.saveAs(kLateStreamBehavior.value), + kReloadTimedOutStreams.key: + kReloadTimedOutStreams.saveAs(kReloadTimedOutStreams.value), + kUseHardwareDecoding.key: + kUseHardwareDecoding.saveAs(kUseHardwareDecoding.value), + kDownloadOnMobileData.key: + kDownloadOnMobileData.saveAs(kDownloadOnMobileData.value), + kChooseLocationEveryTime.key: + kChooseLocationEveryTime.saveAs(kChooseLocationEveryTime.value), + kAllowAppCloseWhenDownloading.key: kAllowAppCloseWhenDownloading + .saveAs(kAllowAppCloseWhenDownloading.value), + kPictureInPicture.key: + kPictureInPicture.saveAs(kPictureInPicture.value), + kEventsSpeed.key: kEventsSpeed.saveAs(kEventsSpeed.value), + kEventsVolume.key: kEventsVolume.saveAs(kEventsVolume.value), + kShowDifferentColorsForEvents.key: kShowDifferentColorsForEvents + .saveAs(kShowDifferentColorsForEvents.value), + kPauseToBuffer.key: kPauseToBuffer.saveAs(kPauseToBuffer.value), + kTimelineInitialPoint.key: + kTimelineInitialPoint.saveAs(kTimelineInitialPoint.value), + kThemeMode.key: kThemeMode.saveAs(kThemeMode.value), + kLanguageCode.key: kLanguageCode.saveAs(kLanguageCode.value), + }); + } catch (e) { + debugPrint('Error saving settings: $e'); + } + super.save(notifyListeners: notifyListeners); + } +} diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 8958f365..7619d57d 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -17,195 +17,282 @@ * along with this program. If not, see . */ -import 'dart:io'; - import 'package:bluecherry_client/providers/app_provider_interface.dart'; -import 'package:bluecherry_client/providers/downloads_provider.dart'; -import 'package:bluecherry_client/providers/home_provider.dart'; -import 'package:bluecherry_client/providers/update_provider.dart'; -import 'package:bluecherry_client/utils/constants.dart'; +import 'package:bluecherry_client/screens/layouts/desktop/external_stream.dart'; import 'package:bluecherry_client/utils/storage.dart'; -import 'package:bluecherry_client/utils/video_player.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/intl.dart'; -import 'package:system_date_time_format/system_date_time_format.dart'; import 'package:unity_video_player/unity_video_player.dart'; -/// This class manages & saves the settings inside the application. -class SettingsProvider extends UnityProvider { - SettingsProvider._(); - static late final SettingsProvider instance; - - static const kDefaultThemeMode = ThemeMode.system; - static const kDefaultDateFormat = 'EEEE, dd MMMM yyyy'; - static const kDefaultTimeFormat = 'hh:mm a'; - static final defaultSnoozedUntil = DateTime(1969, 7, 20, 20, 18, 04); - static const kDefaultNotificationClickBehavior = - NotificationClickBehavior.showFullscreenCamera; - static const kDefaultCameraViewFit = UnityVideoFit.contain; - static const kDefaultLayoutCyclingEnabled = false; - static const kDefaultLayoutCyclingTogglePeriod = Duration(seconds: 30); - static const kDefaultCameraRefreshPeriod = Duration(minutes: 5); - static Future get kDefaultDownloadsDirectory => - DownloadsManager.kDefaultDownloadsDirectory; - static const kDefaultStreamingType = - kIsWeb ? StreamingType.hls : StreamingType.rtsp; - static const kDefaultRTSPProtocol = RTSPProtocol.tcp; - static const kDefaultVideoQuality = RenderingQuality.automatic; - static const kDefaultWakelockEnabled = true; - static const kDefaultBetaMatrixedZoomEnabled = false; - static const kDefaultShowDebugInfo = false; - static const kDefaultLateVideoBehavior = LateVideoBehavior.automatic; - - 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 Duration _cameraRefreshPeriod; - late StreamingType _streamingType; - late RTSPProtocol _rtspProtocol; - late RenderingQuality _videoQuality; - late bool _wakelockEnabled; - late bool _betaMatrixedZoomEnabled; - late bool _showDebugInfo; - late LateVideoBehavior _lateVideoBehavior; - - // Getters. - Locale get locale => _locale; - ThemeMode get themeMode => _themeMode; - DateFormat get dateFormat => _dateFormat; - DateFormat get timeFormat => _timeFormat; - DateTime get snoozedUntil => _snoozedUntil; - NotificationClickBehavior get notificationClickBehavior => - _notificationClickBehavior; - UnityVideoFit get cameraViewFit => _cameraViewFit; - String get downloadsDirectory => _downloadsDirectory; - bool get layoutCyclingEnabled => _layoutCyclingEnabled; - Duration get layoutCyclingTogglePeriod => _layoutCyclingTogglePeriod; - Duration get cameraRefreshPeriod => _cameraRefreshPeriod; - StreamingType get streamingType => _streamingType; - RTSPProtocol get rtspProtocol => _rtspProtocol; - RenderingQuality get videoQuality => _videoQuality; - bool get wakelockEnabled => _wakelockEnabled; - bool get betaMatrixedZoomEnabled => _betaMatrixedZoomEnabled; - bool get showDebugInfo => _showDebugInfo; - LateVideoBehavior get lateVideoBehavior => _lateVideoBehavior; - - // Setters. - set locale(Locale value) { - _locale = value; - save(); - } - - set themeMode(ThemeMode value) { - _themeMode = value; - save().then((_) { - HomeProvider.setDefaultStatusBarStyle( - // we can not do [isLight: value == ThemeMode.light] because theme - // mode also accepts [ThemeMode.system]. When null is provided, the - // function will use the system's theme mode. - isLight: value == ThemeMode.light ? true : null, - ); - }); - } - - set dateFormat(DateFormat value) { - _dateFormat = value; - save(); - } - - set timeFormat(DateFormat value) { - _timeFormat = value; - save(); - } - - set snoozedUntil(DateTime value) { - _snoozedUntil = value; - save(); - } - - set notificationClickBehavior(NotificationClickBehavior value) { - _notificationClickBehavior = value; - save(); - } - - set cameraViewFit(UnityVideoFit value) { - _cameraViewFit = value; - save(); - } - - set downloadsDirectory(String value) { - _downloadsDirectory = value; - save(); - } - - set layoutCyclingEnabled(bool value) { - _layoutCyclingEnabled = value; - save(); - } - - set layoutCyclingTogglePeriod(Duration value) { - _layoutCyclingTogglePeriod = value; - save(); - } - - set cameraRefreshPeriod(Duration value) { - _cameraRefreshPeriod = value; - UnityPlayers.createTimer(); - save(); - } - - set streamingType(StreamingType value) { - _streamingType = value; - save(); - UnityPlayers.reloadAll(); - } - - set rtspProtocol(RTSPProtocol value) { - _rtspProtocol = value; - save(); - UnityPlayers.reloadAll(); - } - - set videoQuality(RenderingQuality value) { - _videoQuality = value; - save(); - } - - set wakelockEnabled(bool value) { - _wakelockEnabled = value; - UnityVideoPlayerInterface.wakelockEnabled = value; - save(); - } +enum NetworkUsage { auto, wifiOnly, never } + +enum TimelineIntialPoint { beggining, firstEvent, lastEvent } + +enum EnabledPreference { on, ask, never } + +class SettingsOption { + final String key; + final T def; + + late final String Function(T value) saveAs; + late final T Function(String value) loadFrom; + + late T value; + + SettingsOption({ + required this.key, + required this.def, + String Function(T value)? saveAs, + T Function(String value)? loadFrom, + }) { + value = def; + + if (saveAs != null) { + this.saveAs = saveAs; + } else if (T == bool) { + this.saveAs = (value) => value.toString(); + } else if (T == Duration) { + this.saveAs = (value) => (value as Duration).inMilliseconds.toString(); + } else if (T == Enum) { + this.saveAs = (value) => (value as Enum).index.toString(); + } else if (T == DateFormat) { + this.saveAs = (value) => (value as DateFormat).pattern ?? ''; + } else if (T == Locale) { + this.saveAs = (value) => (value as Locale).toLanguageTag(); + } else { + this.saveAs = (value) => value.toString(); + } - set betaMatrixedZoomEnabled(bool value) { - _betaMatrixedZoomEnabled = value; - if (!value) { - for (var player in UnityPlayers.players.values) { - player.resetCrop(); - } + if (loadFrom != null) { + this.loadFrom = loadFrom; + } else if (T == bool) { + this.loadFrom = (value) => (bool.tryParse(value) ?? def) as T; + } else if (T == Duration) { + this.loadFrom = (value) => Duration(milliseconds: int.parse(value)) as T; + } else if (T == Enum) { + throw UnsupportedError('Enum type must provide a loadFrom function'); + } else if (T == DateFormat) { + this.loadFrom = (value) => DateFormat(value) as T; + } else if (T == Locale) { + this.loadFrom = (value) => Locale.fromSubtags(languageCode: value) as T; + } else { + this.loadFrom = (value) => value as T; } - save(); } - set showDebugInfo(bool value) { - _showDebugInfo = value; - save(); - } + String get defAsString => saveAs(def); +} - set lateVideoBehavior(LateVideoBehavior value) { - _lateVideoBehavior = value; - save(); - } +class SettingsProvider extends UnityProvider { + SettingsProvider._(); + static late SettingsProvider instance; + + // General settings + final kLayoutCyclePeriod = SettingsOption( + def: const Duration(seconds: 5), + key: 'general.cycle_period', + ); + final kWakelock = SettingsOption( + def: true, + key: 'general.wakelock', + ); + + // Notifications + final kNotificationsEnabled = SettingsOption( + def: true, + key: 'notifications.enabled', + ); + final kSnoozeNotificationsUntil = SettingsOption( + def: DateTime.utc(1969, 7, 20, 20, 18, 04), + key: 'notifications.snooze_until', + ); + final kNotificationClickBehavior = SettingsOption( + def: NotificationClickBehavior.showEventsScreen, + key: 'notifications.click_behavior', + loadFrom: (value) => NotificationClickBehavior.values[int.parse(value)], + ); + + // Data usage + final kAutomaticStreaming = SettingsOption( + def: NetworkUsage.wifiOnly, + key: 'data_usage.automatic_streaming', + loadFrom: (value) => NetworkUsage.values[int.parse(value)], + ); + final kStreamOnBackground = SettingsOption( + def: NetworkUsage.wifiOnly, + key: 'data_usage.stream_on_background', + loadFrom: (value) => NetworkUsage.values[int.parse(value)], + ); + + // Streaming settings + final kStreamingType = SettingsOption( + def: kIsWeb ? StreamingType.hls : StreamingType.rtsp, + key: 'streaming.type', + loadFrom: (value) => StreamingType.values[int.parse(value)], + ); + final kRTSPProtocol = SettingsOption( + def: RTSPProtocol.tcp, + key: 'streaming.rtsp_protocol', + loadFrom: (value) => RTSPProtocol.values[int.parse(value)], + ); + final kRenderingQuality = SettingsOption( + def: RenderingQuality.automatic, + key: 'streaming.rendering_quality', + loadFrom: (value) => RenderingQuality.values[int.parse(value)], + ); + final kVideoFit = SettingsOption( + def: UnityVideoFit.contain, + key: 'streaming.video_fit', + loadFrom: (value) => UnityVideoFit.values[int.parse(value)], + ); + final kRefreshRate = SettingsOption( + def: const Duration(minutes: 5), + key: 'streaming.refresh_rate', + ); + final kLateStreamBehavior = SettingsOption( + def: LateVideoBehavior.automatic, + key: 'streaming.late_video_behavior', + loadFrom: (value) => LateVideoBehavior.values[int.parse(value)], + ); + final kReloadTimedOutStreams = SettingsOption( + def: true, + key: 'streaming.reload_timed_out_streams', + ); + final kUseHardwareDecoding = SettingsOption( + def: true, + key: 'streaming.use_hardware_decoding', + ); + + // Downloads + final kDownloadOnMobileData = SettingsOption( + def: false, + key: 'downloads.download_on_mobile_data', + ); + final kChooseLocationEveryTime = SettingsOption( + def: false, + key: 'downloads.choose_location_every_time', + ); + final kAllowAppCloseWhenDownloading = SettingsOption( + def: false, + key: 'downloads.allow_app_close_when_downloading', + ); + final kDownloadsDirectory = SettingsOption( + def: '', + key: 'downloads.directory', + ); + + // Events + final kPictureInPicture = SettingsOption( + def: false, + key: 'events.picture_in_picture', + ); + final kEventsSpeed = SettingsOption( + def: 1.0, + key: 'events.speed', + ); + final kEventsVolume = SettingsOption( + def: 1.0, + key: 'events.volume', + ); + + // Timeline of Events + final kShowDifferentColorsForEvents = SettingsOption( + def: false, + key: 'timeline.show_different_colors_for_events', + ); + final kPauseToBuffer = SettingsOption( + def: false, + key: 'timeline.pause_to_buffer', + ); + final kTimelineInitialPoint = SettingsOption( + def: TimelineIntialPoint.beggining, + key: 'timeline.initial_point', + loadFrom: (value) => TimelineIntialPoint.values[int.parse(value)], + ); + + // Application + final kThemeMode = SettingsOption( + def: ThemeMode.system, + key: 'application.theme_mode', + loadFrom: (value) => ThemeMode.values[int.parse(value)], + ); + final kLanguageCode = SettingsOption( + def: Locale.fromSubtags(languageCode: Intl.getCurrentLocale()), + key: 'application.language_code', + ); + final kDateFormat = SettingsOption( + def: DateFormat('EEEE, dd MMMM yyyy'), + key: 'application.date_format', + ); + final kTimeFormat = SettingsOption( + def: DateFormat('hh:mm a'), + key: 'application.time_format', + ); + + // Window + final kLaunchAppOnStartup = SettingsOption( + def: false, + key: 'window.launch_app_on_startup', + ); + final kMinimizeToTray = SettingsOption( + def: false, + key: 'window.minimize_to_tray', + ); + + // Acessibility + final kAnimationsEnabled = SettingsOption( + def: true, + key: 'accessibility.animations_enabled', + ); + final kHighContrast = SettingsOption( + def: false, + key: 'accessibility.high_contrast', + ); + final kLargeFont = SettingsOption( + def: false, + key: 'accessibility.large_font', + ); + + // Privacy and Security + final kAllowDataCollection = SettingsOption( + def: true, + key: 'privacy.allow_data_collection', + ); + final kAllowCrashReports = SettingsOption( + def: true, + key: 'privacy.allow_crash_reports', + ); + + // Updates + final kAutoUpdate = SettingsOption( + def: true, + key: 'updates.auto_update', + ); + final kShowReleaseNotes = SettingsOption( + def: true, + key: 'updates.show_release_notes', + ); + + // Other + final kDefaultBetaMatrixedZoomEnabled = SettingsOption( + def: false, + key: 'other.matrixed_zoom_enabled', + ); + final kMatrixSize = SettingsOption( + def: MatrixType.t16, + key: 'other.matrix_size', + loadFrom: (value) => MatrixType.values[int.parse(value)], + ); + final kShowDebugInfo = SettingsOption( + def: false, + key: 'other.show_debug_info', + ); + final kShowNetworkUsage = SettingsOption( + def: false, + key: 'other.show_network_usage', + ); /// Initializes the [SettingsProvider] instance & fetches state from `async` /// `package:hive` method-calls. Called before [runApp]. @@ -215,114 +302,204 @@ class SettingsProvider extends UnityProvider { return instance; } + @override + Future initialize() async { + final data = await tryReadStorage(() => settings.read()); + + kLayoutCyclePeriod.value = kLayoutCyclePeriod.loadFrom( + data[kLayoutCyclePeriod.key] ?? kLayoutCyclePeriod.defAsString, + ); + kWakelock.value = kWakelock.loadFrom( + data[kWakelock.key] ?? kWakelock.defAsString, + ); + kNotificationsEnabled.value = kNotificationsEnabled.loadFrom( + data[kNotificationsEnabled.key] ?? kNotificationsEnabled.defAsString, + ); + kSnoozeNotificationsUntil.value = kSnoozeNotificationsUntil.loadFrom( + data[kSnoozeNotificationsUntil.key] ?? + kSnoozeNotificationsUntil.defAsString, + ); + kNotificationClickBehavior.value = kNotificationClickBehavior.loadFrom( + data[kNotificationClickBehavior.key] ?? + kNotificationClickBehavior.defAsString, + ); + kAutomaticStreaming.value = kAutomaticStreaming.loadFrom( + data[kAutomaticStreaming.key] ?? kAutomaticStreaming.defAsString, + ); + kStreamOnBackground.value = kStreamOnBackground.loadFrom( + data[kStreamOnBackground.key] ?? kStreamOnBackground.defAsString, + ); + kStreamingType.value = kStreamingType.loadFrom( + data[kStreamingType.key] ?? kStreamingType.defAsString, + ); + kRTSPProtocol.value = kRTSPProtocol.loadFrom( + data[kRTSPProtocol.key] ?? kRTSPProtocol.defAsString, + ); + kRenderingQuality.value = kRenderingQuality.loadFrom( + data[kRenderingQuality.key] ?? kRenderingQuality.defAsString, + ); + kVideoFit.value = kVideoFit.loadFrom( + data[kVideoFit.key] ?? kVideoFit.defAsString, + ); + kRefreshRate.value = kRefreshRate.loadFrom( + data[kRefreshRate.key] ?? kRefreshRate.defAsString, + ); + kLateStreamBehavior.value = kLateStreamBehavior.loadFrom( + data[kLateStreamBehavior.key] ?? kLateStreamBehavior.defAsString, + ); + kReloadTimedOutStreams.value = kReloadTimedOutStreams.loadFrom( + data[kReloadTimedOutStreams.key] ?? kReloadTimedOutStreams.defAsString, + ); + kUseHardwareDecoding.value = kUseHardwareDecoding.loadFrom( + data[kUseHardwareDecoding.key] ?? kUseHardwareDecoding.defAsString, + ); + kDownloadOnMobileData.value = kDownloadOnMobileData.loadFrom( + data[kDownloadOnMobileData.key] ?? kDownloadOnMobileData.defAsString, + ); + kChooseLocationEveryTime.value = kChooseLocationEveryTime.loadFrom( + data[kChooseLocationEveryTime.key] ?? + kChooseLocationEveryTime.defAsString, + ); + kAllowAppCloseWhenDownloading.value = + kAllowAppCloseWhenDownloading.loadFrom( + data[kAllowAppCloseWhenDownloading.key] ?? + kAllowAppCloseWhenDownloading.defAsString, + ); + kPictureInPicture.value = kPictureInPicture.loadFrom( + data[kPictureInPicture.key] ?? kPictureInPicture.defAsString, + ); + kEventsSpeed.value = kEventsSpeed.loadFrom( + data[kEventsSpeed.key] ?? kEventsSpeed.defAsString, + ); + kEventsVolume.value = kEventsVolume.loadFrom( + data[kEventsVolume.key] ?? kEventsVolume.defAsString, + ); + kShowDifferentColorsForEvents.value = + kShowDifferentColorsForEvents.loadFrom( + data[kShowDifferentColorsForEvents.key] ?? + kShowDifferentColorsForEvents.defAsString, + ); + kPauseToBuffer.value = kPauseToBuffer.loadFrom( + data[kPauseToBuffer.key] ?? kPauseToBuffer.defAsString, + ); + kTimelineInitialPoint.value = kTimelineInitialPoint.loadFrom( + data[kTimelineInitialPoint.key] ?? kTimelineInitialPoint.defAsString, + ); + kThemeMode.value = kThemeMode.loadFrom( + data[kThemeMode.key] ?? kThemeMode.defAsString, + ); + kLanguageCode.value = kLanguageCode.loadFrom( + data[kLanguageCode.key] ?? kLanguageCode.defAsString, + ); + kDateFormat.value = kDateFormat.loadFrom( + data[kDateFormat.key] ?? kDateFormat.defAsString, + ); + kTimeFormat.value = kTimeFormat.loadFrom( + data[kTimeFormat.key] ?? kTimeFormat.defAsString, + ); + kLaunchAppOnStartup.value = kLaunchAppOnStartup.loadFrom( + data[kLaunchAppOnStartup.key] ?? kLaunchAppOnStartup.defAsString, + ); + kMinimizeToTray.value = kMinimizeToTray.loadFrom( + data[kMinimizeToTray.key] ?? kMinimizeToTray.defAsString, + ); + kAnimationsEnabled.value = kAnimationsEnabled.loadFrom( + data[kAnimationsEnabled.key] ?? kAnimationsEnabled.defAsString, + ); + kHighContrast.value = kHighContrast.loadFrom( + data[kHighContrast.key] ?? kHighContrast.defAsString, + ); + kLargeFont.value = kLargeFont.loadFrom( + data[kLargeFont.key] ?? kLargeFont.defAsString, + ); + kAllowDataCollection.value = kAllowDataCollection.loadFrom( + data[kAllowDataCollection.key] ?? kAllowDataCollection.defAsString, + ); + kAllowCrashReports.value = kAllowCrashReports.loadFrom( + data[kAllowCrashReports.key] ?? kAllowCrashReports.defAsString, + ); + kAutoUpdate.value = kAutoUpdate.loadFrom( + data[kAutoUpdate.key] ?? kAutoUpdate.defAsString, + ); + kShowReleaseNotes.value = kShowReleaseNotes.loadFrom( + data[kShowReleaseNotes.key] ?? kShowReleaseNotes.defAsString, + ); + kDefaultBetaMatrixedZoomEnabled.value = + kDefaultBetaMatrixedZoomEnabled.loadFrom( + data[kDefaultBetaMatrixedZoomEnabled.key] ?? + kDefaultBetaMatrixedZoomEnabled.defAsString, + ); + kMatrixSize.value = kMatrixSize.loadFrom( + data[kMatrixSize.key] ?? kMatrixSize.defAsString, + ); + kShowDebugInfo.value = kShowDebugInfo.loadFrom( + data[kShowDebugInfo.key] ?? kShowDebugInfo.defAsString, + ); + kShowNetworkUsage.value = kShowNetworkUsage.loadFrom( + data[kShowNetworkUsage.key] ?? kShowNetworkUsage.defAsString, + ); + } + @override Future save({bool notifyListeners = true}) async { try { await settings.write({ - kHiveLocale: locale.toLanguageTag(), - kHiveThemeMode: themeMode.index, - kHiveDateFormat: dateFormat.pattern!, - kHiveTimeFormat: timeFormat.pattern!, - kHiveSnoozedUntil: snoozedUntil.toIso8601String(), - kHiveNotificationClickBehavior: notificationClickBehavior.index, - kHiveCameraViewFit: cameraViewFit.index, - kHiveDownloadsDirectorySetting: downloadsDirectory, - kHiveLayoutCycling: layoutCyclingEnabled, - kHiveLayoutCyclingPeriod: layoutCyclingTogglePeriod.inMilliseconds, - kHiveCameraRefreshPeriod: cameraRefreshPeriod.inMilliseconds, - kHiveStreamingType: streamingType.index, - kHiveStreamingProtocol: rtspProtocol.index, - kHiveVideoQuality: videoQuality.index, - kHiveWakelockEnabled: wakelockEnabled, - kHiveBetaMatrixedZoom: betaMatrixedZoomEnabled, - kHiveShowDebugInfo: showDebugInfo, - kHiveLateVideoBehavior: lateVideoBehavior.index, + kLayoutCyclePeriod.key: + kLayoutCyclePeriod.saveAs(kLayoutCyclePeriod.value), + kWakelock.key: kWakelock.saveAs(kWakelock.value), + kNotificationsEnabled.key: + kNotificationsEnabled.saveAs(kNotificationsEnabled.value), + kSnoozeNotificationsUntil.key: + kSnoozeNotificationsUntil.saveAs(kSnoozeNotificationsUntil.value), + kNotificationClickBehavior.key: + kNotificationClickBehavior.saveAs(kNotificationClickBehavior.value), + kAutomaticStreaming.key: + kAutomaticStreaming.saveAs(kAutomaticStreaming.value), + kStreamOnBackground.key: + kStreamOnBackground.saveAs(kStreamOnBackground.value), + kStreamingType.key: kStreamingType.saveAs(kStreamingType.value), + kRTSPProtocol.key: kRTSPProtocol.saveAs(kRTSPProtocol.value), + kRenderingQuality.key: + kRenderingQuality.saveAs(kRenderingQuality.value), + kVideoFit.key: kVideoFit.saveAs(kVideoFit.value), + kRefreshRate.key: kRefreshRate.saveAs(kRefreshRate.value), + kLateStreamBehavior.key: + kLateStreamBehavior.saveAs(kLateStreamBehavior.value), + kReloadTimedOutStreams.key: + kReloadTimedOutStreams.saveAs(kReloadTimedOutStreams.value), + kUseHardwareDecoding.key: + kUseHardwareDecoding.saveAs(kUseHardwareDecoding.value), + kDownloadOnMobileData.key: + kDownloadOnMobileData.saveAs(kDownloadOnMobileData.value), + kChooseLocationEveryTime.key: + kChooseLocationEveryTime.saveAs(kChooseLocationEveryTime.value), + kAllowAppCloseWhenDownloading.key: kAllowAppCloseWhenDownloading + .saveAs(kAllowAppCloseWhenDownloading.value), + kPictureInPicture.key: + kPictureInPicture.saveAs(kPictureInPicture.value), + kEventsSpeed.key: kEventsSpeed.saveAs(kEventsSpeed.value), + kEventsVolume.key: kEventsVolume.saveAs(kEventsVolume.value), + kShowDifferentColorsForEvents.key: kShowDifferentColorsForEvents + .saveAs(kShowDifferentColorsForEvents.value), + kPauseToBuffer.key: kPauseToBuffer.saveAs(kPauseToBuffer.value), + kTimelineInitialPoint.key: + kTimelineInitialPoint.saveAs(kTimelineInitialPoint.value), + kThemeMode.key: kThemeMode.saveAs(kThemeMode.value), + kLanguageCode.key: kLanguageCode.saveAs(kLanguageCode.value), }); - } catch (error, stack) { - debugPrint('Failed to save settings: $error\n$stack'); + } catch (e) { + debugPrint('Error saving settings: $e'); } - super.save(notifyListeners: notifyListeners); } - @override - Future initialize() async { - final data = await tryReadStorage(() => settings.read()); - - if (data.containsKey(kHiveLocale)) { - _locale = Locale(data[kHiveLocale]!); - } else { - _locale = Locale(Intl.getCurrentLocale()); - } - if (data.containsKey(kHiveThemeMode)) { - _themeMode = ThemeMode.values[data[kHiveThemeMode]!]; - } else { - _themeMode = kDefaultThemeMode; - } - - initializeDateFormatting(_locale.languageCode); - Intl.defaultLocale = _locale.toLanguageTag(); - final systemLocale = Intl.getCurrentLocale(); - String? timePattern; - if (!UpdateManager.isEmbedded && !kIsWeb) { - // can not access system_date_time_format from embedded - final format = SystemDateTimeFormat(); - timePattern = await format.getTimePattern(); - } - - _dateFormat = DateFormat( - data[kHiveDateFormat] ?? kDefaultDateFormat, - systemLocale, - ); - _timeFormat = DateFormat( - data[kHiveTimeFormat] ?? timePattern ?? kDefaultTimeFormat, - systemLocale, - ); - _snoozedUntil = - DateTime.tryParse((data[kHiveSnoozedUntil] as String?) ?? '') ?? - defaultSnoozedUntil; - _notificationClickBehavior = NotificationClickBehavior.values[ - data[kHiveNotificationClickBehavior] ?? - kDefaultNotificationClickBehavior.index]; - _cameraViewFit = UnityVideoFit - .values[data[kHiveCameraViewFit] ?? kDefaultCameraViewFit.index]; - if (!kIsWeb) { - _downloadsDirectory = data[kHiveDownloadsDirectorySetting] ?? - ((await kDefaultDownloadsDirectory).path); - } - _layoutCyclingEnabled = - data[kHiveLayoutCycling] ?? kDefaultLayoutCyclingEnabled; - _layoutCyclingTogglePeriod = Duration( - milliseconds: data[kHiveLayoutCyclingPeriod] ?? - kDefaultLayoutCyclingTogglePeriod.inMilliseconds, - ); - _cameraRefreshPeriod = Duration( - milliseconds: data[kHiveCameraRefreshPeriod] ?? - kDefaultCameraRefreshPeriod.inMilliseconds, - ); - _streamingType = StreamingType - .values[data[kHiveStreamingType] ?? kDefaultStreamingType.index]; - _rtspProtocol = RTSPProtocol - .values[data[kHiveStreamingProtocol] ?? kDefaultRTSPProtocol.index]; - _videoQuality = RenderingQuality - .values[data[kHiveVideoQuality] ?? kDefaultVideoQuality.index]; - _wakelockEnabled = data[kHiveWakelockEnabled] ?? kDefaultWakelockEnabled; - _betaMatrixedZoomEnabled = - data[kHiveBetaMatrixedZoom] ?? kDefaultBetaMatrixedZoomEnabled; - _showDebugInfo = data[kHiveShowDebugInfo] ?? kDefaultShowDebugInfo; - _lateVideoBehavior = LateVideoBehavior.values[ - data[kHiveLateVideoBehavior] ?? kDefaultLateVideoBehavior.index]; - - notifyListeners(); - } - /// Formats the date according to the current [dateFormat]. /// /// [toLocal] defines if the date will be converted to local time. Defaults to `true` String formatDate(DateTime date, {bool toLocal = false}) { if (toLocal) date = date.toLocal(); - return dateFormat.format(date); + return kDateFormat.value.format(date); } /// Formats the date according to the current [dateFormat]. @@ -331,12 +508,7 @@ class SettingsProvider extends UnityProvider { String formatTime(DateTime time, {bool toLocal = false}) { if (toLocal) time = time.toLocal(); - return timeFormat.format(time); - } - - bool toggleCycling() { - layoutCyclingEnabled = !layoutCyclingEnabled; - return layoutCyclingEnabled; + return kTimeFormat.value.format(time); } } diff --git a/lib/screens/events_timeline/desktop/timeline.dart b/lib/screens/events_timeline/desktop/timeline.dart index 353fcf54..ca193ee0 100644 --- a/lib/screens/events_timeline/desktop/timeline.dart +++ b/lib/screens/events_timeline/desktop/timeline.dart @@ -670,7 +670,7 @@ class _TimelineEventsViewState extends State { ]), ), Text( - '${settings.dateFormat.format(timeline.currentDate)} ' + '${settings.kDateFormat.value.format(timeline.currentDate)} ' '${timelineTimeFormat.format(timeline.currentDate)}', ), ConstrainedBox( @@ -986,7 +986,7 @@ class _TimelineTile extends StatelessWidget { // 1)] // : theme.colorScheme.primary, color: theme.colorScheme.primary, - child: settings.showDebugInfo + child: settings.kShowDebugInfo.value ? Align( alignment: AlignmentDirectional.centerStart, child: Text( diff --git a/lib/screens/events_timeline/desktop/timeline_card.dart b/lib/screens/events_timeline/desktop/timeline_card.dart index 871b49ec..ad101db0 100644 --- a/lib/screens/events_timeline/desktop/timeline_card.dart +++ b/lib/screens/events_timeline/desktop/timeline_card.dart @@ -70,7 +70,7 @@ class _TimelineCardState extends State { color: Colors.transparent, fit: _fit ?? device.server.additionalSettings.videoFit ?? - settings.cameraViewFit, + settings.kVideoFit.value, paneBuilder: (context, controller) { if (currentEvent == null) { return RepaintBoundary( @@ -131,7 +131,7 @@ class _TimelineCardState extends State { const TextSpan(text: '\n'), if (states.isHovering) TextSpan( - text: settings.showDebugInfo + text: settings.kShowDebugInfo.value ? currentEvent .position(widget.timeline.currentDate) .toString() @@ -139,7 +139,7 @@ class _TimelineCardState extends State { .position(widget.timeline.currentDate) .humanReadableCompact(context), ), - if (settings.showDebugInfo) ...[ + if (settings.kShowDebugInfo.value) ...[ const TextSpan(text: '\ndebug: '), TextSpan(text: controller.currentPos.toString()), TextSpan( @@ -153,7 +153,7 @@ class _TimelineCardState extends State { ], ), ), - if (settings.showDebugInfo) + if (settings.kShowDebugInfo.value) Positioned( top: 36.0, right: 0.0, @@ -222,7 +222,7 @@ class _TimelineCardState extends State { CameraViewFitButton( fit: _fit ?? device.server.additionalSettings.videoFit ?? - settings.cameraViewFit, + settings.kVideoFit.value, onChanged: (fit) => setState(() => _fit = fit), ), SquaredIconButton( diff --git a/lib/screens/events_timeline/mobile/timeline_device_view.dart b/lib/screens/events_timeline/mobile/timeline_device_view.dart index 82890392..90cec3ab 100644 --- a/lib/screens/events_timeline/mobile/timeline_device_view.dart +++ b/lib/screens/events_timeline/mobile/timeline_device_view.dart @@ -324,8 +324,8 @@ class _TimelineDeviceViewState extends State { heroTag: currentEvent?.videoUrl, player: tile.videoController, fit: device?.server.additionalSettings.videoFit ?? - settings.cameraViewFit, - paneBuilder: !settings.showDebugInfo + settings.kVideoFit.value, + paneBuilder: !settings.kShowDebugInfo.value ? null : (context, controller) { return Padding( @@ -392,7 +392,7 @@ class _TimelineDeviceViewState extends State { child: currentDate == null ? const Text(' ■■■■■ • ■■■■■ ') : Text( - '${settings.dateFormat.format(currentDate!)}' + '${settings.kDateFormat.value.format(currentDate!)}' ' ' '${timelineTimeFormat.format(currentDate!)}', style: theme.textTheme.labelMedium, diff --git a/lib/screens/layouts/desktop/desktop_device_grid.dart b/lib/screens/layouts/desktop/desktop_device_grid.dart index c6f29eef..e0ff9357 100644 --- a/lib/screens/layouts/desktop/desktop_device_grid.dart +++ b/lib/screens/layouts/desktop/desktop_device_grid.dart @@ -305,7 +305,7 @@ class DesktopDeviceTile extends StatefulWidget { class _DesktopDeviceTileState extends State { late UnityVideoFit fit = widget.device.server.additionalSettings.videoFit ?? - SettingsProvider.instance.cameraViewFit; + SettingsProvider.instance.kVideoFit.value; @override Widget build(BuildContext context) { @@ -471,7 +471,7 @@ class _DesktopTileViewportState extends State { final fit = context.findAncestorWidgetOfExactType()?.fit ?? widget.device.server.additionalSettings.videoFit ?? - settings.cameraViewFit; + settings.kVideoFit.value; return Stack(children: [ Positioned.fill(child: MulticastViewport(device: widget.device)), @@ -500,7 +500,7 @@ class _DesktopTileViewportState extends State { shadows: outlinedText(), ), ), - if (states.isHovering && settings.showDebugInfo) + if (states.isHovering && settings.kShowDebugInfo.value) TextSpan( text: '\nsource: ${video?.player.dataSource}' '\nposition: ${video?.player.currentPos}' diff --git a/lib/screens/layouts/desktop/external_stream.dart b/lib/screens/layouts/desktop/external_stream.dart index 18bbc29a..8477ff05 100644 --- a/lib/screens/layouts/desktop/external_stream.dart +++ b/lib/screens/layouts/desktop/external_stream.dart @@ -242,7 +242,8 @@ class _AddExternalStreamDialogState extends State { ), ), ), - if (settings.betaMatrixedZoomEnabled && showMoreOptions) ...[ + if (settings.kDefaultBetaMatrixedZoomEnabled.value && + showMoreOptions) ...[ const SizedBox(height: 16.0), Text(loc.matrixType, style: theme.textTheme.headlineSmall), const SizedBox(height: 6.0), diff --git a/lib/screens/layouts/desktop/layout_manager.dart b/lib/screens/layouts/desktop/layout_manager.dart index 0889471b..ca5bf1c4 100644 --- a/lib/screens/layouts/desktop/layout_manager.dart +++ b/lib/screens/layouts/desktop/layout_manager.dart @@ -62,7 +62,7 @@ class _LayoutManagerState extends State { super.didChangeDependencies(); final settings = context.watch(); timer?.cancel(); - timer = Timer.periodic(settings.layoutCyclingTogglePeriod, (timer) { + timer = Timer.periodic(settings.kLayoutCyclePeriod.value, (timer) { if (!mounted) return; final view = DesktopViewProvider.instance; diff --git a/lib/screens/layouts/desktop/multicast_view.dart b/lib/screens/layouts/desktop/multicast_view.dart index ffd7c46c..b08c6672 100644 --- a/lib/screens/layouts/desktop/multicast_view.dart +++ b/lib/screens/layouts/desktop/multicast_view.dart @@ -97,7 +97,7 @@ class _MulticastViewportState extends State { if (view == null || view.lastImageUpdate == null || - !settings.betaMatrixedZoomEnabled) { + !settings.kDefaultBetaMatrixedZoomEnabled.value) { return const SizedBox.shrink(); } diff --git a/lib/screens/layouts/desktop/stream_data.dart b/lib/screens/layouts/desktop/stream_data.dart index 3826a2d9..68f128d4 100644 --- a/lib/screens/layouts/desktop/stream_data.dart +++ b/lib/screens/layouts/desktop/stream_data.dart @@ -189,7 +189,7 @@ class _StreamDataState extends State { Icon(fit.icon), const SizedBox(width: 8.0), Flexible(child: Text(fit.locale(context), maxLines: 1)), - if (settings.cameraViewFit == fit) ...[ + if (settings.kVideoFit.value == fit) ...[ const SizedBox(width: 2.5), const DefaultValueIcon(), ], @@ -209,7 +209,7 @@ class _StreamDataState extends State { isSelected: StreamingType.values .map( (type) => streamingType == null - ? type == settings.streamingType + ? type == settings.kStreamingType.value : type == streamingType, ) .toList(), @@ -228,7 +228,7 @@ class _StreamDataState extends State { children: [ const SizedBox(width: 12.0), Text(type.name.toUpperCase()), - if (settings.streamingType == type) ...[ + if (settings.kStreamingType.value == type) ...[ const SizedBox(width: 10.0), const DefaultValueIcon(), ], @@ -243,7 +243,7 @@ class _StreamDataState extends State { }, ), ], - if (settings.betaMatrixedZoomEnabled) ...[ + if (settings.kDefaultBetaMatrixedZoomEnabled.value) ...[ const SizedBox(height: 16.0), Text(loc.matrixType, style: theme.textTheme.headlineSmall), const SizedBox(height: 6.0), diff --git a/lib/screens/layouts/mobile/device_view.dart b/lib/screens/layouts/mobile/device_view.dart index a6faeb4d..60e6333d 100644 --- a/lib/screens/layouts/mobile/device_view.dart +++ b/lib/screens/layouts/mobile/device_view.dart @@ -244,7 +244,7 @@ class DeviceTileState extends State { heroTag: widget.device.streamURL, player: videoPlayer, fit: widget.device.server.additionalSettings.videoFit ?? - settings.cameraViewFit, + settings.kVideoFit.value, paneBuilder: (context, controller) { final video = UnityVideoView.of(context); final error = video.error; diff --git a/lib/screens/layouts/mobile/mobile_device_grid.dart b/lib/screens/layouts/mobile/mobile_device_grid.dart index cecb10a4..9bba3c48 100644 --- a/lib/screens/layouts/mobile/mobile_device_grid.dart +++ b/lib/screens/layouts/mobile/mobile_device_grid.dart @@ -40,7 +40,7 @@ class _MobileDeviceGridState extends State { super.didChangeDependencies(); final settings = context.watch(); timer?.cancel(); - timer = Timer.periodic(settings.layoutCyclingTogglePeriod, (timer) { + timer = Timer.periodic(settings.kLayoutCyclePeriod.value, (timer) { final settings = SettingsProvider.instance; final view = MobileViewProvider.instance; diff --git a/lib/screens/layouts/video_status_label.dart b/lib/screens/layouts/video_status_label.dart index 9daeae9a..e8e908f3 100644 --- a/lib/screens/layouts/video_status_label.dart +++ b/lib/screens/layouts/video_status_label.dart @@ -150,7 +150,7 @@ class _VideoStatusLabelState extends State { } final isLateDismissal = status == VideoLabel.late && - settings.lateVideoBehavior == LateVideoBehavior.manual; + settings.kLateStreamBehavior.value == LateVideoBehavior.manual; return MouseRegion( onEnter: _openWithTap || isLateDismissal ? null : (_) => showOverlay(), diff --git a/lib/screens/multi_window/single_camera_window.dart b/lib/screens/multi_window/single_camera_window.dart index c3d63a3f..d0eaa362 100644 --- a/lib/screens/multi_window/single_camera_window.dart +++ b/lib/screens/multi_window/single_camera_window.dart @@ -39,7 +39,7 @@ class _CameraViewState extends State { late final UnityVideoPlayer controller; late final StreamSubscription _durationSubscription; late UnityVideoFit fit = widget.device.server.additionalSettings.videoFit ?? - SettingsProvider.instance.cameraViewFit; + SettingsProvider.instance.kVideoFit.value; @override void initState() { diff --git a/lib/screens/players/event_player_desktop.dart b/lib/screens/players/event_player_desktop.dart index f6d6a54e..c5e785ab 100644 --- a/lib/screens/players/event_player_desktop.dart +++ b/lib/screens/players/event_player_desktop.dart @@ -102,7 +102,7 @@ class _EventPlayerDesktopState extends State { title: title, ); fit = device?.server.additionalSettings.videoFit ?? - SettingsProvider.instance.cameraViewFit; + SettingsProvider.instance.kVideoFit.value; playingSubscription = videoController.onPlayingStateUpdate.listen((isPlaying) { if (!mounted) return; @@ -194,7 +194,7 @@ class _EventPlayerDesktopState extends State { Center( child: ErrorWarning(message: video.error!), ), - if (settings.showDebugInfo) + if (settings.kShowDebugInfo.value) Padding( padding: const EdgeInsets.all(8.0), child: Text( diff --git a/lib/screens/players/event_player_mobile.dart b/lib/screens/players/event_player_mobile.dart index 4fa21c98..3424b576 100644 --- a/lib/screens/players/event_player_mobile.dart +++ b/lib/screens/players/event_player_mobile.dart @@ -122,7 +122,7 @@ class __EventPlayerMobileState extends State<_EventPlayerMobile> { child: UnityVideoView( heroTag: widget.event.mediaURL, player: videoController, - fit: settings.cameraViewFit, + fit: settings.kVideoFit.value, videoBuilder: (context, video) { return InteractiveViewer( minScale: 1.0, @@ -294,7 +294,7 @@ class _VideoViewportState extends State { DownloadIndicator(event: widget.event), ]), ), - if (settings.showDebugInfo) + if (settings.kShowDebugInfo.value) Padding( padding: const EdgeInsets.all(8.0), child: Text( diff --git a/lib/screens/players/live_player.dart b/lib/screens/players/live_player.dart index 2ac7d06a..4561f125 100644 --- a/lib/screens/players/live_player.dart +++ b/lib/screens/players/live_player.dart @@ -133,7 +133,7 @@ class _MobileLivePlayer extends StatefulWidget { class __MobileLivePlayerState extends State<_MobileLivePlayer> { bool overlay = true; late UnityVideoFit fit = widget.device.server.additionalSettings.videoFit ?? - SettingsProvider.instance.cameraViewFit; + SettingsProvider.instance.kVideoFit.value; late bool ptzEnabled = widget.ptzEnabled; @@ -260,7 +260,7 @@ class __MobileLivePlayerState extends State<_MobileLivePlayer> { onPressed: toggleOverlay, ), ), - if (overlay && settings.showDebugInfo) + if (overlay && settings.kShowDebugInfo.value) PositionedDirectional( bottom: 8.0, start: 8.0, @@ -313,7 +313,7 @@ class _DesktopLivePlayer extends StatefulWidget { class __DesktopLivePlayerState extends State<_DesktopLivePlayer> { late UnityVideoFit fit = widget.device.server.additionalSettings.videoFit ?? - SettingsProvider.instance.cameraViewFit; + SettingsProvider.instance.kVideoFit.value; late bool ptzEnabled = widget.ptzEnabled; late StreamSubscription _volumeStreamSubscription; @@ -368,7 +368,7 @@ class __DesktopLivePlayerState extends State<_DesktopLivePlayer> { ), ), ), - if (states.isHovering && settings.showDebugInfo) + if (states.isHovering && settings.kShowDebugInfo.value) Padding( padding: const EdgeInsets.all(8.0), child: Text( diff --git a/lib/screens/servers/additional_server_settings.dart b/lib/screens/servers/additional_server_settings.dart index de72a2ec..d07c22f6 100644 --- a/lib/screens/servers/additional_server_settings.dart +++ b/lib/screens/servers/additional_server_settings.dart @@ -111,7 +111,7 @@ class _AdditionalServerSettingsState extends State { title: loc.streamingType, values: StreamingType.values, value: streamingType, - defaultValue: settings.streamingType, + defaultValue: settings.kStreamingType.value, onChanged: (value) { setState(() { streamingType = value; @@ -122,7 +122,7 @@ class _AdditionalServerSettingsState extends State { title: loc.rtspProtocol, values: RTSPProtocol.values, value: rtspProtocol, - defaultValue: settings.rtspProtocol, + defaultValue: settings.kRTSPProtocol.value, onChanged: (value) { setState(() { rtspProtocol = value; @@ -134,7 +134,7 @@ class _AdditionalServerSettingsState extends State { description: loc.cameraViewFitDescription, values: UnityVideoFit.values, value: videoFit, - defaultValue: settings.cameraViewFit, + defaultValue: settings.kVideoFit.value, onChanged: (value) { setState(() { videoFit = value; @@ -146,7 +146,7 @@ class _AdditionalServerSettingsState extends State { description: loc.renderingQualityDescription, values: RenderingQuality.values, value: renderingQuality, - defaultValue: settings.videoQuality, + defaultValue: settings.kRenderingQuality.value, onChanged: (value) { setState(() { renderingQuality = value; diff --git a/lib/screens/settings/advanced_options.dart b/lib/screens/settings/advanced_options.dart index c196f7c5..83dc7c5e 100644 --- a/lib/screens/settings/advanced_options.dart +++ b/lib/screens/settings/advanced_options.dart @@ -49,10 +49,10 @@ class AdvancedOptionsSettings extends StatelessWidget { contentPadding: DesktopSettings.horizontalPadding, title: Text(loc.matrixedViewZoom), subtitle: Text(loc.matrixedViewZoomDescription), - value: settings.betaMatrixedZoomEnabled, + value: settings.kDefaultBetaMatrixedZoomEnabled.value, onChanged: (value) { if (value != null) { - settings.betaMatrixedZoomEnabled = value; + settings.kDefaultBetaMatrixedZoomEnabled.value = value; } }, ), @@ -114,10 +114,10 @@ class AdvancedOptionsSettings extends StatelessWidget { 'Display useful information for debugging, such as video metadata ' 'and other useful information for debugging purposes.', ), - value: settings.showDebugInfo, + value: settings.kShowDebugInfo.value, onChanged: (v) { if (v != null) { - settings.showDebugInfo = v; + settings.kShowDebugInfo.value = v; } }, dense: false, diff --git a/lib/screens/settings/application.dart b/lib/screens/settings/application.dart index 1c7512ae..7b310820 100644 --- a/lib/screens/settings/application.dart +++ b/lib/screens/settings/application.dart @@ -40,7 +40,7 @@ class ApplicationSettings extends StatelessWidget { OptionsChooserTile( title: 'Theme', icon: Icons.contrast, - value: settings.themeMode, + value: settings.kThemeMode.value, values: ThemeMode.values.map((mode) { return Option( value: mode, @@ -57,7 +57,7 @@ class ApplicationSettings extends StatelessWidget { ); }), onChanged: (v) { - settings.themeMode = v; + settings.kThemeMode.value = v; }, ), const LanguageSection(), @@ -87,9 +87,9 @@ class ApplicationSettings extends StatelessWidget { foregroundColor: theme.iconTheme.color, child: const Icon(Icons.sensor_door), ), - title: const Text('Close app to tray'), + title: const Text('Minimize to tray'), subtitle: const Text( - 'Whether to close the app to the system tray when the window is closed. ' + 'Whether to minimize app to the system tray when the window is closed. ' 'This will keep the app running in the background.', ), ), @@ -163,7 +163,7 @@ class LanguageSection extends StatelessWidget { title: Text(loc.language), trailing: DropdownButton( value: currentLocale, - onChanged: (value) => settings.locale = value!, + onChanged: (value) => settings.kLanguageCode.value = value!, items: locales.map((locale) { final name = names.nameOf(locale.toLanguageTag()) ?? locale.toLanguageTag(); @@ -229,7 +229,7 @@ class DateFormatSection extends StatelessWidget { ); }), onChanged: (v) { - settings.dateFormat = DateFormat(v!, locale); + settings.kDateFormat.value = DateFormat(v!, locale); }, ); } @@ -257,7 +257,7 @@ class TimeFormatSection extends StatelessWidget { ); }), onChanged: (v) { - settings.timeFormat = DateFormat(v!, locale); + settings.kTimeFormat.value = DateFormat(v!, locale); }, ); } diff --git a/lib/screens/settings/general.dart b/lib/screens/settings/general.dart index bdb4116d..a6bd066f 100644 --- a/lib/screens/settings/general.dart +++ b/lib/screens/settings/general.dart @@ -39,7 +39,7 @@ class GeneralSettings extends StatelessWidget { title: loc.cycleTogglePeriod, description: loc.cycleTogglePeriodDescription, icon: Icons.timelapse, - value: settings.layoutCyclingTogglePeriod, + value: settings.kLayoutCyclePeriod.value, values: [5, 10, 30, 60, 60 * 5] .map((seconds) => Duration(seconds: seconds)) .map((duration) { @@ -49,7 +49,7 @@ class GeneralSettings extends StatelessWidget { ); }), onChanged: (value) { - settings.layoutCyclingTogglePeriod = value; + settings.kLayoutCyclePeriod.value = value; }, ), CheckboxListTile.adaptive( @@ -62,9 +62,9 @@ class GeneralSettings extends StatelessWidget { title: Text(loc.wakelock), subtitle: Text(loc.wakelockDescription), isThreeLine: true, - value: settings.wakelockEnabled, + value: settings.kWakelock.value, onChanged: (value) { - settings.wakelockEnabled = !settings.wakelockEnabled; + settings.kWakelock.value = !settings.kWakelock.value; }, ), const SubHeader( @@ -91,20 +91,25 @@ class GeneralSettings extends StatelessWidget { ), title: Text(loc.snoozeNotifications), subtitle: Text( - settings.snoozedUntil.isAfter(DateTime.now()) + settings.kSnoozeNotificationsUntil.value.isAfter(DateTime.now()) ? loc.snoozedUntil( [ - if (settings.snoozedUntil.difference(DateTime.now()) > + if (settings.kSnoozeNotificationsUntil.value + .difference(DateTime.now()) > const Duration(hours: 24)) - settings.formatDate(settings.snoozedUntil), - settings.formatTime(settings.snoozedUntil), + settings + .formatDate(settings.kSnoozeNotificationsUntil.value), + settings + .formatTime(settings.kSnoozeNotificationsUntil.value), ].join(' '), ) : loc.notSnoozed, ), onTap: () async { - if (settings.snoozedUntil.isAfter(DateTime.now())) { - settings.snoozedUntil = SettingsProvider.defaultSnoozedUntil; + if (settings.kSnoozeNotificationsUntil.value + .isAfter(DateTime.now())) { + settings.kSnoozeNotificationsUntil.value = + SettingsProvider.instance.kSnoozeNotificationsUntil.def; } else { final timeOfDay = await showTimePicker( context: context, @@ -113,7 +118,7 @@ class GeneralSettings extends StatelessWidget { useRootNavigator: false, ); if (timeOfDay != null) { - settings.snoozedUntil = DateTime( + settings.kSnoozeNotificationsUntil.value = DateTime( DateTime.now().year, DateTime.now().month, DateTime.now().day, @@ -128,7 +133,7 @@ class GeneralSettings extends StatelessWidget { title: loc.notificationClickBehavior, description: loc.notificationClickBehaviorDescription, icon: Icons.beenhere_rounded, - value: settings.notificationClickBehavior, + value: settings.kNotificationClickBehavior.value, values: NotificationClickBehavior.values .map((behavior) => Option( value: behavior, @@ -137,7 +142,7 @@ class GeneralSettings extends StatelessWidget { )) .toList(), onChanged: (v) { - settings.notificationClickBehavior = v; + settings.kNotificationClickBehavior.value = v; }, ), const SubHeader( diff --git a/lib/screens/settings/server_and_devices.dart b/lib/screens/settings/server_and_devices.dart index 11583bfe..ac8db8f8 100644 --- a/lib/screens/settings/server_and_devices.dart +++ b/lib/screens/settings/server_and_devices.dart @@ -44,9 +44,6 @@ class ServerSettings extends StatelessWidget { const SizedBox(height: 8.0), const StreamingSettings(), const SizedBox(height: 12.0), - const SubHeader('Devices Settings'), - const SizedBox(height: 8.0), - const CamerasSettings(), ]); } } @@ -56,6 +53,7 @@ class StreamingSettings extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = Theme.of(context); final settings = context.watch(); final loc = AppLocalizations.of(context); @@ -63,7 +61,7 @@ class StreamingSettings extends StatelessWidget { OptionsChooserTile( title: loc.streamingType, icon: Icons.sensors, - value: settings.streamingType, + value: settings.kStreamingType.value, values: StreamingType.values.map((value) { return Option( value: value, @@ -73,44 +71,33 @@ class StreamingSettings extends StatelessWidget { ); }), onChanged: (v) { - settings.streamingType = v; + settings.kStreamingType.value = v; }, ), const SizedBox(height: 8.0), OptionsChooserTile( title: loc.rtspProtocol, icon: Icons.sensors, - value: settings.rtspProtocol, + value: settings.kRTSPProtocol.value, values: RTSPProtocol.values.map((value) { return Option( value: value, text: value.name.toUpperCase(), ); }), - onChanged: !kIsWeb && settings.streamingType == StreamingType.rtsp - ? (v) { - settings.rtspProtocol = v; - } - : null, + onChanged: + !kIsWeb && settings.kStreamingType.value == StreamingType.rtsp + ? (v) { + settings.kRTSPProtocol.value = v; + } + : null, ), - ]); - } -} - -class CamerasSettings extends StatelessWidget { - const CamerasSettings({super.key}); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final settings = context.watch(); - final loc = AppLocalizations.of(context); - return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!kIsWeb) OptionsChooserTile( title: loc.renderingQuality, + description: loc.renderingQualityDescription, icon: Icons.hd, - value: settings.videoQuality, + value: settings.kRenderingQuality.value, values: RenderingQuality.values.map((value) { return Option( value: value, @@ -118,7 +105,7 @@ class CamerasSettings extends StatelessWidget { ); }), onChanged: (v) { - settings.videoQuality = v; + settings.kRenderingQuality.value = v; }, ), const SizedBox(height: 8.0), @@ -126,7 +113,7 @@ class CamerasSettings extends StatelessWidget { title: loc.cameraViewFit, description: loc.cameraViewFitDescription, icon: Icons.fit_screen, - value: settings.cameraViewFit, + value: settings.kVideoFit.value, values: UnityVideoFit.values.map((value) { return Option( value: value, @@ -134,7 +121,7 @@ class CamerasSettings extends StatelessWidget { ); }), onChanged: (v) { - settings.cameraViewFit = v; + settings.kVideoFit.value = v; }, ), const SizedBox(height: 8.0), @@ -166,7 +153,7 @@ class CamerasSettings extends StatelessWidget { style: theme.textTheme.bodyMedium, children: [ const TextSpan(text: '\n'), - switch (settings.lateVideoBehavior) { + switch (settings.kLateStreamBehavior.value) { LateVideoBehavior.automatic => TextSpan( text: loc.automaticBehaviorDescription, style: const TextStyle(fontWeight: FontWeight.w600), @@ -210,7 +197,7 @@ class CamerasSettings extends StatelessWidget { ), ), icon: Icons.watch_later, - value: settings.lateVideoBehavior, + value: settings.kLateStreamBehavior.value, values: LateVideoBehavior.values.map((value) { return Option( value: value, @@ -218,7 +205,7 @@ class CamerasSettings extends StatelessWidget { ); }), onChanged: (v) { - settings.lateVideoBehavior = v; + settings.kLateStreamBehavior.value = v; }, ), const SizedBox(height: 8.0), @@ -241,9 +228,9 @@ class CamerasSettings extends StatelessWidget { foregroundColor: theme.iconTheme.color, child: const Icon(Icons.memory), ), - title: const Text('Hardware rendering'), + title: const Text('Hardware decoding'), subtitle: const Text( - 'Use hardware rendering when available. This will improve the ' + 'Use hardware decoding when available. This improves the ' 'performance of the video streams and reduce the CPU usage. ' 'If not supported, it will fall back to software rendering. ', ), diff --git a/lib/screens/settings/shared/update.dart b/lib/screens/settings/shared/update.dart index b9988554..0c872ccb 100644 --- a/lib/screens/settings/shared/update.dart +++ b/lib/screens/settings/shared/update.dart @@ -324,7 +324,8 @@ class AppUpdateOptions extends StatelessWidget { TextSpan(text: version.version), const TextSpan(text: ' '), TextSpan( - text: SettingsProvider.instance.dateFormat.format( + text: SettingsProvider.instance.kDateFormat.value + .format( DateFormat('EEE, d MMM yyyy', 'en_US') .parse(version.publishedAt), ), diff --git a/lib/screens/settings/updates_and_help.dart b/lib/screens/settings/updates_and_help.dart index 7e37363b..73602189 100644 --- a/lib/screens/settings/updates_and_help.dart +++ b/lib/screens/settings/updates_and_help.dart @@ -89,10 +89,10 @@ class BetaFeatures extends StatelessWidget { ), title: Text(loc.matrixedViewZoom), subtitle: Text(loc.matrixedViewZoomDescription), - value: settings.betaMatrixedZoomEnabled, + value: settings.kDefaultBetaMatrixedZoomEnabled.value, onChanged: (value) { if (value != null) { - settings.betaMatrixedZoomEnabled = value; + settings.kDefaultBetaMatrixedZoomEnabled.value = value; } }, ), @@ -136,10 +136,10 @@ class BetaFeatures extends StatelessWidget { secondary: const Icon(Icons.adb), title: const Text('Show debug info'), subtitle: const Text('Display useful information for debugging'), - value: settings.showDebugInfo, + value: settings.kShowDebugInfo.value, onChanged: (v) { if (v != null) { - settings.showDebugInfo = v; + settings.kShowDebugInfo.value = v; } }, dense: false, diff --git a/lib/utils/video_player.dart b/lib/utils/video_player.dart index b0930322..7c7f1930 100644 --- a/lib/utils/video_player.dart +++ b/lib/utils/video_player.dart @@ -47,9 +47,9 @@ class UnityPlayers with ChangeNotifier { static void createTimer() { UnityPlayers._reloadTimer?.cancel(); UnityPlayers._reloadTimer = null; - if (SettingsProvider.instance.cameraRefreshPeriod > Duration.zero) { + if (SettingsProvider.instance.kRefreshRate.value > Duration.zero) { UnityPlayers._reloadTimer = Timer.periodic( - SettingsProvider.instance.cameraRefreshPeriod, + SettingsProvider.instance.kRefreshRate.value, (_) => reloadDevices(), ); } @@ -84,7 +84,7 @@ class UnityPlayers with ChangeNotifier { } else { var streamingType = device.preferredStreamingType ?? device.server.additionalSettings.preferredStreamingType ?? - settings.streamingType; + settings.kStreamingType.value; if (kIsWeb && streamingType == StreamingType.rtsp) { streamingType = StreamingType.hls; } @@ -108,7 +108,7 @@ class UnityPlayers with ChangeNotifier { controller = UnityVideoPlayer.create( quality: switch (device.server.additionalSettings.renderingQuality ?? - settings.videoQuality) { + settings.kRenderingQuality.value) { RenderingQuality.p4k => UnityVideoQuality.p4k, RenderingQuality.p1080 => UnityVideoQuality.p1080, RenderingQuality.p720 => UnityVideoQuality.p720, diff --git a/lib/utils/window.dart b/lib/utils/window.dart index 5c44109c..a00f21e4 100644 --- a/lib/utils/window.dart +++ b/lib/utils/window.dart @@ -92,7 +92,7 @@ extension DeviceWindowExtension on Device { final window = await MultiWindow.run([ '${MultiWindowType.device.index}', json.encode(toJson()), - '${SettingsProvider.instance.themeMode.index}', + '${SettingsProvider.instance.kThemeMode.value.index}', ]); debugPrint('Opened window with id ${window.windowId}'); @@ -110,7 +110,7 @@ extension LayoutWindowExtension on Layout { final window = await MultiWindow.run([ '${MultiWindowType.layout.index}', json.encode(toMap()), - '${SettingsProvider.instance.themeMode.index}', + '${SettingsProvider.instance.kThemeMode.value.index}', ]); debugPrint('Opened window with id ${window.windowId}'); From a3914983ce01406f0cecd715f3eca72bf365e8f2 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Wed, 28 Feb 2024 16:46:14 -0300 Subject: [PATCH 16/26] fix: Downloads and Layouts settings --- lib/providers/downloads_provider.dart | 2 +- lib/providers/settings_provider.dart | 35 +++++++++++++++++++ .../layouts/desktop/desktop_device_grid.dart | 2 +- .../layouts/desktop/layout_manager.dart | 4 +-- .../layouts/mobile/mobile_device_grid.dart | 4 +-- .../settings/events_and_downloads.dart | 7 ++-- 6 files changed, 45 insertions(+), 9 deletions(-) diff --git a/lib/providers/downloads_provider.dart b/lib/providers/downloads_provider.dart index 0ed76482..378aa0b1 100644 --- a/lib/providers/downloads_provider.dart +++ b/lib/providers/downloads_provider.dart @@ -225,7 +225,7 @@ class DownloadsManager extends UnityProvider { downloading[event] = 0.0; notifyListeners(); - final dir = SettingsProvider.instance.downloadsDirectory; + final dir = SettingsProvider.instance.kDownloadsDirectory.value; final fileName = 'event_${event.id}_${event.deviceID}_${event.server.name}.mp4'; final downloadPath = path.join(dir, fileName); diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 7619d57d..0b84e26b 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -18,6 +18,7 @@ */ import 'package:bluecherry_client/providers/app_provider_interface.dart'; +import 'package:bluecherry_client/providers/downloads_provider.dart'; import 'package:bluecherry_client/screens/layouts/desktop/external_stream.dart'; import 'package:bluecherry_client/utils/storage.dart'; import 'package:flutter/foundation.dart'; @@ -61,6 +62,8 @@ class SettingsOption { this.saveAs = (value) => (value as DateFormat).pattern ?? ''; } else if (T == Locale) { this.saveAs = (value) => (value as Locale).toLanguageTag(); + } else if (T == DateTime) { + this.saveAs = (value) => (value as DateTime).toIso8601String(); } else { this.saveAs = (value) => value.toString(); } @@ -77,6 +80,8 @@ class SettingsOption { this.loadFrom = (value) => DateFormat(value) as T; } else if (T == Locale) { this.loadFrom = (value) => Locale.fromSubtags(languageCode: value) as T; + } else if (T == DateTime) { + this.loadFrom = (value) => DateTime.parse(value) as T; } else { this.loadFrom = (value) => value as T; } @@ -94,6 +99,10 @@ class SettingsProvider extends UnityProvider { def: const Duration(seconds: 5), key: 'general.cycle_period', ); + final kLayoutCycleEnabled = SettingsOption( + def: true, + key: 'general.cycle_enabled', + ); final kWakelock = SettingsOption( def: true, key: 'general.wakelock', @@ -112,6 +121,7 @@ class SettingsProvider extends UnityProvider { def: NotificationClickBehavior.showEventsScreen, key: 'notifications.click_behavior', loadFrom: (value) => NotificationClickBehavior.values[int.parse(value)], + saveAs: (value) => value.index.toString(), ); // Data usage @@ -119,11 +129,13 @@ class SettingsProvider extends UnityProvider { def: NetworkUsage.wifiOnly, key: 'data_usage.automatic_streaming', loadFrom: (value) => NetworkUsage.values[int.parse(value)], + saveAs: (value) => value.index.toString(), ); final kStreamOnBackground = SettingsOption( def: NetworkUsage.wifiOnly, key: 'data_usage.stream_on_background', loadFrom: (value) => NetworkUsage.values[int.parse(value)], + saveAs: (value) => value.index.toString(), ); // Streaming settings @@ -131,21 +143,25 @@ class SettingsProvider extends UnityProvider { def: kIsWeb ? StreamingType.hls : StreamingType.rtsp, key: 'streaming.type', loadFrom: (value) => StreamingType.values[int.parse(value)], + saveAs: (value) => value.index.toString(), ); final kRTSPProtocol = SettingsOption( def: RTSPProtocol.tcp, key: 'streaming.rtsp_protocol', loadFrom: (value) => RTSPProtocol.values[int.parse(value)], + saveAs: (value) => value.index.toString(), ); final kRenderingQuality = SettingsOption( def: RenderingQuality.automatic, key: 'streaming.rendering_quality', loadFrom: (value) => RenderingQuality.values[int.parse(value)], + saveAs: (value) => value.index.toString(), ); final kVideoFit = SettingsOption( def: UnityVideoFit.contain, key: 'streaming.video_fit', loadFrom: (value) => UnityVideoFit.values[int.parse(value)], + saveAs: (value) => value.index.toString(), ); final kRefreshRate = SettingsOption( def: const Duration(minutes: 5), @@ -155,6 +171,7 @@ class SettingsProvider extends UnityProvider { def: LateVideoBehavior.automatic, key: 'streaming.late_video_behavior', loadFrom: (value) => LateVideoBehavior.values[int.parse(value)], + saveAs: (value) => value.index.toString(), ); final kReloadTimedOutStreams = SettingsOption( def: true, @@ -210,6 +227,7 @@ class SettingsProvider extends UnityProvider { def: TimelineIntialPoint.beggining, key: 'timeline.initial_point', loadFrom: (value) => TimelineIntialPoint.values[int.parse(value)], + saveAs: (value) => value.index.toString(), ); // Application @@ -217,6 +235,7 @@ class SettingsProvider extends UnityProvider { def: ThemeMode.system, key: 'application.theme_mode', loadFrom: (value) => ThemeMode.values[int.parse(value)], + saveAs: (value) => value.index.toString(), ); final kLanguageCode = SettingsOption( def: Locale.fromSubtags(languageCode: Intl.getCurrentLocale()), @@ -284,6 +303,7 @@ class SettingsProvider extends UnityProvider { def: MatrixType.t16, key: 'other.matrix_size', loadFrom: (value) => MatrixType.values[int.parse(value)], + saveAs: (value) => value.index.toString(), ); final kShowDebugInfo = SettingsOption( def: false, @@ -304,11 +324,15 @@ class SettingsProvider extends UnityProvider { @override Future initialize() async { + // await settings.delete(); final data = await tryReadStorage(() => settings.read()); kLayoutCyclePeriod.value = kLayoutCyclePeriod.loadFrom( data[kLayoutCyclePeriod.key] ?? kLayoutCyclePeriod.defAsString, ); + kLayoutCycleEnabled.value = kLayoutCycleEnabled.loadFrom( + data[kLayoutCycleEnabled.key] ?? kLayoutCycleEnabled.defAsString, + ); kWakelock.value = kWakelock.loadFrom( data[kWakelock.key] ?? kWakelock.defAsString, ); @@ -360,6 +384,10 @@ class SettingsProvider extends UnityProvider { data[kChooseLocationEveryTime.key] ?? kChooseLocationEveryTime.defAsString, ); + kDownloadsDirectory.value = kDownloadsDirectory.loadFrom( + data[kDownloadsDirectory.key] ?? + (await DownloadsManager.kDefaultDownloadsDirectory).path, + ); kAllowAppCloseWhenDownloading.value = kAllowAppCloseWhenDownloading.loadFrom( data[kAllowAppCloseWhenDownloading.key] ?? @@ -446,6 +474,8 @@ class SettingsProvider extends UnityProvider { await settings.write({ kLayoutCyclePeriod.key: kLayoutCyclePeriod.saveAs(kLayoutCyclePeriod.value), + kLayoutCycleEnabled.key: + kLayoutCycleEnabled.saveAs(kLayoutCycleEnabled.value), kWakelock.key: kWakelock.saveAs(kWakelock.value), kNotificationsEnabled.key: kNotificationsEnabled.saveAs(kNotificationsEnabled.value), @@ -510,6 +540,11 @@ class SettingsProvider extends UnityProvider { return kTimeFormat.value.format(time); } + + void toggleCycling() { + kLayoutCycleEnabled.value = !kLayoutCycleEnabled.value; + save(); + } } enum NotificationClickBehavior { diff --git a/lib/screens/layouts/desktop/desktop_device_grid.dart b/lib/screens/layouts/desktop/desktop_device_grid.dart index e0ff9357..1e0e283e 100644 --- a/lib/screens/layouts/desktop/desktop_device_grid.dart +++ b/lib/screens/layouts/desktop/desktop_device_grid.dart @@ -70,7 +70,7 @@ class _DesktopDeviceGridState extends State { icon: Icon( Icons.cyclone, size: 20.0, - color: settings.layoutCyclingEnabled + color: settings.kLayoutCycleEnabled.value ? theme.colorScheme.primary : IconTheme.of(context).color, ), diff --git a/lib/screens/layouts/desktop/layout_manager.dart b/lib/screens/layouts/desktop/layout_manager.dart index ca5bf1c4..b2f6e039 100644 --- a/lib/screens/layouts/desktop/layout_manager.dart +++ b/lib/screens/layouts/desktop/layout_manager.dart @@ -67,7 +67,7 @@ class _LayoutManagerState extends State { final view = DesktopViewProvider.instance; - if (settings.layoutCyclingEnabled) { + if (settings.kLayoutCycleEnabled.value) { final currentIsLast = view.currentLayoutIndex == view.layouts.length - 1; @@ -123,7 +123,7 @@ class _LayoutManagerState extends State { icon: Icon( Icons.cyclone, size: 18.0, - color: settings.layoutCyclingEnabled + color: settings.kLayoutCycleEnabled.value ? theme.colorScheme.primary : IconTheme.of(context).color, ), diff --git a/lib/screens/layouts/mobile/mobile_device_grid.dart b/lib/screens/layouts/mobile/mobile_device_grid.dart index 9bba3c48..20424fd6 100644 --- a/lib/screens/layouts/mobile/mobile_device_grid.dart +++ b/lib/screens/layouts/mobile/mobile_device_grid.dart @@ -44,7 +44,7 @@ class _MobileDeviceGridState extends State { final settings = SettingsProvider.instance; final view = MobileViewProvider.instance; - if (settings.layoutCyclingEnabled) { + if (settings.kLayoutCycleEnabled.value) { if (view.tab == view.devices.keys.last) { view.setTab(view.devices.keys.first); } else { @@ -119,7 +119,7 @@ class _MobileDeviceGridState extends State { icon: Icon( Icons.cyclone, size: 18.0, - color: settings.layoutCyclingEnabled + color: settings.kLayoutCycleEnabled.value ? theme.colorScheme.primary : theme.colorScheme.onBackground, ), diff --git a/lib/screens/settings/events_and_downloads.dart b/lib/screens/settings/events_and_downloads.dart index 5de3d6b8..b7786d75 100644 --- a/lib/screens/settings/events_and_downloads.dart +++ b/lib/screens/settings/events_and_downloads.dart @@ -57,17 +57,18 @@ class EventsAndDownloadsSettings extends StatelessWidget { child: const Icon(Icons.notifications_paused), ), title: Text(loc.downloadPath), - subtitle: Text(settings.downloadsDirectory), + subtitle: Text(settings.kDownloadsDirectory.value), trailing: const Icon(Icons.navigate_next), onTap: () async { final selectedDirectory = await FilePicker.platform.getDirectoryPath( dialogTitle: loc.downloadPath, - initialDirectory: settings.downloadsDirectory, + initialDirectory: settings.kDownloadsDirectory.value, lockParentWindow: true, ); if (selectedDirectory != null) { - settings.downloadsDirectory = Directory(selectedDirectory).path; + settings.kDownloadsDirectory.value = + Directory(selectedDirectory).path; } }, ), From 7afe8ccc107924dadb2314c7fbda279a82778c4b Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Wed, 28 Feb 2024 16:54:59 -0300 Subject: [PATCH 17/26] fix: Correctly assign the default downloads directory --- lib/providers/downloads_provider.dart | 38 +++++++++++++++------------ lib/providers/settings_provider.dart | 3 ++- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/lib/providers/downloads_provider.dart b/lib/providers/downloads_provider.dart index 378aa0b1..5c87cac8 100644 --- a/lib/providers/downloads_provider.dart +++ b/lib/providers/downloads_provider.dart @@ -32,14 +32,6 @@ import 'package:flutter/foundation.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; -import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; -import 'package:path_provider_windows/path_provider_windows.dart' - hide WindowsKnownFolder; -// ignore: implementation_imports -import 'package:path_provider_windows/src/folders_stub.dart' - if (dart.library.ffi) 'package:path_provider_windows/src/folders.dart' - show WindowsKnownFolder; - class DownloadedEvent { final Event event; final String downloadPath; @@ -105,20 +97,32 @@ class DownloadsManager extends UnityProvider { static Future get kDefaultDownloadsDirectory async { Directory? dir; - if (PathProviderPlatform.instance is PathProviderWindows) { - final instance = PathProviderPlatform.instance as PathProviderWindows; - final videosPath = - // ignore: unnecessary_cast - await instance.getPath((WindowsKnownFolder as dynamic).Videos) - as String; - dir = Directory(path.join(videosPath, 'Bluecherry Client', 'Downloads')); - } + try { + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + final dirs = await getExternalStorageDirectories( + type: StorageDirectory.downloads); + if (dirs?.isNotEmpty ?? false) dir = dirs!.first; + } - if (dir == null) { + if (dir == null) { + final downloadsDir = await getDownloadsDirectory(); + if (downloadsDir != null) { + dir = Directory(path.join(downloadsDir.path, 'Bluecherry Client')); + } + } + + if (dir == null) { + final docsDir = await getApplicationSupportDirectory(); + dir = Directory(path.join(docsDir.path, 'downloads')); + } + } catch (error, stack) { + debugPrint('Failed to get default downloads directory: $error\n$stack'); final docsDir = await getApplicationSupportDirectory(); dir = Directory(path.join(docsDir.path, 'downloads')); } + debugPrint('The default downloads is ${dir.path}'); + return dir.create(recursive: true); } diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 0b84e26b..59abac7b 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -82,6 +82,8 @@ class SettingsOption { this.loadFrom = (value) => Locale.fromSubtags(languageCode: value) as T; } else if (T == DateTime) { this.loadFrom = (value) => DateTime.parse(value) as T; + } else if (T == double) { + this.loadFrom = (value) => double.parse(value) as T; } else { this.loadFrom = (value) => value as T; } @@ -324,7 +326,6 @@ class SettingsProvider extends UnityProvider { @override Future initialize() async { - // await settings.delete(); final data = await tryReadStorage(() => settings.read()); kLayoutCyclePeriod.value = kLayoutCyclePeriod.loadFrom( From 8d4bd14ac8e4930c0728667701a6c317f61b51fc Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Wed, 28 Feb 2024 17:08:19 -0300 Subject: [PATCH 18/26] feat: restore the default settings --- lib/providers/settings_provider.dart | 11 +++++++++ lib/screens/settings/advanced_options.dart | 27 +++++++++++++++++----- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 59abac7b..b4a27679 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -326,6 +326,7 @@ class SettingsProvider extends UnityProvider { @override Future initialize() async { + await settings.delete(); final data = await tryReadStorage(() => settings.read()); kLayoutCyclePeriod.value = kLayoutCyclePeriod.loadFrom( @@ -524,6 +525,11 @@ class SettingsProvider extends UnityProvider { super.save(notifyListeners: notifyListeners); } + void updateProperty(VoidCallback update) { + update(); + save(); + } + /// Formats the date according to the current [dateFormat]. /// /// [toLocal] defines if the date will be converted to local time. Defaults to `true` @@ -546,6 +552,11 @@ class SettingsProvider extends UnityProvider { kLayoutCycleEnabled.value = !kLayoutCycleEnabled.value; save(); } + + Future restoreDefaults() async { + await settings.delete(); + await initialize(); + } } enum NotificationClickBehavior { diff --git a/lib/screens/settings/advanced_options.dart b/lib/screens/settings/advanced_options.dart index 83dc7c5e..fe040055 100644 --- a/lib/screens/settings/advanced_options.dart +++ b/lib/screens/settings/advanced_options.dart @@ -52,7 +52,9 @@ class AdvancedOptionsSettings extends StatelessWidget { value: settings.kDefaultBetaMatrixedZoomEnabled.value, onChanged: (value) { if (value != null) { - settings.kDefaultBetaMatrixedZoomEnabled.value = value; + settings.updateProperty(() { + settings.kDefaultBetaMatrixedZoomEnabled.value = value; + }); } }, ), @@ -66,7 +68,11 @@ class AdvancedOptionsSettings extends StatelessWidget { text: size.toString(), ); }), - onChanged: (v) {}, + onChanged: (v) { + settings.updateProperty(() { + settings.kMatrixSize.value = v; + }); + }, ), const SubHeader('Developer Options'), if (!kIsWeb) ...[ @@ -117,7 +123,9 @@ class AdvancedOptionsSettings extends StatelessWidget { value: settings.kShowDebugInfo.value, onChanged: (v) { if (v != null) { - settings.kShowDebugInfo.value = v; + settings.updateProperty(() { + settings.kShowDebugInfo.value = v; + }); } }, dense: false, @@ -129,8 +137,14 @@ class AdvancedOptionsSettings extends StatelessWidget { subtitle: const Text( 'Display network usage information over playing videos.', ), - value: false, - onChanged: (v) {}, + value: settings.kShowNetworkUsage.value, + onChanged: (v) { + if (v != null) { + settings.updateProperty(() { + settings.kShowNetworkUsage.value = v; + }); + } + }, dense: false, ), ListTile( @@ -142,7 +156,8 @@ class AdvancedOptionsSettings extends StatelessWidget { 'affect your servers or any other data.', ), trailing: const Icon(Icons.navigate_next), - onTap: () {}, + // TODO(bdlukaa): Show an "Are you sure?" dialog + onTap: settings.restoreDefaults, ), ], ]); From 3cedbbb6df30946e835dff31fd51ec93229469df Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Thu, 29 Feb 2024 09:34:03 -0300 Subject: [PATCH 19/26] fix: Do not throw error if hls errored --- lib/models/device.dart | 24 +-- lib/providers/desktop_view_provider.dart | 1 + lib/providers/downloads_provider.dart | 1 + lib/providers/events_playback_provider.dart | 1 + lib/providers/mobile_view_provider.dart | 1 + lib/providers/server_provider.dart | 1 + lib/providers/settings_provider.dart | 186 ++++++-------------- lib/providers/update_provider.dart | 1 + 8 files changed, 69 insertions(+), 147 deletions(-) diff --git a/lib/models/device.dart b/lib/models/device.dart index c4c510f4..9d555fd4 100644 --- a/lib/models/device.dart +++ b/lib/models/device.dart @@ -265,17 +265,21 @@ class Device { queryParameters: data, ); - var response = await API.client.get(uri); - - if (response.statusCode == 200) { - var ret = json.decode(response.body) as Map; - - if (ret['status'] == 6) { - var hlsLink = ret['msg'][0]; - return Uri.encodeFull(hlsLink); + try { + var response = await API.client.get(uri); + + if (response.statusCode == 200) { + var ret = json.decode(response.body) as Map; + + if (ret['status'] == 6) { + var hlsLink = ret['msg'][0]; + return Uri.encodeFull(hlsLink); + } + } else { + debugPrint('Request failed with status: ${response.statusCode}'); } - } else { - debugPrint('Request failed with status: ${response.statusCode}'); + } catch (error, stacktrace) { + debugPrint('Failed to get HLS url($uri): $error, $stacktrace'); } return hlsURL; diff --git a/lib/providers/desktop_view_provider.dart b/lib/providers/desktop_view_provider.dart index 7b8af42c..b41de7d0 100644 --- a/lib/providers/desktop_view_provider.dart +++ b/lib/providers/desktop_view_provider.dart @@ -37,6 +37,7 @@ class DesktopViewProvider extends UnityProvider { static Future ensureInitialized() async { instance = DesktopViewProvider._(); await instance.initialize(); + debugPrint('DesktopViewProvider initialized'); return instance; } diff --git a/lib/providers/downloads_provider.dart b/lib/providers/downloads_provider.dart index 5c87cac8..63d3c7dd 100644 --- a/lib/providers/downloads_provider.dart +++ b/lib/providers/downloads_provider.dart @@ -92,6 +92,7 @@ class DownloadsManager extends UnityProvider { static Future ensureInitialized() async { instance = DownloadsManager._(); await instance.initialize(); + debugPrint('DownloadsManager initialized'); return instance; } diff --git a/lib/providers/events_playback_provider.dart b/lib/providers/events_playback_provider.dart index 30619242..a4a70563 100644 --- a/lib/providers/events_playback_provider.dart +++ b/lib/providers/events_playback_provider.dart @@ -32,6 +32,7 @@ class EventsProvider extends UnityProvider { static Future ensureInitialized() async { instance = EventsProvider._(); await instance.initialize(); + debugPrint('EventsProvider initialized'); return instance; } diff --git a/lib/providers/mobile_view_provider.dart b/lib/providers/mobile_view_provider.dart index 3201edb7..050ad7a6 100644 --- a/lib/providers/mobile_view_provider.dart +++ b/lib/providers/mobile_view_provider.dart @@ -33,6 +33,7 @@ class MobileViewProvider extends UnityProvider { static Future ensureInitialized() async { instance = MobileViewProvider._(); await instance.initialize(); + debugPrint('MobileViewProvider initialized'); return instance; } diff --git a/lib/providers/server_provider.dart b/lib/providers/server_provider.dart index 5a8f551f..9860df0c 100644 --- a/lib/providers/server_provider.dart +++ b/lib/providers/server_provider.dart @@ -38,6 +38,7 @@ class ServersProvider extends UnityProvider { static Future ensureInitialized() async { instance = ServersProvider._(); await instance.initialize(); + debugPrint('ServersProvider initialized'); return instance; } diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index b4a27679..c5fc2fd2 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -90,6 +90,10 @@ class SettingsOption { } String get defAsString => saveAs(def); + + void loadData(Map data) { + value = loadFrom(data[key] ?? defAsString); + } } class SettingsProvider extends UnityProvider { @@ -321,153 +325,61 @@ class SettingsProvider extends UnityProvider { static Future ensureInitialized() async { instance = SettingsProvider._(); await instance.initialize(); + debugPrint('SettingsProvider initialized'); return instance; } @override Future initialize() async { - await settings.delete(); + // await settings.delete(); final data = await tryReadStorage(() => settings.read()); - kLayoutCyclePeriod.value = kLayoutCyclePeriod.loadFrom( - data[kLayoutCyclePeriod.key] ?? kLayoutCyclePeriod.defAsString, - ); - kLayoutCycleEnabled.value = kLayoutCycleEnabled.loadFrom( - data[kLayoutCycleEnabled.key] ?? kLayoutCycleEnabled.defAsString, - ); - kWakelock.value = kWakelock.loadFrom( - data[kWakelock.key] ?? kWakelock.defAsString, - ); - kNotificationsEnabled.value = kNotificationsEnabled.loadFrom( - data[kNotificationsEnabled.key] ?? kNotificationsEnabled.defAsString, - ); - kSnoozeNotificationsUntil.value = kSnoozeNotificationsUntil.loadFrom( - data[kSnoozeNotificationsUntil.key] ?? - kSnoozeNotificationsUntil.defAsString, - ); - kNotificationClickBehavior.value = kNotificationClickBehavior.loadFrom( - data[kNotificationClickBehavior.key] ?? - kNotificationClickBehavior.defAsString, - ); - kAutomaticStreaming.value = kAutomaticStreaming.loadFrom( - data[kAutomaticStreaming.key] ?? kAutomaticStreaming.defAsString, - ); - kStreamOnBackground.value = kStreamOnBackground.loadFrom( - data[kStreamOnBackground.key] ?? kStreamOnBackground.defAsString, - ); - kStreamingType.value = kStreamingType.loadFrom( - data[kStreamingType.key] ?? kStreamingType.defAsString, - ); - kRTSPProtocol.value = kRTSPProtocol.loadFrom( - data[kRTSPProtocol.key] ?? kRTSPProtocol.defAsString, - ); - kRenderingQuality.value = kRenderingQuality.loadFrom( - data[kRenderingQuality.key] ?? kRenderingQuality.defAsString, - ); - kVideoFit.value = kVideoFit.loadFrom( - data[kVideoFit.key] ?? kVideoFit.defAsString, - ); - kRefreshRate.value = kRefreshRate.loadFrom( - data[kRefreshRate.key] ?? kRefreshRate.defAsString, - ); - kLateStreamBehavior.value = kLateStreamBehavior.loadFrom( - data[kLateStreamBehavior.key] ?? kLateStreamBehavior.defAsString, - ); - kReloadTimedOutStreams.value = kReloadTimedOutStreams.loadFrom( - data[kReloadTimedOutStreams.key] ?? kReloadTimedOutStreams.defAsString, - ); - kUseHardwareDecoding.value = kUseHardwareDecoding.loadFrom( - data[kUseHardwareDecoding.key] ?? kUseHardwareDecoding.defAsString, - ); - kDownloadOnMobileData.value = kDownloadOnMobileData.loadFrom( - data[kDownloadOnMobileData.key] ?? kDownloadOnMobileData.defAsString, - ); - kChooseLocationEveryTime.value = kChooseLocationEveryTime.loadFrom( - data[kChooseLocationEveryTime.key] ?? - kChooseLocationEveryTime.defAsString, - ); - kDownloadsDirectory.value = kDownloadsDirectory.loadFrom( + kLayoutCyclePeriod.loadData(data); + kLayoutCycleEnabled.loadData(data); + kWakelock.loadData(data); + kNotificationsEnabled.loadData(data); + kSnoozeNotificationsUntil.loadData(data); + kNotificationClickBehavior.loadData(data); + kAutomaticStreaming.loadData(data); + kStreamOnBackground.loadData(data); + kStreamingType.loadData(data); + kRTSPProtocol.loadData(data); + kRenderingQuality.loadData(data); + kVideoFit.loadData(data); + kRefreshRate.loadData(data); + kLateStreamBehavior.loadData(data); + kReloadTimedOutStreams.loadData(data); + kUseHardwareDecoding.loadData(data); + kDownloadOnMobileData.loadData(data); + kChooseLocationEveryTime.loadData(data); + kDownloadsDirectory.loadFrom( data[kDownloadsDirectory.key] ?? (await DownloadsManager.kDefaultDownloadsDirectory).path, ); - kAllowAppCloseWhenDownloading.value = - kAllowAppCloseWhenDownloading.loadFrom( - data[kAllowAppCloseWhenDownloading.key] ?? - kAllowAppCloseWhenDownloading.defAsString, - ); - kPictureInPicture.value = kPictureInPicture.loadFrom( - data[kPictureInPicture.key] ?? kPictureInPicture.defAsString, - ); - kEventsSpeed.value = kEventsSpeed.loadFrom( - data[kEventsSpeed.key] ?? kEventsSpeed.defAsString, - ); - kEventsVolume.value = kEventsVolume.loadFrom( - data[kEventsVolume.key] ?? kEventsVolume.defAsString, - ); - kShowDifferentColorsForEvents.value = - kShowDifferentColorsForEvents.loadFrom( - data[kShowDifferentColorsForEvents.key] ?? - kShowDifferentColorsForEvents.defAsString, - ); - kPauseToBuffer.value = kPauseToBuffer.loadFrom( - data[kPauseToBuffer.key] ?? kPauseToBuffer.defAsString, - ); - kTimelineInitialPoint.value = kTimelineInitialPoint.loadFrom( - data[kTimelineInitialPoint.key] ?? kTimelineInitialPoint.defAsString, - ); - kThemeMode.value = kThemeMode.loadFrom( - data[kThemeMode.key] ?? kThemeMode.defAsString, - ); - kLanguageCode.value = kLanguageCode.loadFrom( - data[kLanguageCode.key] ?? kLanguageCode.defAsString, - ); - kDateFormat.value = kDateFormat.loadFrom( - data[kDateFormat.key] ?? kDateFormat.defAsString, - ); - kTimeFormat.value = kTimeFormat.loadFrom( - data[kTimeFormat.key] ?? kTimeFormat.defAsString, - ); - kLaunchAppOnStartup.value = kLaunchAppOnStartup.loadFrom( - data[kLaunchAppOnStartup.key] ?? kLaunchAppOnStartup.defAsString, - ); - kMinimizeToTray.value = kMinimizeToTray.loadFrom( - data[kMinimizeToTray.key] ?? kMinimizeToTray.defAsString, - ); - kAnimationsEnabled.value = kAnimationsEnabled.loadFrom( - data[kAnimationsEnabled.key] ?? kAnimationsEnabled.defAsString, - ); - kHighContrast.value = kHighContrast.loadFrom( - data[kHighContrast.key] ?? kHighContrast.defAsString, - ); - kLargeFont.value = kLargeFont.loadFrom( - data[kLargeFont.key] ?? kLargeFont.defAsString, - ); - kAllowDataCollection.value = kAllowDataCollection.loadFrom( - data[kAllowDataCollection.key] ?? kAllowDataCollection.defAsString, - ); - kAllowCrashReports.value = kAllowCrashReports.loadFrom( - data[kAllowCrashReports.key] ?? kAllowCrashReports.defAsString, - ); - kAutoUpdate.value = kAutoUpdate.loadFrom( - data[kAutoUpdate.key] ?? kAutoUpdate.defAsString, - ); - kShowReleaseNotes.value = kShowReleaseNotes.loadFrom( - data[kShowReleaseNotes.key] ?? kShowReleaseNotes.defAsString, - ); - kDefaultBetaMatrixedZoomEnabled.value = - kDefaultBetaMatrixedZoomEnabled.loadFrom( - data[kDefaultBetaMatrixedZoomEnabled.key] ?? - kDefaultBetaMatrixedZoomEnabled.defAsString, - ); - kMatrixSize.value = kMatrixSize.loadFrom( - data[kMatrixSize.key] ?? kMatrixSize.defAsString, - ); - kShowDebugInfo.value = kShowDebugInfo.loadFrom( - data[kShowDebugInfo.key] ?? kShowDebugInfo.defAsString, - ); - kShowNetworkUsage.value = kShowNetworkUsage.loadFrom( - data[kShowNetworkUsage.key] ?? kShowNetworkUsage.defAsString, - ); + kAllowAppCloseWhenDownloading.loadData(data); + kPictureInPicture.loadData(data); + kEventsSpeed.loadData(data); + kEventsVolume.loadData(data); + kShowDifferentColorsForEvents.loadData(data); + kPauseToBuffer.loadData(data); + kTimelineInitialPoint.loadData(data); + kThemeMode.loadData(data); + kLanguageCode.loadData(data); + kDateFormat.loadData(data); + kTimeFormat.loadData(data); + kLaunchAppOnStartup.loadData(data); + kMinimizeToTray.loadData(data); + kAnimationsEnabled.loadData(data); + kHighContrast.loadData(data); + kLargeFont.loadData(data); + kAllowDataCollection.loadData(data); + kAllowCrashReports.loadData(data); + kAutoUpdate.loadData(data); + kShowReleaseNotes.loadData(data); + kDefaultBetaMatrixedZoomEnabled.loadData(data); + kMatrixSize.loadData(data); + kShowDebugInfo.loadData(data); + kShowNetworkUsage.loadData(data); } @override diff --git a/lib/providers/update_provider.dart b/lib/providers/update_provider.dart index 860eb52c..80fbc5f1 100644 --- a/lib/providers/update_provider.dart +++ b/lib/providers/update_provider.dart @@ -113,6 +113,7 @@ class UpdateManager extends UnityProvider { static Future ensureInitialized() async { instance = UpdateManager._(); await instance.initialize(); + debugPrint('UpdateManager initialized'); return instance; } From d931d6a7505297cc5a67aec7501bd3d95ddc7e7f Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Sat, 2 Mar 2024 11:44:33 -0300 Subject: [PATCH 20/26] fix: Analysis issues --- lib/screens/events_timeline/desktop/timeline.dart | 6 +++--- lib/screens/layouts/desktop/layout_manager.dart | 2 +- lib/screens/layouts/mobile/mobile_device_grid.dart | 8 ++++---- lib/screens/settings/settings_desktop.dart | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/screens/events_timeline/desktop/timeline.dart b/lib/screens/events_timeline/desktop/timeline.dart index ca193ee0..66ad52bf 100644 --- a/lib/screens/events_timeline/desktop/timeline.dart +++ b/lib/screens/events_timeline/desktop/timeline.dart @@ -810,13 +810,13 @@ class _TimelineEventsViewState extends State { child: Container( width: 8, height: 4, - // color: theme.colorScheme.onBackground, + // color: theme.colorScheme.onSurface, color: Colors.black, ), ), Expanded( child: Container( - // color: theme.colorScheme.onBackground, + // color: theme.colorScheme.onSurface, width: 1.8, color: Colors.black, ), @@ -1051,7 +1051,7 @@ class _TimelineHours extends StatelessWidget { child: Container( height: 6.5, width: 2, - color: theme.colorScheme.onBackground, + color: theme.colorScheme.onSurface, ), ), ); diff --git a/lib/screens/layouts/desktop/layout_manager.dart b/lib/screens/layouts/desktop/layout_manager.dart index b2f6e039..60df1534 100644 --- a/lib/screens/layouts/desktop/layout_manager.dart +++ b/lib/screens/layouts/desktop/layout_manager.dart @@ -488,7 +488,7 @@ class _NewLayoutDialogState extends State { content: Text( message, style: TextStyle( - color: Theme.of(context).colorScheme.onBackground, + color: Theme.of(context).colorScheme.onSurface, ), ), actions: [ diff --git a/lib/screens/layouts/mobile/mobile_device_grid.dart b/lib/screens/layouts/mobile/mobile_device_grid.dart index 20424fd6..2923f5f2 100644 --- a/lib/screens/layouts/mobile/mobile_device_grid.dart +++ b/lib/screens/layouts/mobile/mobile_device_grid.dart @@ -90,7 +90,7 @@ class _MobileDeviceGridState extends State { return FadeThroughTransition( animation: primaryAnimation, secondaryAnimation: secondaryAnimation, - fillColor: theme.colorScheme.background, + fillColor: theme.colorScheme.surface, child: child, ); }, @@ -111,7 +111,7 @@ class _MobileDeviceGridState extends State { width: double.infinity, child: Row(children: [ UnityDrawerButton( - iconColor: theme.colorScheme.onBackground, + iconColor: theme.colorScheme.onSurface, iconSize: 18.0, splashRadius: 24.0, ), @@ -121,7 +121,7 @@ class _MobileDeviceGridState extends State { size: 18.0, color: settings.kLayoutCycleEnabled.value ? theme.colorScheme.primary - : theme.colorScheme.onBackground, + : theme.colorScheme.onSurface, ), tooltip: loc.cycle, onPressed: settings.toggleCycling, @@ -144,7 +144,7 @@ class _MobileDeviceGridState extends State { style: TextStyle( color: view.tab == tab ? theme.colorScheme.onPrimary - : theme.colorScheme.onBackground, + : theme.colorScheme.onSurface, fontSize: 18.0, ), ), diff --git a/lib/screens/settings/settings_desktop.dart b/lib/screens/settings/settings_desktop.dart index 9b5977be..0d8d2ed0 100644 --- a/lib/screens/settings/settings_desktop.dart +++ b/lib/screens/settings/settings_desktop.dart @@ -100,7 +100,7 @@ class _DesktopSettingsState extends State { data: theme.copyWith( cardTheme: CardTheme( color: ElevationOverlay.applySurfaceTint( - theme.colorScheme.background, + theme.colorScheme.surface, theme.colorScheme.surfaceTint, 4, ), From d601312eb6ddf606d7cfae3a4ab6248b95da7ade Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Sat, 2 Mar 2024 11:52:54 -0300 Subject: [PATCH 21/26] feat: Allow async default values --- lib/providers/settings_provider.dart | 99 +++++++++++++++------------- 1 file changed, 53 insertions(+), 46 deletions(-) diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index c5fc2fd2..4449ca34 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -36,6 +36,7 @@ enum EnabledPreference { on, ask, never } class SettingsOption { final String key; final T def; + final Future Function()? getDefault; late final String Function(T value) saveAs; late final T Function(String value) loadFrom; @@ -45,54 +46,62 @@ class SettingsOption { SettingsOption({ required this.key, required this.def, + this.getDefault, String Function(T value)? saveAs, T Function(String value)? loadFrom, }) { - value = def; - - if (saveAs != null) { - this.saveAs = saveAs; - } else if (T == bool) { - this.saveAs = (value) => value.toString(); - } else if (T == Duration) { - this.saveAs = (value) => (value as Duration).inMilliseconds.toString(); - } else if (T == Enum) { - this.saveAs = (value) => (value as Enum).index.toString(); - } else if (T == DateFormat) { - this.saveAs = (value) => (value as DateFormat).pattern ?? ''; - } else if (T == Locale) { - this.saveAs = (value) => (value as Locale).toLanguageTag(); - } else if (T == DateTime) { - this.saveAs = (value) => (value as DateTime).toIso8601String(); - } else { - this.saveAs = (value) => value.toString(); - } - - if (loadFrom != null) { - this.loadFrom = loadFrom; - } else if (T == bool) { - this.loadFrom = (value) => (bool.tryParse(value) ?? def) as T; - } else if (T == Duration) { - this.loadFrom = (value) => Duration(milliseconds: int.parse(value)) as T; - } else if (T == Enum) { - throw UnsupportedError('Enum type must provide a loadFrom function'); - } else if (T == DateFormat) { - this.loadFrom = (value) => DateFormat(value) as T; - } else if (T == Locale) { - this.loadFrom = (value) => Locale.fromSubtags(languageCode: value) as T; - } else if (T == DateTime) { - this.loadFrom = (value) => DateTime.parse(value) as T; - } else if (T == double) { - this.loadFrom = (value) => double.parse(value) as T; - } else { - this.loadFrom = (value) => value as T; - } + Future.microtask(() async { + value = getDefault != null ? await getDefault!() : def; + + if (saveAs != null) { + this.saveAs = saveAs; + } else if (T == bool) { + this.saveAs = (value) => value.toString(); + } else if (T == Duration) { + this.saveAs = (value) => (value as Duration).inMilliseconds.toString(); + } else if (T == Enum) { + this.saveAs = (value) => (value as Enum).index.toString(); + } else if (T == DateFormat) { + this.saveAs = (value) => (value as DateFormat).pattern ?? ''; + } else if (T == Locale) { + this.saveAs = (value) => (value as Locale).toLanguageTag(); + } else if (T == DateTime) { + this.saveAs = (value) => (value as DateTime).toIso8601String(); + } else { + this.saveAs = (value) => value.toString(); + } + + if (loadFrom != null) { + this.loadFrom = loadFrom; + } else if (T == bool) { + this.loadFrom = (value) => (bool.tryParse(value) ?? def) as T; + } else if (T == Duration) { + this.loadFrom = + (value) => Duration(milliseconds: int.parse(value)) as T; + } else if (T == Enum) { + throw UnsupportedError('Enum type must provide a loadFrom function'); + } else if (T == DateFormat) { + this.loadFrom = (value) => DateFormat(value) as T; + } else if (T == Locale) { + this.loadFrom = (value) => Locale.fromSubtags(languageCode: value) as T; + } else if (T == DateTime) { + this.loadFrom = (value) => DateTime.parse(value) as T; + } else if (T == double) { + this.loadFrom = (value) => double.parse(value) as T; + } else { + this.loadFrom = (value) => value as T; + } + }); } String get defAsString => saveAs(def); - void loadData(Map data) { - value = loadFrom(data[key] ?? defAsString); + Future loadData(Map data) async { + String? serializedData = data[key]; + if (getDefault != null) serializedData ??= saveAs(await getDefault!()); + serializedData ??= defAsString; + + value = loadFrom(serializedData); } } @@ -203,6 +212,8 @@ class SettingsProvider extends UnityProvider { ); final kDownloadsDirectory = SettingsOption( def: '', + getDefault: () async => + (await DownloadsManager.kDefaultDownloadsDirectory).path, key: 'downloads.directory', ); @@ -331,7 +342,6 @@ class SettingsProvider extends UnityProvider { @override Future initialize() async { - // await settings.delete(); final data = await tryReadStorage(() => settings.read()); kLayoutCyclePeriod.loadData(data); @@ -352,10 +362,7 @@ class SettingsProvider extends UnityProvider { kUseHardwareDecoding.loadData(data); kDownloadOnMobileData.loadData(data); kChooseLocationEveryTime.loadData(data); - kDownloadsDirectory.loadFrom( - data[kDownloadsDirectory.key] ?? - (await DownloadsManager.kDefaultDownloadsDirectory).path, - ); + kDownloadsDirectory.loadData(data); kAllowAppCloseWhenDownloading.loadData(data); kPictureInPicture.loadData(data); kEventsSpeed.loadData(data); From b234d88a73535369be4247a3bf176dd6c7a113f2 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Sat, 2 Mar 2024 11:58:37 -0300 Subject: [PATCH 22/26] fix: Remove migration from old storage system; --- ...firebase_messaging_background_handler.dart | 6 +- lib/providers/desktop_view_provider.dart | 10 ++-- lib/providers/downloads_provider.dart | 8 +-- lib/providers/events_playback_provider.dart | 6 +- lib/providers/mobile_view_provider.dart | 10 ++-- lib/providers/server_provider.dart | 15 ++--- lib/providers/update_provider.dart | 12 ++-- lib/utils/constants.dart | 41 ++++--------- lib/utils/storage.dart | 59 ------------------- pubspec.lock | 16 ----- pubspec.yaml | 5 -- 11 files changed, 45 insertions(+), 143 deletions(-) diff --git a/lib/firebase_messaging_background_handler.dart b/lib/firebase_messaging_background_handler.dart index 93ad36cf..50e5d8a6 100644 --- a/lib/firebase_messaging_background_handler.dart +++ b/lib/firebase_messaging_background_handler.dart @@ -434,7 +434,7 @@ abstract class FirebaseConfiguration { FirebaseMessaging.instance.onTokenRefresh.listen( (token) async { debugPrint('[FirebaseMessaging.instance.onTokenRefresh]: $token'); - storage.add({kHiveNotificationToken: token}); + storage.add({kStorageNotificationToken: token}); for (final server in ServersProvider.instance.servers) { API.instance.registerNotificationToken( await API.instance.checkServerCredentials(server), @@ -450,10 +450,10 @@ abstract class FirebaseConfiguration { if (token != null) { final data = await tryReadStorage(() => storage.read()); // Do not proceed, if token is already saved. - if (data[kHiveNotificationToken] == token) { + if (data[kStorageNotificationToken] == token) { return; } - await storage.add({kHiveNotificationToken: token}); + await storage.add({kStorageNotificationToken: token}); for (final server in ServersProvider.instance.servers) { API.instance.registerNotificationToken( await API.instance.checkServerCredentials(server), diff --git a/lib/providers/desktop_view_provider.dart b/lib/providers/desktop_view_provider.dart index b41de7d0..94e0b71c 100644 --- a/lib/providers/desktop_view_provider.dart +++ b/lib/providers/desktop_view_provider.dart @@ -61,7 +61,7 @@ class DesktopViewProvider extends UnityProvider { @override Future initialize() async { await tryReadStorage( - () => initializeStorage(desktopView, kHiveDesktopLayouts), + () => initializeStorage(desktopView, kStorageDesktopLayouts), ); await Future.wait( currentLayout.devices.map((device) { @@ -86,9 +86,9 @@ class DesktopViewProvider extends UnityProvider { Future save({bool notifyListeners = true}) async { try { await desktopView.write({ - kHiveDesktopLayouts: + kStorageDesktopLayouts: jsonEncode(layouts.map((layout) => layout.toMap()).toList()), - kHiveDesktopCurrentLayout: _currentLayout, + kStorageDesktopCurrentLayout: _currentLayout, }); } catch (error, stackTrace) { debugPrint('Failed to save desktop view:\n $error\n$stackTrace'); @@ -104,14 +104,14 @@ class DesktopViewProvider extends UnityProvider { layouts = ((await compute( jsonDecode, - data[kHiveDesktopLayouts] as String, + data[kStorageDesktopLayouts] as String, ) ?? []) as List) .cast() .map((item) { return Layout.fromMap(item.cast()); }).toList(); - _currentLayout = data[kHiveDesktopCurrentLayout] ?? 0; + _currentLayout = data[kStorageDesktopCurrentLayout] ?? 0; super.restore(notifyListeners: notifyListeners); } diff --git a/lib/providers/downloads_provider.dart b/lib/providers/downloads_provider.dart index 63d3c7dd..899b4322 100644 --- a/lib/providers/downloads_provider.dart +++ b/lib/providers/downloads_provider.dart @@ -160,14 +160,14 @@ class DownloadsManager extends UnityProvider { }); await tryReadStorage( - () => super.initializeStorage(downloads, kHiveDownloads)); + () => super.initializeStorage(downloads, kStorageDownloads)); } @override Future save({bool notifyListeners = true}) async { try { await downloads.write({ - kHiveDownloads: + kStorageDownloads: jsonEncode(downloadedEvents.map((de) => de.toJson()).toList()), }); } catch (e) { @@ -181,9 +181,9 @@ class DownloadsManager extends UnityProvider { Future restore({bool notifyListeners = true}) async { final data = await tryReadStorage(() => downloads.read()); - downloadedEvents = data[kHiveDownloads] == null + downloadedEvents = data[kStorageDownloads] == null ? [] - : ((await compute(jsonDecode, data[kHiveDownloads] as String) ?? []) + : ((await compute(jsonDecode, data[kStorageDownloads] as String) ?? []) as List) .cast() .map((item) { diff --git a/lib/providers/events_playback_provider.dart b/lib/providers/events_playback_provider.dart index a4a70563..76f19684 100644 --- a/lib/providers/events_playback_provider.dart +++ b/lib/providers/events_playback_provider.dart @@ -39,7 +39,7 @@ class EventsProvider extends UnityProvider { @override Future initialize() { return tryReadStorage( - () => super.initializeStorage(eventsPlayback, kHiveEventsPlayback)); + () => super.initializeStorage(eventsPlayback, kStorageEventsPlayback)); } /// The list of the device ids that are currently selected @@ -51,7 +51,7 @@ class EventsProvider extends UnityProvider { Future save({bool notifyListeners = true}) async { try { await eventsPlayback.write({ - kHiveEventsPlayback: jsonEncode(selectedIds), + kStorageEventsPlayback: jsonEncode(selectedIds), }); } catch (e) { debugPrint(e.toString()); @@ -66,7 +66,7 @@ class EventsProvider extends UnityProvider { final data = await tryReadStorage(() => eventsPlayback.read()); selectedIds = - (jsonDecode(data[kHiveEventsPlayback]) as List).cast(); + (jsonDecode(data[kStorageEventsPlayback]) as List).cast(); super.restore(notifyListeners: notifyListeners); } diff --git a/lib/providers/mobile_view_provider.dart b/lib/providers/mobile_view_provider.dart index 050ad7a6..50f4827e 100644 --- a/lib/providers/mobile_view_provider.dart +++ b/lib/providers/mobile_view_provider.dart @@ -74,7 +74,7 @@ class MobileViewProvider extends UnityProvider { @override Future initialize() async { await tryReadStorage( - () => super.initializeStorage(mobileView, kHiveMobileView)); + () => super.initializeStorage(mobileView, kStorageMobileView)); for (final device in current) { if (device != null) { UnityPlayers.players[device.uuid] ??= UnityPlayers.forDevice(device); @@ -191,8 +191,8 @@ class MobileViewProvider extends UnityProvider { ); try { await mobileView.write({ - kHiveMobileView: jsonEncode(data), - kHiveMobileViewTab: tab, + kStorageMobileView: jsonEncode(data), + kStorageMobileViewTab: tab, }); } catch (e) { debugPrint(e.toString()); @@ -206,7 +206,7 @@ class MobileViewProvider extends UnityProvider { Future restore({bool notifyListeners = true}) async { final data = await tryReadStorage(() => mobileView.read()); devices = - ((await compute(jsonDecode, data[kHiveMobileView] as String)) as Map) + ((await compute(jsonDecode, data[kStorageMobileView] as String)) as Map) .map( (key, value) => MapEntry>( int.parse(key), @@ -226,7 +226,7 @@ class MobileViewProvider extends UnityProvider { }); } - tab = data[kHiveMobileViewTab]!; + tab = data[kStorageMobileViewTab]!; super.restore(notifyListeners: notifyListeners); } } diff --git a/lib/providers/server_provider.dart b/lib/providers/server_provider.dart index 9860df0c..5e9d542d 100644 --- a/lib/providers/server_provider.dart +++ b/lib/providers/server_provider.dart @@ -56,7 +56,7 @@ class ServersProvider extends UnityProvider { @override Future initialize() async { await tryReadStorage( - () => super.initializeStorage(serversStorage, kHiveServers)); + () => super.initializeStorage(serversStorage, kStorageServers)); refreshDevices(startup: true); } @@ -73,8 +73,9 @@ class ServersProvider extends UnityProvider { // Register notification token. try { final data = await tryReadStorage(() => serversStorage.read()); - final notificationToken = data[kHiveNotificationToken]; - assert(notificationToken != null, '[kHiveNotificationToken] is null.'); + final notificationToken = data[kStorageNotificationToken]; + assert( + notificationToken != null, '[kStorageNotificationToken] is null.'); await API.instance .registerNotificationToken(server, notificationToken!); } catch (exception, stacktrace) { @@ -184,7 +185,7 @@ class ServersProvider extends UnityProvider { Future save({bool notifyListeners = true}) async { try { await serversStorage.write({ - kHiveServers: servers.map((e) => e.toJson()).toList(), + kStorageServers: servers.map((e) => e.toJson()).toList(), }); } catch (e) { debugPrint(e.toString()); @@ -197,9 +198,9 @@ class ServersProvider extends UnityProvider { Future restore({bool notifyListeners = true}) async { final data = await tryReadStorage(() => serversStorage.read()); - final serversData = data[kHiveServers] is String - ? await compute(jsonDecode, data[kHiveServers] as String) - : data[kHiveServers] as List; + final serversData = data[kStorageServers] is String + ? await compute(jsonDecode, data[kStorageServers] as String) + : data[kStorageServers] as List; servers = serversData .cast>() .map(Server.fromJson) diff --git a/lib/providers/update_provider.dart b/lib/providers/update_provider.dart index 80fbc5f1..5707aa9f 100644 --- a/lib/providers/update_provider.dart +++ b/lib/providers/update_provider.dart @@ -155,7 +155,7 @@ class UpdateManager extends UnityProvider { return _setPackageInfo(); } await tryReadStorage( - () => super.initializeStorage(updates, kHiveAutomaticUpdates)); + () => super.initializeStorage(updates, kStorageAutomaticUpdates)); tempDir = (await getTemporaryDirectory()).path; @@ -173,8 +173,8 @@ class UpdateManager extends UnityProvider { Future save({bool notifyListeners = true}) async { try { await updates.write({ - kHiveAutomaticUpdates: automaticDownloads, - kHiveLastCheck: lastCheck?.toIso8601String(), + kStorageAutomaticUpdates: automaticDownloads, + kStorageLastCheck: lastCheck?.toIso8601String(), }); } catch (e) { debugPrint(e.toString()); @@ -187,10 +187,10 @@ class UpdateManager extends UnityProvider { Future restore({bool notifyListeners = true}) async { final data = await updates.read() as Map; - _automaticDownloads = data[kHiveAutomaticUpdates]; - _lastCheck = data[kHiveLastCheck] == null + _automaticDownloads = data[kStorageAutomaticUpdates]; + _lastCheck = data[kStorageLastCheck] == null ? null - : DateTime.tryParse(data[kHiveLastCheck]!); + : DateTime.tryParse(data[kStorageLastCheck]!); super.restore(notifyListeners: notifyListeners); } diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index f511ab22..2fc23606 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -33,36 +33,17 @@ const kDefaultUsername = 'Admin'; /// Default password used in Bluecherry DVR server. const kDefaultPassword = 'bluecherry'; -// Keys used for storing data in cache using `package:hive`. - -const kHiveServers = 'servers'; -const kHiveMobileView = 'mobile_view'; -const kHiveMobileViewTab = 'mobile_view_current_tab'; -const kHiveDesktopLayouts = 'desktop_view_layouts'; -const kHiveDesktopCurrentLayout = 'desktop_view_current_layout'; -const kHiveNotificationToken = 'notification_token'; -const kHiveLocale = 'locale'; -const kHiveThemeMode = 'theme_mode'; -const kHiveDateFormat = 'date_format'; -const kHiveTimeFormat = 'time_format'; -const kHiveSnoozedUntil = 'snoozed_until'; -const kHiveNotificationClickBehavior = 'notification_click_action'; -const kHiveCameraViewFit = 'camera_view_fit'; -const kHiveDownloadsDirectorySetting = 'downloads_dir'; -const kHiveDownloads = 'downloads'; -const kHiveEventsPlayback = 'events_playback'; -const kHiveLayoutCycling = 'layout_cycling'; -const kHiveLayoutCyclingPeriod = 'layout_cycling_period'; -const kHiveCameraRefreshPeriod = 'camera_refresh_period'; -const kHiveAutomaticUpdates = 'automatic_download_updates'; -const kHiveLastCheck = 'last_update_check'; -const kHiveStreamingType = 'streaming_type'; -const kHiveStreamingProtocol = 'streaming_protocol'; -const kHiveVideoQuality = 'video_quality'; -const kHiveWakelockEnabled = 'wakelock_enabled'; -const kHiveBetaMatrixedZoom = 'matrized_zoom'; -const kHiveShowDebugInfo = 'show_debug_info'; -const kHiveLateVideoBehavior = 'late_video_behavior'; +// Keys used for storing data. +const kStorageServers = 'servers'; +const kStorageMobileView = 'mobile_view'; +const kStorageMobileViewTab = 'mobile_view_current_tab'; +const kStorageDesktopLayouts = 'desktop_view_layouts'; +const kStorageDesktopCurrentLayout = 'desktop_view_current_layout'; +const kStorageNotificationToken = 'notification_token'; +const kStorageDownloads = 'downloads'; +const kStorageEventsPlayback = 'events_playback'; +const kStorageAutomaticUpdates = 'automatic_download_updates'; +const kStorageLastCheck = 'last_update_check'; /// Used as frame buffer size in [DeviceTile], and calculating aspect ratio. Only relevant on desktop. const kDeviceTileWidth = 640.0; diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index 7dde4fac..d20c39bb 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -19,10 +19,7 @@ import 'dart:io'; -import 'package:bluecherry_client/providers/update_provider.dart'; -import 'package:bluecherry_client/utils/constants.dart'; import 'package:flutter/foundation.dart'; -import 'package:hive_flutter/hive_flutter.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; import 'package:safe_local_storage/safe_local_storage.dart'; @@ -42,56 +39,6 @@ Future configureStorage() async { mobileView = SafeLocalStorage(path.join(dir, 'mobileView.json')); desktopView = SafeLocalStorage(path.join(dir, 'desktopView.json')); updates = SafeLocalStorage(path.join(dir, 'updates.json')); - - if (!UpdateManager.isEmbedded) { - // Migrate from hive to new storage system - // TODO(bdlukaa): remove this legacy code when stable is released - await Hive.initFlutter(dir); - if (await Hive.boxExists('hive')) { - final hive = await Hive.openBox('hive'); - if (hive.isEmpty) { - hive.close(); - } else { - await Future.wait([ - storage._replaceIfNotNull( - kHiveNotificationToken, hive.get(kHiveNotificationToken)), - desktopView._replaceIfNotNull( - kHiveDesktopLayouts, hive.get(kHiveDesktopLayouts)), - desktopView._replaceIfNotNull( - kHiveDesktopCurrentLayout, hive.get(kHiveDesktopCurrentLayout)), - downloads._replaceIfNotNull(kHiveDownloads, hive.get(kHiveDownloads)), - eventsPlayback._replaceIfNotNull( - kHiveEventsPlayback, hive.get(kHiveEventsPlayback)), - mobileView._replaceIfNotNull( - kHiveMobileView, hive.get(kHiveMobileView)), - mobileView._replaceIfNotNull( - kHiveMobileViewTab, hive.get(kHiveMobileViewTab)), - serversStorage._replaceIfNotNull( - kHiveServers, hive.get(kHiveServers)), - settings._replaceIfNotNull(kHiveThemeMode, hive.get(kHiveThemeMode)), - settings._replaceIfNotNull( - kHiveDateFormat, hive.get(kHiveDateFormat)), - settings._replaceIfNotNull( - kHiveTimeFormat, hive.get(kHiveTimeFormat)), - settings._replaceIfNotNull( - kHiveSnoozedUntil, hive.get(kHiveSnoozedUntil)), - settings._replaceIfNotNull( - kHiveNotificationClickBehavior, - hive.get(kHiveNotificationClickBehavior), - ), - settings._replaceIfNotNull( - kHiveCameraViewFit, - hive.get(kHiveCameraViewFit), - ), - settings._replaceIfNotNull( - kHiveDownloadsDirectorySetting, - hive.get(kHiveDownloadsDirectorySetting), - ), - ]); - await Hive.deleteBoxFromDisk('hive'); - } - } - } } late final SafeLocalStorage storage; @@ -110,12 +57,6 @@ extension SafeLocalStorageExtension on SafeLocalStorage { ...data, }); } - - Future _replaceIfNotNull(String key, dynamic value) { - if (value != null) return add({key: value}); - - return Future.value(); - } } enum LogType { video, network } diff --git a/pubspec.lock b/pubspec.lock index a42f6a62..c52be4c2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -397,22 +397,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" - hive: - dependency: "direct main" - description: - name: hive - sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" - url: "https://pub.dev" - source: hosted - version: "2.2.3" - hive_flutter: - dependency: "direct main" - description: - name: hive_flutter - sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc - url: "https://pub.dev" - source: hosted - version: "1.1.0" html: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7fa8b96a..37297a25 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -51,11 +51,6 @@ dependencies: file_picker: ^6.1.1 safe_local_storage: ^1.0.0 - # Hive is just used in terms of migration - # TODO: remove this in future - hive: ^2.2.3 - hive_flutter: ^1.1.0 - permission_handler: ^11.1.0 uuid: ^4.3.3 From 3b353933d109ad8e94e6dc6ece315194da3898d5 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Sun, 3 Mar 2024 13:00:43 -0300 Subject: [PATCH 23/26] feat: Do not show options that do not have any effect yet --- lib/screens/settings/advanced_options.dart | 66 ++++---- lib/screens/settings/application.dart | 139 ++++++++-------- .../settings/events_and_downloads.dart | 153 +++++++++--------- lib/screens/settings/general.dart | 79 ++++----- lib/screens/settings/server_and_devices.dart | 111 +++++++------ lib/screens/settings/shared/update.dart | 28 ++-- lib/screens/settings/updates_and_help.dart | 88 ---------- 7 files changed, 299 insertions(+), 365 deletions(-) diff --git a/lib/screens/settings/advanced_options.dart b/lib/screens/settings/advanced_options.dart index fe040055..113983da 100644 --- a/lib/screens/settings/advanced_options.dart +++ b/lib/screens/settings/advanced_options.dart @@ -58,22 +58,23 @@ class AdvancedOptionsSettings extends StatelessWidget { } }, ), - OptionsChooserTile( - title: 'Default Matrix Size', - icon: Icons.view_quilt, - value: MatrixType.t4, - values: MatrixType.values.map((size) { - return Option( - value: size, - text: size.toString(), - ); - }), - onChanged: (v) { - settings.updateProperty(() { - settings.kMatrixSize.value = v; - }); - }, - ), + if (kDebugMode) + OptionsChooserTile( + title: 'Default Matrix Size', + icon: Icons.view_quilt, + value: MatrixType.t4, + values: MatrixType.values.map((size) { + return Option( + value: size, + text: size.toString(), + ); + }), + onChanged: (v) { + settings.updateProperty(() { + settings.kMatrixSize.value = v; + }); + }, + ), const SubHeader('Developer Options'), if (!kIsWeb) ...[ FutureBuilder( @@ -130,23 +131,24 @@ class AdvancedOptionsSettings extends StatelessWidget { }, dense: false, ), - CheckboxListTile( - contentPadding: DesktopSettings.horizontalPadding, - secondary: const Icon(Icons.network_check), - title: const Text('Network Usage'), - subtitle: const Text( - 'Display network usage information over playing videos.', + if (kDebugMode) + CheckboxListTile( + contentPadding: DesktopSettings.horizontalPadding, + secondary: const Icon(Icons.network_check), + title: const Text('Network Usage'), + subtitle: const Text( + 'Display network usage information over playing videos.', + ), + value: settings.kShowNetworkUsage.value, + onChanged: (v) { + if (v != null) { + settings.updateProperty(() { + settings.kShowNetworkUsage.value = v; + }); + } + }, + dense: false, ), - value: settings.kShowNetworkUsage.value, - onChanged: (v) { - if (v != null) { - settings.updateProperty(() { - settings.kShowNetworkUsage.value = v; - }); - } - }, - dense: false, - ), ListTile( contentPadding: DesktopSettings.horizontalPadding, leading: const Icon(Icons.restore), diff --git a/lib/screens/settings/application.dart b/lib/screens/settings/application.dart index 7b310820..63b2249a 100644 --- a/lib/screens/settings/application.dart +++ b/lib/screens/settings/application.dart @@ -22,6 +22,7 @@ import 'package:bluecherry_client/screens/settings/settings_desktop.dart'; import 'package:bluecherry_client/screens/settings/shared/options_chooser_tile.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/widgets/misc.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'; @@ -63,79 +64,81 @@ class ApplicationSettings extends StatelessWidget { const LanguageSection(), const DateFormatSection(), const TimeFormatSection(), - const SubHeader('Window'), - CheckboxListTile.adaptive( - value: false, - onChanged: (v) {}, - contentPadding: DesktopSettings.horizontalPadding, - secondary: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.launch), - ), - title: const Text('Launch app on startup'), - subtitle: const Text( - 'Whether to launchthe app when the system starts', - ), - ), - CheckboxListTile.adaptive( - value: false, - onChanged: (v) {}, - contentPadding: DesktopSettings.horizontalPadding, - secondary: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.sensor_door), - ), - title: const Text('Minimize to tray'), - subtitle: const Text( - 'Whether to minimize app to the system tray when the window is closed. ' - 'This will keep the app running in the background.', - ), - ), - const SubHeader('Acessibility'), - CheckboxListTile.adaptive( - value: true, - onChanged: (v) {}, - contentPadding: DesktopSettings.horizontalPadding, - secondary: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.animation), + if (kDebugMode) ...[ + const SubHeader('Window'), + CheckboxListTile.adaptive( + value: false, + onChanged: (v) {}, + contentPadding: DesktopSettings.horizontalPadding, + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.launch), + ), + title: const Text('Launch app on startup'), + subtitle: const Text( + 'Whether to launchthe app when the system starts', + ), ), - title: const Text('Animations'), - subtitle: const Text( - 'Disable animations on low-end devices to improve performance. ' - 'This will also disable some visual effects. '), - ), - CheckboxListTile.adaptive( - value: false, - onChanged: (v) {}, - contentPadding: DesktopSettings.horizontalPadding, - secondary: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.filter_b_and_w), + CheckboxListTile.adaptive( + value: false, + onChanged: (v) {}, + contentPadding: DesktopSettings.horizontalPadding, + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.sensor_door), + ), + title: const Text('Minimize to tray'), + subtitle: const Text( + 'Whether to minimize app to the system tray when the window is closed. ' + 'This will keep the app running in the background.', + ), ), - title: const Text('High contrast mode'), - subtitle: const Text( - 'Enable high contrast mode to make the app easier to read and use.', + const SubHeader('Acessibility'), + CheckboxListTile.adaptive( + value: true, + onChanged: (v) {}, + contentPadding: DesktopSettings.horizontalPadding, + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.animation), + ), + title: const Text('Animations'), + subtitle: const Text( + 'Disable animations on low-end devices to improve performance. ' + 'This will also disable some visual effects. '), ), - ), - CheckboxListTile.adaptive( - value: false, - onChanged: (v) {}, - contentPadding: DesktopSettings.horizontalPadding, - secondary: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.accessibility_new), + CheckboxListTile.adaptive( + value: false, + onChanged: (v) {}, + contentPadding: DesktopSettings.horizontalPadding, + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.filter_b_and_w), + ), + title: const Text('High contrast mode'), + subtitle: const Text( + 'Enable high contrast mode to make the app easier to read and use.', + ), ), - title: const Text('Large text'), - subtitle: const Text( - 'Increase the size of the text in the app to make it easier to read.', + CheckboxListTile.adaptive( + value: false, + onChanged: (v) {}, + contentPadding: DesktopSettings.horizontalPadding, + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.accessibility_new), + ), + title: const Text('Large text'), + subtitle: const Text( + 'Increase the size of the text in the app to make it easier to read.', + ), ), - ), + ], ]); } } diff --git a/lib/screens/settings/events_and_downloads.dart b/lib/screens/settings/events_and_downloads.dart index b7786d75..8e45a444 100644 --- a/lib/screens/settings/events_and_downloads.dart +++ b/lib/screens/settings/events_and_downloads.dart @@ -24,6 +24,7 @@ import 'package:bluecherry_client/screens/settings/settings_desktop.dart'; import 'package:bluecherry_client/screens/settings/shared/options_chooser_tile.dart'; import 'package:bluecherry_client/widgets/misc.dart'; import 'package:file_picker/file_picker.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; @@ -38,17 +39,18 @@ class EventsAndDownloadsSettings extends StatelessWidget { final settings = context.watch(); return ListView(children: [ SubHeader(loc.downloads), - CheckboxListTile.adaptive( - value: false, - onChanged: (v) {}, - contentPadding: DesktopSettings.horizontalPadding, - secondary: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.create_new_folder), + if (kDebugMode) + CheckboxListTile.adaptive( + value: false, + onChanged: (v) {}, + contentPadding: DesktopSettings.horizontalPadding, + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.create_new_folder), + ), + title: const Text('Choose location for each download'), ), - title: const Text('Choose location for each download'), - ), ListTile( contentPadding: DesktopSettings.horizontalPadding, leading: CircleAvatar( @@ -72,34 +74,36 @@ class EventsAndDownloadsSettings extends StatelessWidget { } }, ), - CheckboxListTile.adaptive( - value: false, - onChanged: (v) {}, - contentPadding: DesktopSettings.horizontalPadding, - secondary: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.close), + if (kDebugMode) + CheckboxListTile.adaptive( + value: false, + onChanged: (v) {}, + contentPadding: DesktopSettings.horizontalPadding, + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.close), + ), + title: const Text( + 'Block the app from closing when there are ongoing downloads'), ), - title: const Text( - 'Block the app from closing when there are ongoing downloads'), - ), const SizedBox(height: 20.0), const SubHeader('Events'), - CheckboxListTile.adaptive( - value: false, - onChanged: (v) {}, - contentPadding: DesktopSettings.horizontalPadding, - secondary: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.picture_in_picture), - ), - title: const Text('Picture-in-picture'), - subtitle: const Text( - 'Move to picture-in-picture mode when the app moves to background.', + if (kDebugMode) + CheckboxListTile.adaptive( + value: false, + onChanged: (v) {}, + contentPadding: DesktopSettings.horizontalPadding, + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.picture_in_picture), + ), + title: const Text('Picture-in-picture'), + subtitle: const Text( + 'Move to picture-in-picture mode when the app moves to background.', + ), ), - ), ListTile( leading: CircleAvatar( backgroundColor: Colors.transparent, @@ -135,48 +139,51 @@ class EventsAndDownloadsSettings extends StatelessWidget { ), ), const SizedBox(height: 20.0), - const SubHeader('Timeline of Events'), - CheckboxListTile.adaptive( - value: false, - onChanged: (v) {}, - contentPadding: DesktopSettings.horizontalPadding, - secondary: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.color_lens), - ), - title: const Text('Show different colors for events'), - subtitle: const Text( - 'Whether to show different colors for events in the timeline. ' - 'This will help you to easily identify the events.', + if (kDebugMode) ...[ + const SubHeader('Timeline of Events'), + CheckboxListTile.adaptive( + value: false, + onChanged: (v) {}, + contentPadding: DesktopSettings.horizontalPadding, + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.color_lens), + ), + title: const Text('Show different colors for events'), + subtitle: const Text( + 'Whether to show different colors for events in the timeline. ' + 'This will help you to easily identify the events.', + ), ), - ), - CheckboxListTile.adaptive( - value: false, - onChanged: (v) {}, - contentPadding: DesktopSettings.horizontalPadding, - secondary: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.pause_presentation), + CheckboxListTile.adaptive( + value: false, + onChanged: (v) {}, + contentPadding: DesktopSettings.horizontalPadding, + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.pause_presentation), + ), + title: const Text('Pause to buffer'), + subtitle: const Text( + 'Whether the entire timeline should pause to buffer the events.', + ), ), - title: const Text('Pause to buffer'), - subtitle: const Text( - 'Whether the entire timeline should pause to buffer the events.', + OptionsChooserTile( + title: 'Initial point', + description: 'When the timeline should begin.', + icon: Icons.flag, + value: '', + values: const [ + Option(value: '', icon: Icons.start, text: 'Beginning'), + Option(value: '', icon: Icons.first_page, text: 'First event'), + Option( + value: '', icon: Icons.hourglass_bottom, text: 'An hour ago'), + ], + onChanged: (v) {}, ), - ), - OptionsChooserTile( - title: 'Initial point', - description: 'When the timeline should begin.', - icon: Icons.flag, - value: '', - values: const [ - Option(value: '', icon: Icons.start, text: 'Beginning'), - Option(value: '', icon: Icons.first_page, text: 'First event'), - Option(value: '', icon: Icons.hourglass_bottom, text: 'An hour ago'), - ], - onChanged: (v) {}, - ), + ], ]); } } diff --git a/lib/screens/settings/general.dart b/lib/screens/settings/general.dart index a6bd066f..db13eb92 100644 --- a/lib/screens/settings/general.dart +++ b/lib/screens/settings/general.dart @@ -22,6 +22,7 @@ import 'package:bluecherry_client/screens/settings/settings_desktop.dart'; import 'package:bluecherry_client/screens/settings/shared/options_chooser_tile.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/widgets/misc.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; @@ -145,45 +146,47 @@ class GeneralSettings extends StatelessWidget { settings.kNotificationClickBehavior.value = v; }, ), - const SubHeader( - 'Data Usage', - padding: DesktopSettings.horizontalPadding, - ), - OptionsChooserTile( - icon: Icons.data_usage, - title: 'Automatic streaming', - description: 'When to stream videos automatically on startup', - value: '', - values: const [ - Option(value: '', icon: Icons.insights, text: 'Auto'), - Option(value: '', icon: Icons.wifi, text: 'Wifi only'), - Option(value: '', icon: Icons.not_interested, text: 'Never'), - ], - onChanged: (value) {}, - ), - OptionsChooserTile( - icon: Icons.cloud_done, - title: 'Keep streams playing on background', - description: - 'When to keep streams playing when the app is in background', - value: '', - values: const [ - Option(value: '', icon: Icons.insights, text: 'Auto'), - Option(value: '', icon: Icons.wifi, text: 'Wifi only'), - Option(value: '', icon: Icons.not_interested, text: 'Never'), - ], - onChanged: (value) {}, - ), - ListTile( - leading: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.show_chart), + if (kDebugMode) ...[ + const SubHeader( + 'Data Usage', + padding: DesktopSettings.horizontalPadding, ), - contentPadding: DesktopSettings.horizontalPadding, - title: const Text('View previous data usage'), - trailing: const Icon(Icons.navigate_next), - ), + OptionsChooserTile( + icon: Icons.data_usage, + title: 'Automatic streaming', + description: 'When to stream videos automatically on startup', + value: '', + values: const [ + Option(value: '', icon: Icons.insights, text: 'Auto'), + Option(value: '', icon: Icons.wifi, text: 'Wifi only'), + Option(value: '', icon: Icons.not_interested, text: 'Never'), + ], + onChanged: (value) {}, + ), + OptionsChooserTile( + icon: Icons.cloud_done, + title: 'Keep streams playing on background', + description: + 'When to keep streams playing when the app is in background', + value: '', + values: const [ + Option(value: '', icon: Icons.insights, text: 'Auto'), + Option(value: '', icon: Icons.wifi, text: 'Wifi only'), + Option(value: '', icon: Icons.not_interested, text: 'Never'), + ], + onChanged: (value) {}, + ), + ListTile( + leading: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.show_chart), + ), + contentPadding: DesktopSettings.horizontalPadding, + title: const Text('View previous data usage'), + trailing: const Icon(Icons.navigate_next), + ), + ], ]); } } diff --git a/lib/screens/settings/server_and_devices.dart b/lib/screens/settings/server_and_devices.dart index ac8db8f8..c638ef84 100644 --- a/lib/screens/settings/server_and_devices.dart +++ b/lib/screens/settings/server_and_devices.dart @@ -124,25 +124,27 @@ class StreamingSettings extends StatelessWidget { settings.kVideoFit.value = v; }, ), - const SizedBox(height: 8.0), - OptionsChooserTile( - title: 'Refresh Period', - description: 'How often to refresh the cameras', - icon: Icons.sync, - value: Duration.zero, - values: const [ - Duration.zero, - Duration(seconds: 30), - Duration(minutes: 2), - Duration(minutes: 5), - ].map((q) { - return Option( - value: q, - text: q.humanReadableCompact(context), - ); - }), - onChanged: (v) {}, - ), + if (kDebugMode) ...[ + const SizedBox(height: 8.0), + OptionsChooserTile( + title: 'Refresh Period', + description: 'How often to refresh the cameras', + icon: Icons.sync, + value: Duration.zero, + values: const [ + Duration.zero, + Duration(seconds: 30), + Duration(minutes: 2), + Duration(minutes: 5), + ].map((q) { + return Option( + value: q, + text: q.humanReadableCompact(context), + ); + }), + onChanged: (v) {}, + ), + ], const SizedBox(height: 8.0), OptionsChooserTile( title: loc.lateStreamBehavior, @@ -209,43 +211,46 @@ class StreamingSettings extends StatelessWidget { }, ), const SizedBox(height: 8.0), - CheckboxListTile.adaptive( - secondary: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.history), + if (kDebugMode) ...[ + CheckboxListTile.adaptive( + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.history), + ), + title: const Text('Automatically reload timed out streams'), + subtitle: + const Text('When to reload timed out streams automatically'), + contentPadding: DesktopSettings.horizontalPadding, + value: true, + onChanged: (v) {}, ), - title: const Text('Automatically reload timed out streams'), - subtitle: const Text('When to reload timed out streams automatically'), - contentPadding: DesktopSettings.horizontalPadding, - value: true, - onChanged: (v) {}, - ), - const SizedBox(height: 8.0), - CheckboxListTile.adaptive( - secondary: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.memory), + const SizedBox(height: 8.0), + CheckboxListTile.adaptive( + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.memory), + ), + title: const Text('Hardware decoding'), + subtitle: const Text( + 'Use hardware decoding when available. This improves the ' + 'performance of the video streams and reduce the CPU usage. ' + 'If not supported, it will fall back to software rendering. ', + ), + isThreeLine: true, + contentPadding: DesktopSettings.horizontalPadding, + value: true, + onChanged: (v) {}, ), - title: const Text('Hardware decoding'), - subtitle: const Text( - 'Use hardware decoding when available. This improves the ' - 'performance of the video streams and reduce the CPU usage. ' - 'If not supported, it will fall back to software rendering. ', + const SizedBox(height: 8.0), + ListTile( + title: const Text('Run a video test'), + trailing: const Icon(Icons.play_arrow), + contentPadding: DesktopSettings.horizontalPadding, + onTap: () {}, ), - isThreeLine: true, - contentPadding: DesktopSettings.horizontalPadding, - value: true, - onChanged: (v) {}, - ), - const SizedBox(height: 8.0), - ListTile( - title: const Text('Run a video test'), - trailing: const Icon(Icons.play_arrow), - contentPadding: DesktopSettings.horizontalPadding, - onTap: () {}, - ), + ], ]); } } diff --git a/lib/screens/settings/shared/update.dart b/lib/screens/settings/shared/update.dart index 0c872ccb..3f615898 100644 --- a/lib/screens/settings/shared/update.dart +++ b/lib/screens/settings/shared/update.dart @@ -22,6 +22,7 @@ import 'package:bluecherry_client/providers/update_provider.dart'; import 'package:bluecherry_client/screens/settings/settings_desktop.dart'; import 'package:bluecherry_client/utils/methods.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -269,20 +270,21 @@ class AppUpdateOptions extends StatelessWidget { ), isThreeLine: true, ), - CheckboxListTile.adaptive( - secondary: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.memory), - ), - title: const Text('Show release notes'), - subtitle: const Text( - 'Display release notes when a new version is downloaded', + if (kDebugMode) + CheckboxListTile.adaptive( + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.memory), + ), + title: const Text('Show release notes'), + subtitle: const Text( + 'Display release notes when a new version is downloaded', + ), + contentPadding: DesktopSettings.horizontalPadding, + value: true, + onChanged: (v) {}, ), - contentPadding: DesktopSettings.horizontalPadding, - value: true, - onChanged: (v) {}, - ), ListTile( leading: CircleAvatar( backgroundColor: Colors.transparent, diff --git a/lib/screens/settings/updates_and_help.dart b/lib/screens/settings/updates_and_help.dart index 73602189..e2fed0eb 100644 --- a/lib/screens/settings/updates_and_help.dart +++ b/lib/screens/settings/updates_and_help.dart @@ -19,16 +19,12 @@ import 'dart:io'; -import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/providers/update_provider.dart'; import 'package:bluecherry_client/screens/settings/shared/update.dart'; -import 'package:bluecherry_client/utils/logging.dart'; -import 'package:bluecherry_client/utils/window.dart'; import 'package:bluecherry_client/widgets/misc.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:provider/provider.dart'; class UpdatesSettings extends StatelessWidget { const UpdatesSettings({super.key}); @@ -60,92 +56,8 @@ class UpdatesSettings extends StatelessWidget { ], // TODO(bdlukaa): Show option to downlaod the native client when running // on the web. - // Padding( - // padding: DesktopSettings.horizontalPadding, - // child: Text('Beta Features', style: theme.textTheme.titleMedium), - // ), - // const BetaFeatures(), const Divider(), const About(), ]); } } - -class BetaFeatures extends StatelessWidget { - const BetaFeatures({super.key}); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final loc = AppLocalizations.of(context); - final settings = context.watch(); - - return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - CheckboxListTile.adaptive( - secondary: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.crop), - ), - title: Text(loc.matrixedViewZoom), - subtitle: Text(loc.matrixedViewZoomDescription), - value: settings.kDefaultBetaMatrixedZoomEnabled.value, - onChanged: (value) { - if (value != null) { - settings.kDefaultBetaMatrixedZoomEnabled.value = value; - } - }, - ), - ExpansionTile( - leading: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.developer_mode), - ), - title: const Text('Developer options'), - subtitle: - const Text('Most of these options are for debugging purposes'), - children: [ - if (!kIsWeb) - FutureBuilder( - future: getLogFile(), - builder: (context, snapshot) { - return ListTile( - contentPadding: const EdgeInsetsDirectional.only( - start: 68.0, - end: 26.0, - ), - leading: const Icon(Icons.bug_report), - title: const Text('Open log file'), - subtitle: Text(snapshot.data?.path ?? loc.loading), - trailing: const Icon(Icons.navigate_next), - dense: false, - onTap: snapshot.data == null - ? null - : () { - launchFileExplorer(snapshot.data!.path); - }, - ); - }, - ), - CheckboxListTile( - contentPadding: const EdgeInsetsDirectional.only( - start: 68.0, - end: 26.0, - ), - secondary: const Icon(Icons.adb), - title: const Text('Show debug info'), - subtitle: const Text('Display useful information for debugging'), - value: settings.kShowDebugInfo.value, - onChanged: (v) { - if (v != null) { - settings.kShowDebugInfo.value = v; - } - }, - dense: false, - ) - ], - ), - ]); - } -} From fcfd462029b0f53f81458a242b9fe86c46203c42 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Sun, 3 Mar 2024 13:01:08 -0300 Subject: [PATCH 24/26] feat: Update SettingsProvider when the value of an option is updated --- lib/providers/settings_provider.dart | 103 ++++++++++++++------------- 1 file changed, 55 insertions(+), 48 deletions(-) diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 4449ca34..f1f39a05 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -33,7 +33,7 @@ enum TimelineIntialPoint { beggining, firstEvent, lastEvent } enum EnabledPreference { on, ask, never } -class SettingsOption { +class _SettingsOption { final String key; final T def; final Future Function()? getDefault; @@ -41,9 +41,16 @@ class SettingsOption { late final String Function(T value) saveAs; late final T Function(String value) loadFrom; - late T value; + late T _value; - SettingsOption({ + T get value => _value; + set value(T newValue) { + SettingsProvider.instance.updateProperty(() { + _value = newValue; + }); + } + + _SettingsOption({ required this.key, required this.def, this.getDefault, @@ -51,7 +58,7 @@ class SettingsOption { T Function(String value)? loadFrom, }) { Future.microtask(() async { - value = getDefault != null ? await getDefault!() : def; + _value = getDefault != null ? await getDefault!() : def; if (saveAs != null) { this.saveAs = saveAs; @@ -101,7 +108,7 @@ class SettingsOption { if (getDefault != null) serializedData ??= saveAs(await getDefault!()); serializedData ??= defAsString; - value = loadFrom(serializedData); + _value = loadFrom(serializedData); } } @@ -110,29 +117,29 @@ class SettingsProvider extends UnityProvider { static late SettingsProvider instance; // General settings - final kLayoutCyclePeriod = SettingsOption( + final kLayoutCyclePeriod = _SettingsOption( def: const Duration(seconds: 5), key: 'general.cycle_period', ); - final kLayoutCycleEnabled = SettingsOption( + final kLayoutCycleEnabled = _SettingsOption( def: true, key: 'general.cycle_enabled', ); - final kWakelock = SettingsOption( + final kWakelock = _SettingsOption( def: true, key: 'general.wakelock', ); // Notifications - final kNotificationsEnabled = SettingsOption( + final kNotificationsEnabled = _SettingsOption( def: true, key: 'notifications.enabled', ); - final kSnoozeNotificationsUntil = SettingsOption( + final kSnoozeNotificationsUntil = _SettingsOption( def: DateTime.utc(1969, 7, 20, 20, 18, 04), key: 'notifications.snooze_until', ); - final kNotificationClickBehavior = SettingsOption( + final kNotificationClickBehavior = _SettingsOption( def: NotificationClickBehavior.showEventsScreen, key: 'notifications.click_behavior', loadFrom: (value) => NotificationClickBehavior.values[int.parse(value)], @@ -140,13 +147,13 @@ class SettingsProvider extends UnityProvider { ); // Data usage - final kAutomaticStreaming = SettingsOption( + final kAutomaticStreaming = _SettingsOption( def: NetworkUsage.wifiOnly, key: 'data_usage.automatic_streaming', loadFrom: (value) => NetworkUsage.values[int.parse(value)], saveAs: (value) => value.index.toString(), ); - final kStreamOnBackground = SettingsOption( + final kStreamOnBackground = _SettingsOption( def: NetworkUsage.wifiOnly, key: 'data_usage.stream_on_background', loadFrom: (value) => NetworkUsage.values[int.parse(value)], @@ -154,63 +161,63 @@ class SettingsProvider extends UnityProvider { ); // Streaming settings - final kStreamingType = SettingsOption( + final kStreamingType = _SettingsOption( def: kIsWeb ? StreamingType.hls : StreamingType.rtsp, key: 'streaming.type', loadFrom: (value) => StreamingType.values[int.parse(value)], saveAs: (value) => value.index.toString(), ); - final kRTSPProtocol = SettingsOption( + final kRTSPProtocol = _SettingsOption( def: RTSPProtocol.tcp, key: 'streaming.rtsp_protocol', loadFrom: (value) => RTSPProtocol.values[int.parse(value)], saveAs: (value) => value.index.toString(), ); - final kRenderingQuality = SettingsOption( + final kRenderingQuality = _SettingsOption( def: RenderingQuality.automatic, key: 'streaming.rendering_quality', loadFrom: (value) => RenderingQuality.values[int.parse(value)], saveAs: (value) => value.index.toString(), ); - final kVideoFit = SettingsOption( + final kVideoFit = _SettingsOption( def: UnityVideoFit.contain, key: 'streaming.video_fit', loadFrom: (value) => UnityVideoFit.values[int.parse(value)], saveAs: (value) => value.index.toString(), ); - final kRefreshRate = SettingsOption( + final kRefreshRate = _SettingsOption( def: const Duration(minutes: 5), key: 'streaming.refresh_rate', ); - final kLateStreamBehavior = SettingsOption( + final kLateStreamBehavior = _SettingsOption( def: LateVideoBehavior.automatic, key: 'streaming.late_video_behavior', loadFrom: (value) => LateVideoBehavior.values[int.parse(value)], saveAs: (value) => value.index.toString(), ); - final kReloadTimedOutStreams = SettingsOption( + final kReloadTimedOutStreams = _SettingsOption( def: true, key: 'streaming.reload_timed_out_streams', ); - final kUseHardwareDecoding = SettingsOption( + final kUseHardwareDecoding = _SettingsOption( def: true, key: 'streaming.use_hardware_decoding', ); // Downloads - final kDownloadOnMobileData = SettingsOption( + final kDownloadOnMobileData = _SettingsOption( def: false, key: 'downloads.download_on_mobile_data', ); - final kChooseLocationEveryTime = SettingsOption( + final kChooseLocationEveryTime = _SettingsOption( def: false, key: 'downloads.choose_location_every_time', ); - final kAllowAppCloseWhenDownloading = SettingsOption( + final kAllowAppCloseWhenDownloading = _SettingsOption( def: false, key: 'downloads.allow_app_close_when_downloading', ); - final kDownloadsDirectory = SettingsOption( + final kDownloadsDirectory = _SettingsOption( def: '', getDefault: () async => (await DownloadsManager.kDefaultDownloadsDirectory).path, @@ -218,29 +225,29 @@ class SettingsProvider extends UnityProvider { ); // Events - final kPictureInPicture = SettingsOption( + final kPictureInPicture = _SettingsOption( def: false, key: 'events.picture_in_picture', ); - final kEventsSpeed = SettingsOption( + final kEventsSpeed = _SettingsOption( def: 1.0, key: 'events.speed', ); - final kEventsVolume = SettingsOption( + final kEventsVolume = _SettingsOption( def: 1.0, key: 'events.volume', ); // Timeline of Events - final kShowDifferentColorsForEvents = SettingsOption( + final kShowDifferentColorsForEvents = _SettingsOption( def: false, key: 'timeline.show_different_colors_for_events', ); - final kPauseToBuffer = SettingsOption( + final kPauseToBuffer = _SettingsOption( def: false, key: 'timeline.pause_to_buffer', ); - final kTimelineInitialPoint = SettingsOption( + final kTimelineInitialPoint = _SettingsOption( def: TimelineIntialPoint.beggining, key: 'timeline.initial_point', loadFrom: (value) => TimelineIntialPoint.values[int.parse(value)], @@ -248,85 +255,85 @@ class SettingsProvider extends UnityProvider { ); // Application - final kThemeMode = SettingsOption( + final kThemeMode = _SettingsOption( def: ThemeMode.system, key: 'application.theme_mode', loadFrom: (value) => ThemeMode.values[int.parse(value)], saveAs: (value) => value.index.toString(), ); - final kLanguageCode = SettingsOption( + final kLanguageCode = _SettingsOption( def: Locale.fromSubtags(languageCode: Intl.getCurrentLocale()), key: 'application.language_code', ); - final kDateFormat = SettingsOption( + final kDateFormat = _SettingsOption( def: DateFormat('EEEE, dd MMMM yyyy'), key: 'application.date_format', ); - final kTimeFormat = SettingsOption( + final kTimeFormat = _SettingsOption( def: DateFormat('hh:mm a'), key: 'application.time_format', ); // Window - final kLaunchAppOnStartup = SettingsOption( + final kLaunchAppOnStartup = _SettingsOption( def: false, key: 'window.launch_app_on_startup', ); - final kMinimizeToTray = SettingsOption( + final kMinimizeToTray = _SettingsOption( def: false, key: 'window.minimize_to_tray', ); // Acessibility - final kAnimationsEnabled = SettingsOption( + final kAnimationsEnabled = _SettingsOption( def: true, key: 'accessibility.animations_enabled', ); - final kHighContrast = SettingsOption( + final kHighContrast = _SettingsOption( def: false, key: 'accessibility.high_contrast', ); - final kLargeFont = SettingsOption( + final kLargeFont = _SettingsOption( def: false, key: 'accessibility.large_font', ); // Privacy and Security - final kAllowDataCollection = SettingsOption( + final kAllowDataCollection = _SettingsOption( def: true, key: 'privacy.allow_data_collection', ); - final kAllowCrashReports = SettingsOption( + final kAllowCrashReports = _SettingsOption( def: true, key: 'privacy.allow_crash_reports', ); // Updates - final kAutoUpdate = SettingsOption( + final kAutoUpdate = _SettingsOption( def: true, key: 'updates.auto_update', ); - final kShowReleaseNotes = SettingsOption( + final kShowReleaseNotes = _SettingsOption( def: true, key: 'updates.show_release_notes', ); // Other - final kDefaultBetaMatrixedZoomEnabled = SettingsOption( + final kDefaultBetaMatrixedZoomEnabled = _SettingsOption( def: false, key: 'other.matrixed_zoom_enabled', ); - final kMatrixSize = SettingsOption( + final kMatrixSize = _SettingsOption( def: MatrixType.t16, key: 'other.matrix_size', loadFrom: (value) => MatrixType.values[int.parse(value)], saveAs: (value) => value.index.toString(), ); - final kShowDebugInfo = SettingsOption( + final kShowDebugInfo = _SettingsOption( def: false, key: 'other.show_debug_info', ); - final kShowNetworkUsage = SettingsOption( + final kShowNetworkUsage = _SettingsOption( def: false, key: 'other.show_network_usage', ); From 6ef5a506f0a5a37efa67aea4182b8f172b827d21 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Sun, 3 Mar 2024 13:21:22 -0300 Subject: [PATCH 25/26] feat: Update all the options values in the settings --- lib/providers/settings_provider.dart | 4 +- lib/screens/settings/application.dart | 47 ++++++++---- .../settings/events_and_downloads.dart | 44 ++++++++---- lib/screens/settings/general.dart | 8 ++- .../settings/privacy_and_security.dart | 72 ++++++++++++------- lib/screens/settings/server_and_devices.dart | 22 ++++-- lib/screens/settings/shared/update.dart | 2 +- 7 files changed, 136 insertions(+), 63 deletions(-) diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index f1f39a05..70844482 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -303,8 +303,8 @@ class SettingsProvider extends UnityProvider { def: true, key: 'privacy.allow_data_collection', ); - final kAllowCrashReports = _SettingsOption( - def: true, + final kAllowCrashReports = _SettingsOption( + def: EnabledPreference.on, key: 'privacy.allow_crash_reports', ); diff --git a/lib/screens/settings/application.dart b/lib/screens/settings/application.dart index 63b2249a..7fa56182 100644 --- a/lib/screens/settings/application.dart +++ b/lib/screens/settings/application.dart @@ -67,8 +67,12 @@ class ApplicationSettings extends StatelessWidget { if (kDebugMode) ...[ const SubHeader('Window'), CheckboxListTile.adaptive( - value: false, - onChanged: (v) {}, + value: settings.kLaunchAppOnStartup.value, + onChanged: (v) { + if (v != null) { + settings.kLaunchAppOnStartup.value = v; + } + }, contentPadding: DesktopSettings.horizontalPadding, secondary: CircleAvatar( backgroundColor: Colors.transparent, @@ -81,8 +85,12 @@ class ApplicationSettings extends StatelessWidget { ), ), CheckboxListTile.adaptive( - value: false, - onChanged: (v) {}, + value: settings.kMinimizeToTray.value, + onChanged: (v) { + if (v != null) { + settings.kMinimizeToTray.value = v; + } + }, contentPadding: DesktopSettings.horizontalPadding, secondary: CircleAvatar( backgroundColor: Colors.transparent, @@ -97,8 +105,12 @@ class ApplicationSettings extends StatelessWidget { ), const SubHeader('Acessibility'), CheckboxListTile.adaptive( - value: true, - onChanged: (v) {}, + value: settings.kAnimationsEnabled.value, + onChanged: (v) { + if (v != null) { + settings.kAnimationsEnabled.value = v; + } + }, contentPadding: DesktopSettings.horizontalPadding, secondary: CircleAvatar( backgroundColor: Colors.transparent, @@ -107,12 +119,17 @@ class ApplicationSettings extends StatelessWidget { ), title: const Text('Animations'), subtitle: const Text( - 'Disable animations on low-end devices to improve performance. ' - 'This will also disable some visual effects. '), + 'Disable animations on low-end devices to improve performance. This ' + 'will also disable some visual effects. ', + ), ), CheckboxListTile.adaptive( - value: false, - onChanged: (v) {}, + value: settings.kHighContrast.value, + onChanged: (v) { + if (v != null) { + settings.kHighContrast.value = v; + } + }, contentPadding: DesktopSettings.horizontalPadding, secondary: CircleAvatar( backgroundColor: Colors.transparent, @@ -125,15 +142,19 @@ class ApplicationSettings extends StatelessWidget { ), ), CheckboxListTile.adaptive( - value: false, - onChanged: (v) {}, + value: settings.kLargeFont.value, + onChanged: (v) { + if (v != null) { + settings.kLargeFont.value = v; + } + }, contentPadding: DesktopSettings.horizontalPadding, secondary: CircleAvatar( backgroundColor: Colors.transparent, foregroundColor: theme.iconTheme.color, child: const Icon(Icons.accessibility_new), ), - title: const Text('Large text'), + title: const Text('Large Font'), subtitle: const Text( 'Increase the size of the text in the app to make it easier to read.', ), diff --git a/lib/screens/settings/events_and_downloads.dart b/lib/screens/settings/events_and_downloads.dart index 8e45a444..43079a6d 100644 --- a/lib/screens/settings/events_and_downloads.dart +++ b/lib/screens/settings/events_and_downloads.dart @@ -41,8 +41,12 @@ class EventsAndDownloadsSettings extends StatelessWidget { SubHeader(loc.downloads), if (kDebugMode) CheckboxListTile.adaptive( - value: false, - onChanged: (v) {}, + value: settings.kChooseLocationEveryTime.value, + onChanged: (v) { + if (v != null) { + settings.kChooseLocationEveryTime.value = v; + } + }, contentPadding: DesktopSettings.horizontalPadding, secondary: CircleAvatar( backgroundColor: Colors.transparent, @@ -91,8 +95,12 @@ class EventsAndDownloadsSettings extends StatelessWidget { const SubHeader('Events'), if (kDebugMode) CheckboxListTile.adaptive( - value: false, - onChanged: (v) {}, + value: settings.kPictureInPicture.value, + onChanged: (v) { + if (v != null) { + settings.kPictureInPicture.value = v; + } + }, contentPadding: DesktopSettings.horizontalPadding, secondary: CircleAvatar( backgroundColor: Colors.transparent, @@ -116,8 +124,10 @@ class EventsAndDownloadsSettings extends StatelessWidget { trailing: SizedBox( width: 160.0, child: Slider( - value: 1.0, - onChanged: (v) {}, + value: settings.kEventsSpeed.value, + onChanged: (v) { + settings.kEventsSpeed.value = v; + }, ), ), ), @@ -133,8 +143,10 @@ class EventsAndDownloadsSettings extends StatelessWidget { trailing: SizedBox( width: 160.0, child: Slider( - value: 1.0, - onChanged: (v) {}, + value: settings.kEventsVolume.value, + onChanged: (v) { + settings.kEventsVolume.value = v; + }, ), ), ), @@ -142,8 +154,12 @@ class EventsAndDownloadsSettings extends StatelessWidget { if (kDebugMode) ...[ const SubHeader('Timeline of Events'), CheckboxListTile.adaptive( - value: false, - onChanged: (v) {}, + value: settings.kShowDifferentColorsForEvents.value, + onChanged: (v) { + if (v != null) { + settings.kShowDifferentColorsForEvents.value = v; + } + }, contentPadding: DesktopSettings.horizontalPadding, secondary: CircleAvatar( backgroundColor: Colors.transparent, @@ -157,8 +173,12 @@ class EventsAndDownloadsSettings extends StatelessWidget { ), ), CheckboxListTile.adaptive( - value: false, - onChanged: (v) {}, + value: settings.kPauseToBuffer.value, + onChanged: (v) { + if (v != null) { + settings.kPauseToBuffer.value = v; + } + }, contentPadding: DesktopSettings.horizontalPadding, secondary: CircleAvatar( backgroundColor: Colors.transparent, diff --git a/lib/screens/settings/general.dart b/lib/screens/settings/general.dart index db13eb92..5249d879 100644 --- a/lib/screens/settings/general.dart +++ b/lib/screens/settings/general.dart @@ -80,8 +80,12 @@ class GeneralSettings extends StatelessWidget { ), contentPadding: DesktopSettings.horizontalPadding, title: const Text('Notifications enabled'), - value: true, - onChanged: (value) {}, + value: settings.kNotificationsEnabled.value, + onChanged: (value) { + if (value != null) { + settings.kNotificationsEnabled.value = value; + } + }, ), ListTile( contentPadding: DesktopSettings.horizontalPadding, diff --git a/lib/screens/settings/privacy_and_security.dart b/lib/screens/settings/privacy_and_security.dart index 55da33f4..b3437d28 100644 --- a/lib/screens/settings/privacy_and_security.dart +++ b/lib/screens/settings/privacy_and_security.dart @@ -17,9 +17,13 @@ * along with this program. If not, see . */ +import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/screens/settings/settings_desktop.dart'; import 'package:bluecherry_client/screens/settings/shared/options_chooser_tile.dart'; +import 'package:bluecherry_client/utils/extensions.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class PrivacySecuritySettings extends StatelessWidget { const PrivacySecuritySettings({super.key}); @@ -27,6 +31,7 @@ class PrivacySecuritySettings extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final settings = context.watch(); return ListView(children: [ CheckboxListTile.adaptive( secondary: CircleAvatar( @@ -42,41 +47,54 @@ class PrivacySecuritySettings extends StatelessWidget { 'any personal information.', ), isThreeLine: true, - value: true, - onChanged: (value) {}, + value: settings.kAllowDataCollection.value, + onChanged: (value) { + if (value != null) { + settings.kAllowDataCollection.value = value; + } + }, ), - OptionsChooserTile( + OptionsChooserTile( title: 'Automatically report errors', description: 'Automatically report errors to Bluecherry to help us ' 'improve the app. Error reports may contain personal information.', icon: Icons.error, - value: 'On', - values: ['On', 'Ask', 'Error'].map((e) => Option(text: e, value: e)), - onChanged: (v) {}, - ), - const Divider(), - ListTile( - contentPadding: DesktopSettings.horizontalPadding, - leading: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.privacy_tip), + value: settings.kAllowCrashReports.value, + values: EnabledPreference.values.map( + (e) => Option( + text: e.name.uppercaseFirst, + value: e, + ), ), - title: const Text('Privacy Policy'), - trailing: const Icon(Icons.chevron_right), - onTap: () {}, + onChanged: (v) { + settings.kAllowCrashReports.value = v; + }, ), - ListTile( - contentPadding: DesktopSettings.horizontalPadding, - leading: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.policy), + if (kDebugMode) ...[ + const Divider(), + ListTile( + contentPadding: DesktopSettings.horizontalPadding, + leading: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.privacy_tip), + ), + title: const Text('Privacy Policy'), + trailing: const Icon(Icons.chevron_right), + onTap: () {}, ), - title: const Text('Terms of Service'), - trailing: const Icon(Icons.chevron_right), - onTap: () {}, - ), + ListTile( + contentPadding: DesktopSettings.horizontalPadding, + leading: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.policy), + ), + title: const Text('Terms of Service'), + trailing: const Icon(Icons.chevron_right), + onTap: () {}, + ), + ], ]); } } diff --git a/lib/screens/settings/server_and_devices.dart b/lib/screens/settings/server_and_devices.dart index c638ef84..000cf7d4 100644 --- a/lib/screens/settings/server_and_devices.dart +++ b/lib/screens/settings/server_and_devices.dart @@ -130,7 +130,7 @@ class StreamingSettings extends StatelessWidget { title: 'Refresh Period', description: 'How often to refresh the cameras', icon: Icons.sync, - value: Duration.zero, + value: settings.kRefreshRate.value, values: const [ Duration.zero, Duration(seconds: 30), @@ -142,7 +142,9 @@ class StreamingSettings extends StatelessWidget { text: q.humanReadableCompact(context), ); }), - onChanged: (v) {}, + onChanged: (v) { + settings.kRefreshRate.value = v; + }, ), ], const SizedBox(height: 8.0), @@ -222,8 +224,12 @@ class StreamingSettings extends StatelessWidget { subtitle: const Text('When to reload timed out streams automatically'), contentPadding: DesktopSettings.horizontalPadding, - value: true, - onChanged: (v) {}, + value: settings.kReloadTimedOutStreams.value, + onChanged: (v) { + if (v != null) { + settings.kReloadTimedOutStreams.value = v; + } + }, ), const SizedBox(height: 8.0), CheckboxListTile.adaptive( @@ -240,8 +246,12 @@ class StreamingSettings extends StatelessWidget { ), isThreeLine: true, contentPadding: DesktopSettings.horizontalPadding, - value: true, - onChanged: (v) {}, + value: settings.kUseHardwareDecoding.value, + onChanged: (v) { + if (v != null) { + settings.kUseHardwareDecoding.value = v; + } + }, ), const SizedBox(height: 8.0), ListTile( diff --git a/lib/screens/settings/shared/update.dart b/lib/screens/settings/shared/update.dart index 3f615898..35f5583c 100644 --- a/lib/screens/settings/shared/update.dart +++ b/lib/screens/settings/shared/update.dart @@ -407,7 +407,7 @@ class About extends StatelessWidget { ), const SizedBox(width: 8.0), Link( - uri: Uri.https('www.bluecherrydvr.com', '/contact'), + uri: Uri.https('www.bluecherrydvr.com', '/contact/'), builder: (context, open) { return TextButton( onPressed: open, From bf451042fb4e05af735377637f7c366765e308d2 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Sun, 3 Mar 2024 13:40:56 -0300 Subject: [PATCH 26/26] fix: Fallback to the default value if data is corrupted --- lib/providers/settings.dart | 482 --------------------------- lib/providers/settings_provider.dart | 7 +- 2 files changed, 6 insertions(+), 483 deletions(-) delete mode 100644 lib/providers/settings.dart diff --git a/lib/providers/settings.dart b/lib/providers/settings.dart deleted file mode 100644 index 62ac8ffd..00000000 --- a/lib/providers/settings.dart +++ /dev/null @@ -1,482 +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 . - */ - -import 'package:bluecherry_client/providers/app_provider_interface.dart'; -import 'package:bluecherry_client/providers/settings_provider.dart'; -import 'package:bluecherry_client/screens/layouts/desktop/external_stream.dart'; -import 'package:bluecherry_client/utils/storage.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:unity_video_player/unity_video_player.dart'; - -enum NetworkUsage { auto, wifiOnly, never } - -enum TimelineIntialPoint { beggining, firstEvent, lastEvent } - -enum EnabledPreference { on, ask, never } - -class SettingsOption { - final String key; - final T def; - - late final String Function(T value) saveAs; - late final T Function(String value) loadFrom; - - late T value; - - SettingsOption({ - required this.key, - required this.def, - String Function(T value)? saveAs, - T Function(String value)? loadFrom, - }) { - value = def; - - if (saveAs != null) { - this.saveAs = saveAs; - } else if (T == bool) { - this.saveAs = (value) => value.toString(); - } else if (T == Duration) { - this.saveAs = (value) => (value as Duration).inMilliseconds.toString(); - } else if (T == Enum) { - this.saveAs = (value) => (value as Enum).index.toString(); - } else { - this.saveAs = (value) => value.toString(); - } - - if (loadFrom != null) { - this.loadFrom = loadFrom; - } else if (T == bool) { - this.loadFrom = (value) => (bool.tryParse(value) ?? def) as T; - } else if (T == Duration) { - this.loadFrom = (value) => Duration(milliseconds: int.parse(value)) as T; - } else if (T == Enum) { - throw UnsupportedError('Enum type must provide a loadFrom function'); - } else { - this.loadFrom = (value) => value as T; - } - } - - String get defAsString => saveAs(def); -} - -class AppSettings extends UnityProvider { - AppSettings._(); - static late AppSettings instance; - - // General settings - final kLayoutCyclePeriod = SettingsOption( - def: const Duration(seconds: 5), - key: 'general.cycle_period', - ); - final kWakelock = SettingsOption( - def: true, - key: 'general.wakelock', - ); - - // Notifications - final kNotificationsEnabled = SettingsOption( - def: true, - key: 'notifications.enabled', - ); - final kSnoozeNotificationsUntil = SettingsOption( - def: null, - key: 'notifications.snooze_until', - ); - final kNotificationClickBehavior = SettingsOption( - def: NotificationClickBehavior.showEventsScreen, - key: 'notifications.click_behavior', - loadFrom: (value) => NotificationClickBehavior.values[int.parse(value)], - ); - - // Data usage - final kAutomaticStreaming = SettingsOption( - def: NetworkUsage.wifiOnly, - key: 'data_usage.automatic_streaming', - loadFrom: (value) => NetworkUsage.values[int.parse(value)], - ); - final kStreamOnBackground = SettingsOption( - def: NetworkUsage.wifiOnly, - key: 'data_usage.stream_on_background', - loadFrom: (value) => NetworkUsage.values[int.parse(value)], - ); - - // Streaming settings - final kStreamingType = SettingsOption( - def: kIsWeb ? StreamingType.hls : StreamingType.rtsp, - key: 'streaming.type', - loadFrom: (value) => StreamingType.values[int.parse(value)], - ); - final kRTSPProtocol = SettingsOption( - def: RTSPProtocol.tcp, - key: 'streaming.rtsp_protocol', - loadFrom: (value) => RTSPProtocol.values[int.parse(value)], - ); - final kRenderingQuality = SettingsOption( - def: RenderingQuality.automatic, - key: 'streaming.rendering_quality', - loadFrom: (value) => RenderingQuality.values[int.parse(value)], - ); - final kVideoFit = SettingsOption( - def: UnityVideoFit.contain, - key: 'streaming.video_fit', - loadFrom: (value) => UnityVideoFit.values[int.parse(value)], - ); - final kRefreshRate = SettingsOption( - def: const Duration(minutes: 5), - key: 'streaming.refresh_rate', - ); - final kLateStreamBehavior = SettingsOption( - def: LateVideoBehavior.automatic, - key: 'streaming.late_video_behavior', - loadFrom: (value) => LateVideoBehavior.values[int.parse(value)], - ); - final kReloadTimedOutStreams = SettingsOption( - def: true, - key: 'streaming.reload_timed_out_streams', - ); - final kUseHardwareDecoding = SettingsOption( - def: true, - key: 'streaming.use_hardware_decoding', - ); - - // Downloads - final kDownloadOnMobileData = SettingsOption( - def: false, - key: 'downloads.download_on_mobile_data', - ); - final kChooseLocationEveryTime = SettingsOption( - def: false, - key: 'downloads.choose_location_every_time', - ); - final kAllowAppCloseWhenDownloading = SettingsOption( - def: false, - key: 'downloads.allow_app_close_when_downloading', - ); - - // Events - final kPictureInPicture = SettingsOption( - def: false, - key: 'events.picture_in_picture', - ); - final kEventsSpeed = SettingsOption( - def: 1.0, - key: 'events.speed', - ); - final kEventsVolume = SettingsOption( - def: 1.0, - key: 'events.volume', - ); - - // Timeline of Events - final kShowDifferentColorsForEvents = SettingsOption( - def: false, - key: 'timeline.show_different_colors_for_events', - ); - final kPauseToBuffer = SettingsOption( - def: false, - key: 'timeline.pause_to_buffer', - ); - final kTimelineInitialPoint = SettingsOption( - def: TimelineIntialPoint.beggining, - key: 'timeline.initial_point', - loadFrom: (value) => TimelineIntialPoint.values[int.parse(value)], - ); - - // Application - final kThemeMode = SettingsOption( - def: ThemeMode.system, - key: 'application.theme_mode', - loadFrom: (value) => ThemeMode.values[int.parse(value)], - ); - final kLanguageCode = SettingsOption( - def: 'en', - key: 'application.language_code', - ); - final kDateFormat = SettingsOption( - def: 'EEEE, dd MMMM yyyy', - key: 'application.date_format', - ); - final kTimeFormat = SettingsOption( - def: 'hh:mm a', - key: 'application.time_format', - ); - - // Window - final kLaunchAppOnStartup = SettingsOption( - def: false, - key: 'window.launch_app_on_startup', - ); - final kMinimizeToTray = SettingsOption( - def: false, - key: 'window.minimize_to_tray', - ); - - // Acessibility - final kAnimationsEnabled = SettingsOption( - def: true, - key: 'accessibility.animations_enabled', - ); - final kHighContrast = SettingsOption( - def: false, - key: 'accessibility.high_contrast', - ); - final kLargeFont = SettingsOption( - def: false, - key: 'accessibility.large_font', - ); - - // Privacy and Security - final kAllowDataCollection = SettingsOption( - def: true, - key: 'privacy.allow_data_collection', - ); - final kAllowCrashReports = SettingsOption( - def: true, - key: 'privacy.allow_crash_reports', - ); - - // Updates - final kAutoUpdate = SettingsOption( - def: true, - key: 'updates.auto_update', - ); - final kShowReleaseNotes = SettingsOption( - def: true, - key: 'updates.show_release_notes', - ); - - // Other - final kDefaultBetaMatrixedZoomEnabled = SettingsOption( - def: false, - key: 'other.matrixed_zoom_enabled', - ); - final kMatrixSize = SettingsOption( - def: MatrixType.t16, - key: 'other.matrix_size', - loadFrom: (value) => MatrixType.values[int.parse(value)], - ); - final kShowDebugInfo = SettingsOption( - def: false, - key: 'other.show_debug_info', - ); - final kShowNetworkUsage = SettingsOption( - def: false, - key: 'other.show_network_usage', - ); - - /// Initializes the [AppSettings] instance & fetches state from `async` - /// `package:hive` method-calls. Called before [runApp]. - static Future ensureInitialized() async { - instance = AppSettings._(); - await instance.initialize(); - return instance; - } - - @override - Future initialize() async { - final data = await tryReadStorage(() => settings.read()); - - kLayoutCyclePeriod.value = kLayoutCyclePeriod.loadFrom( - data[kLayoutCyclePeriod.key] ?? kLayoutCyclePeriod.defAsString, - ); - kWakelock.value = kWakelock.loadFrom( - data[kWakelock.key] ?? kWakelock.defAsString, - ); - kNotificationsEnabled.value = kNotificationsEnabled.loadFrom( - data[kNotificationsEnabled.key] ?? kNotificationsEnabled.defAsString, - ); - kSnoozeNotificationsUntil.value = kSnoozeNotificationsUntil.loadFrom( - data[kSnoozeNotificationsUntil.key] ?? - kSnoozeNotificationsUntil.defAsString, - ); - kNotificationClickBehavior.value = kNotificationClickBehavior.loadFrom( - data[kNotificationClickBehavior.key] ?? - kNotificationClickBehavior.defAsString, - ); - kAutomaticStreaming.value = kAutomaticStreaming.loadFrom( - data[kAutomaticStreaming.key] ?? kAutomaticStreaming.defAsString, - ); - kStreamOnBackground.value = kStreamOnBackground.loadFrom( - data[kStreamOnBackground.key] ?? kStreamOnBackground.defAsString, - ); - kStreamingType.value = kStreamingType.loadFrom( - data[kStreamingType.key] ?? kStreamingType.defAsString, - ); - kRTSPProtocol.value = kRTSPProtocol.loadFrom( - data[kRTSPProtocol.key] ?? kRTSPProtocol.defAsString, - ); - kRenderingQuality.value = kRenderingQuality.loadFrom( - data[kRenderingQuality.key] ?? kRenderingQuality.defAsString, - ); - kVideoFit.value = kVideoFit.loadFrom( - data[kVideoFit.key] ?? kVideoFit.defAsString, - ); - kRefreshRate.value = kRefreshRate.loadFrom( - data[kRefreshRate.key] ?? kRefreshRate.defAsString, - ); - kLateStreamBehavior.value = kLateStreamBehavior.loadFrom( - data[kLateStreamBehavior.key] ?? kLateStreamBehavior.defAsString, - ); - kReloadTimedOutStreams.value = kReloadTimedOutStreams.loadFrom( - data[kReloadTimedOutStreams.key] ?? kReloadTimedOutStreams.defAsString, - ); - kUseHardwareDecoding.value = kUseHardwareDecoding.loadFrom( - data[kUseHardwareDecoding.key] ?? kUseHardwareDecoding.defAsString, - ); - kDownloadOnMobileData.value = kDownloadOnMobileData.loadFrom( - data[kDownloadOnMobileData.key] ?? kDownloadOnMobileData.defAsString, - ); - kChooseLocationEveryTime.value = kChooseLocationEveryTime.loadFrom( - data[kChooseLocationEveryTime.key] ?? - kChooseLocationEveryTime.defAsString, - ); - kAllowAppCloseWhenDownloading.value = - kAllowAppCloseWhenDownloading.loadFrom( - data[kAllowAppCloseWhenDownloading.key] ?? - kAllowAppCloseWhenDownloading.defAsString, - ); - kPictureInPicture.value = kPictureInPicture.loadFrom( - data[kPictureInPicture.key] ?? kPictureInPicture.defAsString, - ); - kEventsSpeed.value = kEventsSpeed.loadFrom( - data[kEventsSpeed.key] ?? kEventsSpeed.defAsString, - ); - kEventsVolume.value = kEventsVolume.loadFrom( - data[kEventsVolume.key] ?? kEventsVolume.defAsString, - ); - kShowDifferentColorsForEvents.value = - kShowDifferentColorsForEvents.loadFrom( - data[kShowDifferentColorsForEvents.key] ?? - kShowDifferentColorsForEvents.defAsString, - ); - kPauseToBuffer.value = kPauseToBuffer.loadFrom( - data[kPauseToBuffer.key] ?? kPauseToBuffer.defAsString, - ); - kTimelineInitialPoint.value = kTimelineInitialPoint.loadFrom( - data[kTimelineInitialPoint.key] ?? kTimelineInitialPoint.defAsString, - ); - kThemeMode.value = kThemeMode.loadFrom( - data[kThemeMode.key] ?? kThemeMode.defAsString, - ); - kLanguageCode.value = kLanguageCode.loadFrom( - data[kLanguageCode.key] ?? kLanguageCode.defAsString, - ); - kDateFormat.value = kDateFormat.loadFrom( - data[kDateFormat.key] ?? kDateFormat.defAsString, - ); - kTimeFormat.value = kTimeFormat.loadFrom( - data[kTimeFormat.key] ?? kTimeFormat.defAsString, - ); - kLaunchAppOnStartup.value = kLaunchAppOnStartup.loadFrom( - data[kLaunchAppOnStartup.key] ?? kLaunchAppOnStartup.defAsString, - ); - kMinimizeToTray.value = kMinimizeToTray.loadFrom( - data[kMinimizeToTray.key] ?? kMinimizeToTray.defAsString, - ); - kAnimationsEnabled.value = kAnimationsEnabled.loadFrom( - data[kAnimationsEnabled.key] ?? kAnimationsEnabled.defAsString, - ); - kHighContrast.value = kHighContrast.loadFrom( - data[kHighContrast.key] ?? kHighContrast.defAsString, - ); - kLargeFont.value = kLargeFont.loadFrom( - data[kLargeFont.key] ?? kLargeFont.defAsString, - ); - kAllowDataCollection.value = kAllowDataCollection.loadFrom( - data[kAllowDataCollection.key] ?? kAllowDataCollection.defAsString, - ); - kAllowCrashReports.value = kAllowCrashReports.loadFrom( - data[kAllowCrashReports.key] ?? kAllowCrashReports.defAsString, - ); - kAutoUpdate.value = kAutoUpdate.loadFrom( - data[kAutoUpdate.key] ?? kAutoUpdate.defAsString, - ); - kShowReleaseNotes.value = kShowReleaseNotes.loadFrom( - data[kShowReleaseNotes.key] ?? kShowReleaseNotes.defAsString, - ); - kDefaultBetaMatrixedZoomEnabled.value = - kDefaultBetaMatrixedZoomEnabled.loadFrom( - data[kDefaultBetaMatrixedZoomEnabled.key] ?? - kDefaultBetaMatrixedZoomEnabled.defAsString, - ); - kMatrixSize.value = kMatrixSize.loadFrom( - data[kMatrixSize.key] ?? kMatrixSize.defAsString, - ); - kShowDebugInfo.value = kShowDebugInfo.loadFrom( - data[kShowDebugInfo.key] ?? kShowDebugInfo.defAsString, - ); - kShowNetworkUsage.value = kShowNetworkUsage.loadFrom( - data[kShowNetworkUsage.key] ?? kShowNetworkUsage.defAsString, - ); - } - - @override - Future save({bool notifyListeners = true}) async { - try { - await settings.write({ - kLayoutCyclePeriod.key: - kLayoutCyclePeriod.saveAs(kLayoutCyclePeriod.value), - kWakelock.key: kWakelock.saveAs(kWakelock.value), - kNotificationsEnabled.key: - kNotificationsEnabled.saveAs(kNotificationsEnabled.value), - kSnoozeNotificationsUntil.key: - kSnoozeNotificationsUntil.saveAs(kSnoozeNotificationsUntil.value), - kNotificationClickBehavior.key: - kNotificationClickBehavior.saveAs(kNotificationClickBehavior.value), - kAutomaticStreaming.key: - kAutomaticStreaming.saveAs(kAutomaticStreaming.value), - kStreamOnBackground.key: - kStreamOnBackground.saveAs(kStreamOnBackground.value), - kStreamingType.key: kStreamingType.saveAs(kStreamingType.value), - kRTSPProtocol.key: kRTSPProtocol.saveAs(kRTSPProtocol.value), - kRenderingQuality.key: - kRenderingQuality.saveAs(kRenderingQuality.value), - kVideoFit.key: kVideoFit.saveAs(kVideoFit.value), - kRefreshRate.key: kRefreshRate.saveAs(kRefreshRate.value), - kLateStreamBehavior.key: - kLateStreamBehavior.saveAs(kLateStreamBehavior.value), - kReloadTimedOutStreams.key: - kReloadTimedOutStreams.saveAs(kReloadTimedOutStreams.value), - kUseHardwareDecoding.key: - kUseHardwareDecoding.saveAs(kUseHardwareDecoding.value), - kDownloadOnMobileData.key: - kDownloadOnMobileData.saveAs(kDownloadOnMobileData.value), - kChooseLocationEveryTime.key: - kChooseLocationEveryTime.saveAs(kChooseLocationEveryTime.value), - kAllowAppCloseWhenDownloading.key: kAllowAppCloseWhenDownloading - .saveAs(kAllowAppCloseWhenDownloading.value), - kPictureInPicture.key: - kPictureInPicture.saveAs(kPictureInPicture.value), - kEventsSpeed.key: kEventsSpeed.saveAs(kEventsSpeed.value), - kEventsVolume.key: kEventsVolume.saveAs(kEventsVolume.value), - kShowDifferentColorsForEvents.key: kShowDifferentColorsForEvents - .saveAs(kShowDifferentColorsForEvents.value), - kPauseToBuffer.key: kPauseToBuffer.saveAs(kPauseToBuffer.value), - kTimelineInitialPoint.key: - kTimelineInitialPoint.saveAs(kTimelineInitialPoint.value), - kThemeMode.key: kThemeMode.saveAs(kThemeMode.value), - kLanguageCode.key: kLanguageCode.saveAs(kLanguageCode.value), - }); - } catch (e) { - debugPrint('Error saving settings: $e'); - } - super.save(notifyListeners: notifyListeners); - } -} diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 70844482..31ec31b8 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -108,7 +108,12 @@ class _SettingsOption { if (getDefault != null) serializedData ??= saveAs(await getDefault!()); serializedData ??= defAsString; - _value = loadFrom(serializedData); + try { + _value = loadFrom(serializedData); + } catch (e) { + debugPrint('Error loading data for $key: $e'); + _value = (await getDefault?.call()) ?? def; + } } }