From 523342781bfdc28bc1e1b35235e58c6a5bc9b0d9 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Sat, 30 Sep 2023 15:30:38 -0300 Subject: [PATCH 01/19] feat: split mobile and desktop settings --- lib/widgets/settings/desktop/appearance.dart | 16 + .../settings/desktop/date_language.dart | 16 + lib/widgets/settings/desktop/general.dart | 16 + lib/widgets/settings/desktop/server.dart | 16 + lib/widgets/settings/desktop/settings.dart | 75 ++++ lib/widgets/settings/desktop/updates.dart | 16 + .../settings/{ => mobile}/date_time.dart | 0 .../settings/{ => mobile}/server_tile.dart | 0 lib/widgets/settings/mobile/settings.dart | 345 +++++++++++++++++ lib/widgets/settings/{ => mobile}/update.dart | 0 lib/widgets/settings/settings.dart | 350 +----------------- 11 files changed, 513 insertions(+), 337 deletions(-) create mode 100644 lib/widgets/settings/desktop/appearance.dart create mode 100644 lib/widgets/settings/desktop/date_language.dart create mode 100644 lib/widgets/settings/desktop/general.dart create mode 100644 lib/widgets/settings/desktop/server.dart create mode 100644 lib/widgets/settings/desktop/settings.dart create mode 100644 lib/widgets/settings/desktop/updates.dart rename lib/widgets/settings/{ => mobile}/date_time.dart (100%) rename lib/widgets/settings/{ => mobile}/server_tile.dart (100%) create mode 100644 lib/widgets/settings/mobile/settings.dart rename lib/widgets/settings/{ => mobile}/update.dart (100%) diff --git a/lib/widgets/settings/desktop/appearance.dart b/lib/widgets/settings/desktop/appearance.dart new file mode 100644 index 00000000..b07a6785 --- /dev/null +++ b/lib/widgets/settings/desktop/appearance.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +class AppearanceSettings extends StatelessWidget { + const AppearanceSettings({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return ListView(children: [ + Text( + 'Appearance', + style: theme.textTheme.titleLarge, + ), + ]); + } +} diff --git a/lib/widgets/settings/desktop/date_language.dart b/lib/widgets/settings/desktop/date_language.dart new file mode 100644 index 00000000..dbf566f3 --- /dev/null +++ b/lib/widgets/settings/desktop/date_language.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +class LocalizationSettings extends StatelessWidget { + const LocalizationSettings({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return ListView(children: [ + Text( + 'Date and Language', + style: theme.textTheme.titleLarge, + ), + ]); + } +} diff --git a/lib/widgets/settings/desktop/general.dart b/lib/widgets/settings/desktop/general.dart new file mode 100644 index 00000000..0281d3ba --- /dev/null +++ b/lib/widgets/settings/desktop/general.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +class GeneralSettings extends StatelessWidget { + const GeneralSettings({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return ListView(children: [ + Text( + 'General', + style: theme.textTheme.titleLarge, + ), + ]); + } +} diff --git a/lib/widgets/settings/desktop/server.dart b/lib/widgets/settings/desktop/server.dart new file mode 100644 index 00000000..c63a6586 --- /dev/null +++ b/lib/widgets/settings/desktop/server.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +class ServerSettings extends StatelessWidget { + const ServerSettings({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return ListView(children: [ + Text( + 'Servers', + style: theme.textTheme.titleLarge, + ), + ]); + } +} diff --git a/lib/widgets/settings/desktop/settings.dart b/lib/widgets/settings/desktop/settings.dart new file mode 100644 index 00000000..cec21909 --- /dev/null +++ b/lib/widgets/settings/desktop/settings.dart @@ -0,0 +1,75 @@ +import 'package:bluecherry_client/widgets/settings/desktop/appearance.dart'; +import 'package:bluecherry_client/widgets/settings/desktop/date_language.dart'; +import 'package:bluecherry_client/widgets/settings/desktop/general.dart'; +import 'package:bluecherry_client/widgets/settings/desktop/server.dart'; +import 'package:bluecherry_client/widgets/settings/desktop/updates.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class DesktopSettings extends StatefulWidget { + const DesktopSettings({super.key}); + + @override + State createState() => _DesktopSettingsState(); +} + +class _DesktopSettingsState extends State { + int currentIndex = 0; + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + return Row(children: [ + NavigationRail( + destinations: [ + const NavigationRailDestination( + icon: Icon(Icons.dashboard), + label: Text('General'), + ), + NavigationRailDestination( + icon: const Icon(Icons.dns), + label: Text(loc.servers), + ), + const NavigationRailDestination( + icon: Icon(Icons.brightness_auto), + label: Text('Appearance'), + ), + NavigationRailDestination( + icon: const Icon(Icons.update), + label: Text(loc.updates), + ), + const NavigationRailDestination( + icon: Icon(Icons.language), + label: Text('Date and Language'), + ), + ], + selectedIndex: currentIndex, + onDestinationSelected: (index) => setState(() => currentIndex = index), + ), + Expanded( + child: Card( + margin: EdgeInsetsDirectional.zero, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadiusDirectional.only( + topStart: Radius.circular(12.0), + ), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: AnimatedSwitcher( + duration: kThemeChangeDuration, + child: switch (currentIndex) { + 0 => const GeneralSettings(), + 1 => const ServerSettings(), + 2 => const AppearanceSettings(), + 3 => const UpdatesSettings(), + 4 => const LocalizationSettings(), + _ => const GeneralSettings(), + }, + ), + ), + ), + ), + ]); + } +} diff --git a/lib/widgets/settings/desktop/updates.dart b/lib/widgets/settings/desktop/updates.dart new file mode 100644 index 00000000..0a080dc8 --- /dev/null +++ b/lib/widgets/settings/desktop/updates.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +class UpdatesSettings extends StatelessWidget { + const UpdatesSettings({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return ListView(children: [ + Text( + 'Updates', + style: theme.textTheme.titleLarge, + ), + ]); + } +} diff --git a/lib/widgets/settings/date_time.dart b/lib/widgets/settings/mobile/date_time.dart similarity index 100% rename from lib/widgets/settings/date_time.dart rename to lib/widgets/settings/mobile/date_time.dart diff --git a/lib/widgets/settings/server_tile.dart b/lib/widgets/settings/mobile/server_tile.dart similarity index 100% rename from lib/widgets/settings/server_tile.dart rename to lib/widgets/settings/mobile/server_tile.dart diff --git a/lib/widgets/settings/mobile/settings.dart b/lib/widgets/settings/mobile/settings.dart new file mode 100644 index 00000000..aa3a3d20 --- /dev/null +++ b/lib/widgets/settings/mobile/settings.dart @@ -0,0 +1,345 @@ +/* + * 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 'dart:io'; + +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:bluecherry_client/models/server.dart'; +import 'package:bluecherry_client/providers/home_provider.dart'; +import 'package:bluecherry_client/providers/server_provider.dart'; +import 'package:bluecherry_client/providers/settings_provider.dart'; +import 'package:bluecherry_client/providers/update_provider.dart'; +import 'package:bluecherry_client/utils/constants.dart'; +import 'package:bluecherry_client/utils/extensions.dart'; +import 'package:bluecherry_client/utils/methods.dart'; +import 'package:bluecherry_client/widgets/edit_server.dart'; +import 'package:bluecherry_client/widgets/misc.dart'; +import 'package:bluecherry_client/widgets/settings/mobile/update.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:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:unity_video_player/unity_video_player.dart'; +import 'package:url_launcher/url_launcher.dart'; + +part 'date_time.dart'; +part 'server_tile.dart'; + +class MobileSettings extends StatefulWidget { + const MobileSettings({super.key}); + + @override + State createState() => _MobileSettingsState(); +} + +class _MobileSettingsState extends State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + SettingsProvider.instance.reload(); + }); + } + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + final theme = Theme.of(context); + final settings = context.watch(); + final update = context.watch(); + final servers = context.watch(); + + return Material( + type: MaterialType.transparency, + child: SafeArea( + bottom: false, + child: Column(children: [ + if (isMobile) + AppBar( + leading: MaybeUnityDrawerButton(context), + title: Text(loc.settings), + ), + Expanded( + child: CustomScrollView(slivers: [ + SliverToBoxAdapter( + child: SubHeader( + loc.servers, + subtext: loc.nServers(servers.servers.length), + ), + ), + const SliverToBoxAdapter(child: ServersList()), + SliverToBoxAdapter( + child: SubHeader(loc.theme, subtext: loc.themeDescription), + ), + SliverList.list( + children: ThemeMode.values.map((e) { + return ListTile( + leading: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: Icon(switch (e) { + ThemeMode.system => Icons.brightness_auto, + ThemeMode.light => Icons.light_mode, + ThemeMode.dark => Icons.dark_mode, + }), + ), + onTap: () { + settings.themeMode = e; + }, + trailing: Radio( + value: e, + groupValue: settings.themeMode, + onChanged: (value) { + settings.themeMode = e; + }, + ), + title: Text(switch (e) { + ThemeMode.system => loc.system, + ThemeMode.light => loc.light, + ThemeMode.dark => loc.dark, + }), + ); + }).toList()), + if (update.isUpdatingSupported) ...[ + SliverToBoxAdapter( + child: SubHeader( + loc.updates, + subtext: loc.runningOn(() { + if (Platform.isLinux) { + return 'Linux ${update.linuxEnvironment}'; + } else if (Platform.isWindows) { + return 'Windows'; + } + + return defaultTargetPlatform.name; + }()), + ), + ), + const SliverToBoxAdapter(child: AppUpdateCard()), + const SliverToBoxAdapter(child: AppUpdateOptions()), + ], + SliverToBoxAdapter(child: SubHeader(loc.miscellaneous)), + SliverList.list(children: [ + CorrectedListTile( + iconData: Icons.notifications_paused, + onTap: () async { + if (settings.snoozedUntil.isAfter(DateTime.now())) { + settings.snoozedUntil = + SettingsProvider.defaultSnoozedUntil; + } else { + final timeOfDay = await showTimePicker( + context: context, + helpText: loc.snoozeNotificationsUntil.toUpperCase(), + initialTime: TimeOfDay.fromDateTime(DateTime.now()), + useRootNavigator: false, + ); + if (timeOfDay != null) { + settings.snoozedUntil = DateTime( + DateTime.now().year, + DateTime.now().month, + DateTime.now().day, + timeOfDay.hour, + timeOfDay.minute, + ); + } + } + }, + title: loc.snoozeNotifications, + height: 72.0, + subtitle: settings.snoozedUntil.isAfter(DateTime.now()) + ? loc.snoozedUntil( + [ + if (settings.snoozedUntil + .difference(DateTime.now()) > + const Duration(hours: 24)) + settings.formatDate(settings.snoozedUntil), + settings.formatTime(settings.snoozedUntil), + ].join(' '), + ) + : loc.notSnoozed, + ), + ExpansionTile( + leading: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.beenhere_rounded), + ), + title: Text(loc.notificationClickBehavior), + textColor: theme.textTheme.bodyLarge?.color, + subtitle: Text( + settings.notificationClickBehavior.locale(context), + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.textTheme.bodySmall?.color, + ), + ), + children: NotificationClickBehavior.values.map((behavior) { + return RadioListTile( + contentPadding: const EdgeInsetsDirectional.only( + start: 68.0, + end: 16.0, + ), + value: behavior, + groupValue: settings.notificationClickBehavior, + onChanged: (value) { + settings.notificationClickBehavior = behavior; + }, + secondary: Icon(behavior.icon), + controlAffinity: ListTileControlAffinity.trailing, + title: Padding( + padding: const EdgeInsetsDirectional.only(start: 16.0), + child: Text(behavior.locale(context)), + ), + ); + }).toList(), + ), + ExpansionTile( + leading: CircleAvatar( + backgroundColor: const Color.fromRGBO(0, 0, 0, 0), + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.fit_screen), + ), + 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, + ), + ), + children: UnityVideoFit.values.map((e) { + return RadioListTile( + contentPadding: const EdgeInsetsDirectional.only( + start: 68.0, + end: 16.0, + ), + value: e, + groupValue: settings.cameraViewFit, + onChanged: (value) { + 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(), + ), + CorrectedListTile( + iconData: Icons.folder, + trailing: Icons.navigate_next, + title: loc.downloadPath, + subtitle: settings.downloadsDirectory, + height: 72.0, + onTap: () async { + final selectedDirectory = + await FilePicker.platform.getDirectoryPath( + dialogTitle: loc.downloadPath, + initialDirectory: settings.downloadsDirectory, + lockParentWindow: true, + ); + + if (selectedDirectory != null) { + settings.downloadsDirectory = + Directory(selectedDirectory).path; + } + }, + ), + ExpansionTile( + leading: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.timelapse), + ), + title: Text(loc.cycleTogglePeriod), + textColor: theme.textTheme.bodyLarge?.color, + subtitle: Text( + settings.layoutCyclingTogglePeriod.humanReadable(context), + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.textTheme.bodySmall?.color, + ), + ), + children: [5, 10, 30, 60, 60 * 5].map((e) { + final dur = Duration(seconds: e); + return RadioListTile( + value: dur, + groupValue: settings.layoutCyclingTogglePeriod, + onChanged: (value) { + settings.layoutCyclingTogglePeriod = dur; + }, + secondary: const Icon(null), + controlAffinity: ListTileControlAffinity.trailing, + title: Padding( + padding: const EdgeInsetsDirectional.only(start: 16.0), + child: Text( + dur.humanReadable(context), + ), + ), + ); + }).toList(), + ), + ]), + const SliverToBoxAdapter(child: DateTimeSection()), + SliverToBoxAdapter(child: SubHeader(loc.about)), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 8.0), + Text(update.packageInfo.version), + const SizedBox(height: 8.0), + Text( + loc.versionText, + style: theme.textTheme.displayMedium, + ), + const SizedBox(height: 8.0), + MaterialButton( + onPressed: () { + launchUrl( + Uri.https('www.bluecherrydvr.com', '/'), + mode: LaunchMode.externalApplication, + ); + }, + padding: EdgeInsets.zero, + minWidth: 0.0, + child: Text( + loc.website, + semanticsLabel: 'www.bluecherrydvr.com', + style: TextStyle( + color: theme.colorScheme.primary, + ), + ), + ), + ], + ), + ), + ), + const SliverToBoxAdapter(child: SizedBox(height: 16.0)), + ]), + ), + ]), + ), + ); + } +} diff --git a/lib/widgets/settings/update.dart b/lib/widgets/settings/mobile/update.dart similarity index 100% rename from lib/widgets/settings/update.dart rename to lib/widgets/settings/mobile/update.dart diff --git a/lib/widgets/settings/settings.dart b/lib/widgets/settings/settings.dart index d569f002..26eb1845 100644 --- a/lib/widgets/settings/settings.dart +++ b/lib/widgets/settings/settings.dart @@ -1,350 +1,26 @@ -/* - * 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 'dart:io'; - -import 'package:auto_size_text/auto_size_text.dart'; -import 'package:bluecherry_client/models/server.dart'; -import 'package:bluecherry_client/providers/home_provider.dart'; -import 'package:bluecherry_client/providers/server_provider.dart'; -import 'package:bluecherry_client/providers/settings_provider.dart'; -import 'package:bluecherry_client/providers/update_provider.dart'; import 'package:bluecherry_client/utils/constants.dart'; -import 'package:bluecherry_client/utils/extensions.dart'; -import 'package:bluecherry_client/utils/methods.dart'; -import 'package:bluecherry_client/widgets/edit_server.dart'; -import 'package:bluecherry_client/widgets/misc.dart'; -import 'package:bluecherry_client/widgets/settings/update.dart'; -import 'package:file_picker/file_picker.dart'; -import 'package:flutter/foundation.dart'; +import 'package:bluecherry_client/widgets/settings/desktop/settings.dart'; +import 'package:bluecherry_client/widgets/settings/mobile/settings.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:intl/intl.dart'; -import 'package:provider/provider.dart'; -import 'package:unity_video_player/unity_video_player.dart'; -import 'package:url_launcher/url_launcher.dart'; - -part 'date_time.dart'; -part 'server_tile.dart'; -typedef ChangeTabCallback = void Function(int tab); - -class Settings extends StatefulWidget { +class Settings extends StatelessWidget { const Settings({super.key}); - @override - State createState() => _SettingsState(); -} - -class _SettingsState extends State { - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - SettingsProvider.instance.reload(); - }); - } - @override Widget build(BuildContext context) { - final loc = AppLocalizations.of(context); - final theme = Theme.of(context); - final settings = context.watch(); - final update = context.watch(); - final servers = context.watch(); + final hasDrawer = Scaffold.hasDrawer(context); return Material( type: MaterialType.transparency, - child: SafeArea( - bottom: false, - child: Column(children: [ - if (isMobile) - AppBar( - leading: MaybeUnityDrawerButton(context), - title: Text(loc.settings), - ), - Expanded( - child: CustomScrollView(slivers: [ - SliverToBoxAdapter( - child: SubHeader( - loc.servers, - subtext: loc.nServers(servers.servers.length), - ), - ), - const SliverToBoxAdapter(child: ServersList()), - SliverToBoxAdapter( - child: SubHeader( - loc.theme, - subtext: loc.themeDescription, - ), - ), - SliverList.list( - children: ThemeMode.values.map((e) { - return ListTile( - leading: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: Icon(switch (e) { - ThemeMode.system => Icons.brightness_auto, - ThemeMode.light => Icons.light_mode, - ThemeMode.dark => Icons.dark_mode, - }), - ), - onTap: () { - settings.themeMode = e; - }, - trailing: Radio( - value: e, - groupValue: settings.themeMode, - onChanged: (value) { - settings.themeMode = e; - }, - ), - title: Text(switch (e) { - ThemeMode.system => loc.system, - ThemeMode.light => loc.light, - ThemeMode.dark => loc.dark, - }), - ); - }).toList()), - if (update.isUpdatingSupported) ...[ - SliverToBoxAdapter( - child: SubHeader( - loc.updates, - subtext: loc.runningOn(() { - if (Platform.isLinux) { - return 'Linux ${update.linuxEnvironment}'; - } else if (Platform.isWindows) { - return 'Windows'; - } - - return defaultTargetPlatform.name; - }()), - ), - ), - const SliverToBoxAdapter(child: AppUpdateCard()), - const SliverToBoxAdapter(child: AppUpdateOptions()), - ], - SliverToBoxAdapter(child: SubHeader(loc.miscellaneous)), - SliverList.list(children: [ - CorrectedListTile( - iconData: Icons.notifications_paused, - onTap: () async { - if (settings.snoozedUntil.isAfter(DateTime.now())) { - settings.snoozedUntil = - SettingsProvider.defaultSnoozedUntil; - } else { - final timeOfDay = await showTimePicker( - context: context, - helpText: loc.snoozeNotificationsUntil.toUpperCase(), - initialTime: TimeOfDay.fromDateTime(DateTime.now()), - useRootNavigator: false, - ); - if (timeOfDay != null) { - settings.snoozedUntil = DateTime( - DateTime.now().year, - DateTime.now().month, - DateTime.now().day, - timeOfDay.hour, - timeOfDay.minute, - ); - } - } - }, - title: loc.snoozeNotifications, - height: 72.0, - subtitle: settings.snoozedUntil.isAfter(DateTime.now()) - ? loc.snoozedUntil( - [ - if (settings.snoozedUntil - .difference(DateTime.now()) > - const Duration(hours: 24)) - settings.formatDate(settings.snoozedUntil), - settings.formatTime(settings.snoozedUntil), - ].join(' '), - ) - : loc.notSnoozed, - ), - ExpansionTile( - leading: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.beenhere_rounded), - ), - title: Text(loc.notificationClickBehavior), - textColor: theme.textTheme.bodyLarge?.color, - subtitle: Text( - settings.notificationClickBehavior.locale(context), - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.textTheme.bodySmall?.color, - ), - ), - children: NotificationClickBehavior.values.map((behavior) { - return RadioListTile( - contentPadding: const EdgeInsetsDirectional.only( - start: 68.0, - end: 16.0, - ), - value: behavior, - groupValue: settings.notificationClickBehavior, - onChanged: (value) { - settings.notificationClickBehavior = behavior; - }, - secondary: Icon(behavior.icon), - controlAffinity: ListTileControlAffinity.trailing, - title: Padding( - padding: const EdgeInsetsDirectional.only(start: 16.0), - child: Text(behavior.locale(context)), - ), - ); - }).toList(), - ), - ExpansionTile( - leading: CircleAvatar( - backgroundColor: const Color.fromRGBO(0, 0, 0, 0), - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.fit_screen), - ), - 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, - ), - ), - children: UnityVideoFit.values.map((e) { - return RadioListTile( - contentPadding: const EdgeInsetsDirectional.only( - start: 68.0, - end: 16.0, - ), - value: e, - groupValue: settings.cameraViewFit, - onChanged: (value) { - 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(), - ), - CorrectedListTile( - iconData: Icons.folder, - trailing: Icons.navigate_next, - title: loc.downloadPath, - subtitle: settings.downloadsDirectory, - height: 72.0, - onTap: () async { - final selectedDirectory = - await FilePicker.platform.getDirectoryPath( - dialogTitle: loc.downloadPath, - initialDirectory: settings.downloadsDirectory, - lockParentWindow: true, - ); - - if (selectedDirectory != null) { - settings.downloadsDirectory = - Directory(selectedDirectory).path; - } - }, - ), - ExpansionTile( - leading: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.timelapse), - ), - title: Text(loc.cycleTogglePeriod), - textColor: theme.textTheme.bodyLarge?.color, - subtitle: Text( - settings.layoutCyclingTogglePeriod.humanReadable(context), - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.textTheme.bodySmall?.color, - ), - ), - children: [5, 10, 30, 60, 60 * 5].map((e) { - final dur = Duration(seconds: e); - return RadioListTile( - value: dur, - groupValue: settings.layoutCyclingTogglePeriod, - onChanged: (value) { - settings.layoutCyclingTogglePeriod = dur; - }, - secondary: const Icon(null), - controlAffinity: ListTileControlAffinity.trailing, - title: Padding( - padding: const EdgeInsetsDirectional.only(start: 16.0), - child: Text( - dur.humanReadable(context), - ), - ), - ); - }).toList(), - ), - ]), - const SliverToBoxAdapter(child: DateTimeSection()), - SliverToBoxAdapter(child: SubHeader(loc.about)), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 8.0), - Text(update.packageInfo.version), - const SizedBox(height: 8.0), - Text( - loc.versionText, - style: theme.textTheme.displayMedium, - ), - const SizedBox(height: 8.0), - MaterialButton( - onPressed: () { - launchUrl( - Uri.https('www.bluecherrydvr.com', '/'), - mode: LaunchMode.externalApplication, - ); - }, - padding: EdgeInsets.zero, - minWidth: 0.0, - child: Text( - loc.website, - semanticsLabel: 'www.bluecherrydvr.com', - style: TextStyle( - color: theme.colorScheme.primary, - ), - ), - ), - ], - ), - ), - ), - const SliverToBoxAdapter(child: SizedBox(height: 16.0)), - ]), - ), - ]), - ), + child: LayoutBuilder(builder: (context, consts) { + final width = consts.biggest.width; + + if (hasDrawer || width < kMobileBreakpoint.width) { + return const MobileSettings(); + } else { + return const DesktopSettings(); + } + }), ); } } From 5628eb13469f5a7b838a56cb216eb3da46853a32 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Sun, 1 Oct 2023 10:55:39 -0300 Subject: [PATCH 02/19] feat: desktop server tab --- lib/providers/settings_provider.dart | 55 +++++++ lib/utils/constants.dart | 3 + lib/widgets/settings/desktop/server.dart | 141 +++++++++++++++++- lib/widgets/settings/desktop/settings.dart | 32 ++-- lib/widgets/settings/mobile/settings.dart | 2 +- .../{mobile => shared}/server_tile.dart | 2 +- 6 files changed, 221 insertions(+), 14 deletions(-) rename lib/widgets/settings/{mobile => shared}/server_tile.dart (99%) diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index d148e5df..1654f3b7 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -45,6 +45,9 @@ class SettingsProvider extends ChangeNotifier { static const kDefaultLayoutCyclingTogglePeriod = Duration(seconds: 30); static Future get kDefaultDownloadsDirectory => DownloadsManager.kDefaultDownloadsDirectory; + static const kDefaultStreamingType = StreamingType.rtsp; + static const kDefaultRTSPProtocol = RTSPProtocol.tcp; + static const kDefaultVideoQuality = UnityVideoQuality.p480; // Getters. ThemeMode get themeMode => _themeMode; @@ -57,6 +60,9 @@ class SettingsProvider extends ChangeNotifier { String get downloadsDirectory => _downloadsDirectory; bool get layoutCyclingEnabled => _layoutCyclingEnabled; Duration get layoutCyclingTogglePeriod => _layoutCyclingTogglePeriod; + StreamingType get streamingType => _streamingType; + RTSPProtocol get rtspProtocol => _rtspProtocol; + UnityVideoQuality get videoQuality => _videoQuality; // Setters. set themeMode(ThemeMode value) { @@ -111,6 +117,21 @@ class SettingsProvider extends ChangeNotifier { _save(); } + set streamingType(StreamingType value) { + _streamingType = value; + _save(); + } + + set rtspProtocol(RTSPProtocol value) { + _rtspProtocol = value; + _save(); + } + + set videoQuality(UnityVideoQuality value) { + _videoQuality = value; + _save(); + } + late ThemeMode _themeMode; late DateFormat _dateFormat; late DateFormat _timeFormat; @@ -120,6 +141,9 @@ class SettingsProvider extends ChangeNotifier { late String _downloadsDirectory; late bool _layoutCyclingEnabled; late Duration _layoutCyclingTogglePeriod; + late StreamingType _streamingType; + late RTSPProtocol _rtspProtocol; + late UnityVideoQuality _videoQuality; /// Initializes the [SettingsProvider] instance & fetches state from `async` /// `package:hive` method-calls. Called before [runApp]. @@ -146,6 +170,9 @@ class SettingsProvider extends ChangeNotifier { kHiveDownloadsDirectorySetting: downloadsDirectory, kHiveLayoutCycling: layoutCyclingEnabled, kHiveLayoutCyclingPeriod: layoutCyclingTogglePeriod.inMilliseconds, + kHiveStreamingType: streamingType.index, + kHiveStreamingProtocol: rtspProtocol.index, + kHiveVideoQuality: videoQuality.index, }); if (notify) notifyListeners(); @@ -217,6 +244,24 @@ class SettingsProvider extends ChangeNotifier { _layoutCyclingTogglePeriod = kDefaultLayoutCyclingTogglePeriod; } + if (data.containsKey(kHiveStreamingType)) { + _streamingType = StreamingType.values[data[kHiveStreamingType]!]; + } else { + _streamingType = kDefaultStreamingType; + } + + if (data.containsKey(kHiveStreamingProtocol)) { + _rtspProtocol = RTSPProtocol.values[data[kHiveStreamingProtocol]!]; + } else { + _rtspProtocol = kDefaultRTSPProtocol; + } + + if (data.containsKey(kHiveVideoQuality)) { + _videoQuality = UnityVideoQuality.values[data[kHiveVideoQuality]!]; + } else { + _videoQuality = kDefaultVideoQuality; + } + notifyListeners(); } @@ -265,3 +310,13 @@ enum NotificationClickBehavior { }; } } + +enum StreamingType { + rtsp, + hls, +} + +enum RTSPProtocol { + tcp, + udp, +} diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index c1f5673a..691811d5 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -51,6 +51,9 @@ const kHiveLayoutCycling = 'layout_cycling'; const kHiveLayoutCyclingPeriod = 'layout_cycling_period'; const kHiveAutomaticUpdates = 'automatic_download_updates'; const kHiveLastCheck = 'last_update_check'; +const kHiveStreamingType = 'streaming_type'; +const kHiveStreamingProtocol = 'streaming_protocol'; +const kHiveVideoQuality = 'video_quality'; /// Used as frame buffer size in [DeviceTile], and calculating aspect ratio. Only relevant on desktop. const kDeviceTileWidth = 640.0; diff --git a/lib/widgets/settings/desktop/server.dart b/lib/widgets/settings/desktop/server.dart index c63a6586..55af5168 100644 --- a/lib/widgets/settings/desktop/server.dart +++ b/lib/widgets/settings/desktop/server.dart @@ -1,15 +1,152 @@ +import 'package:bluecherry_client/providers/settings_provider.dart'; +import 'package:bluecherry_client/utils/extensions.dart'; +import 'package:bluecherry_client/widgets/settings/mobile/settings.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:provider/provider.dart'; +import 'package:unity_video_player/unity_video_player.dart'; class ServerSettings extends StatelessWidget { const ServerSettings({super.key}); @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); final theme = Theme.of(context); return ListView(children: [ Text( - 'Servers', - style: theme.textTheme.titleLarge, + loc.servers, + style: theme.textTheme.titleMedium, + ), + const ServersList(), + Text( + 'Streaming Settings', + style: theme.textTheme.titleMedium, + ), + const SizedBox(height: 8.0), + const StreamingSettings(), + const SizedBox(height: 12.0), + Text( + 'Cameras Settings', + style: theme.textTheme.titleMedium, + ), + const SizedBox(height: 8.0), + const CamerasSettings(), + ]); + } +} + +class StreamingSettings extends StatelessWidget { + const StreamingSettings({super.key}); + + @override + Widget build(BuildContext context) { + final settings = context.watch(); + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Material( + borderRadius: BorderRadius.circular(6.0), + child: ListTile( + title: const Text('Streaming type'), + trailing: DropdownButton( + value: settings.streamingType, + onChanged: (v) { + if (v != null) { + settings.streamingType = v; + } + }, + items: StreamingType.values.map((q) { + return DropdownMenuItem( + value: q, + child: Text(q.name.toUpperCase()), + ); + }).toList(), + ), + ), + ), + const SizedBox(height: 8.0), + Material( + borderRadius: BorderRadius.circular(6.0), + child: ListTile( + enabled: settings.streamingType == StreamingType.rtsp, + title: const Text('RTSP protocol'), + trailing: DropdownButton( + value: settings.rtspProtocol, + onChanged: 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(), + ), + ), + ), + ]); + } +} + +class CamerasSettings extends StatelessWidget { + const CamerasSettings({super.key}); + + @override + Widget build(BuildContext context) { + final settings = context.watch(); + final loc = AppLocalizations.of(context); + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Material( + borderRadius: BorderRadius.circular(6.0), + child: ListTile( + title: const Text('Rendering quality'), + subtitle: const Text( + 'The quality of the video rendering. The higher the quality, the more resources it takes.', + ), + trailing: DropdownButton( + value: settings.videoQuality, + onChanged: (v) { + if (v != null) { + settings.videoQuality = v; + } + }, + items: UnityVideoQuality.values.map((q) { + return DropdownMenuItem( + value: q, + child: Text(q.locale(context)), + ); + }).toList(), + ), + ), + ), + const SizedBox(height: 8.0), + Material( + borderRadius: BorderRadius.circular(6.0), + child: ListTile( + title: Text(loc.cameraViewFit), + subtitle: const Text('The way the video is displayed in the view.'), + 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(), + ), + ), ), ]); } diff --git a/lib/widgets/settings/desktop/settings.dart b/lib/widgets/settings/desktop/settings.dart index cec21909..7de40be9 100644 --- a/lib/widgets/settings/desktop/settings.dart +++ b/lib/widgets/settings/desktop/settings.dart @@ -19,6 +19,8 @@ class _DesktopSettingsState extends State { @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context); + final theme = Theme.of(context); + return Row(children: [ NavigationRail( destinations: [ @@ -56,16 +58,26 @@ class _DesktopSettingsState extends State { ), child: Padding( padding: const EdgeInsets.all(16.0), - child: AnimatedSwitcher( - duration: kThemeChangeDuration, - child: switch (currentIndex) { - 0 => const GeneralSettings(), - 1 => const ServerSettings(), - 2 => const AppearanceSettings(), - 3 => const UpdatesSettings(), - 4 => const LocalizationSettings(), - _ => const GeneralSettings(), - }, + child: DropdownButtonHideUnderline( + child: Theme( + data: theme.copyWith( + colorScheme: theme.colorScheme.copyWith( + surface: theme.colorScheme.background, + background: theme.colorScheme.surface, + ), + ), + child: AnimatedSwitcher( + duration: kThemeChangeDuration, + child: switch (currentIndex) { + 0 => const GeneralSettings(), + 1 => const ServerSettings(), + 2 => const AppearanceSettings(), + 3 => const UpdatesSettings(), + 4 => const LocalizationSettings(), + _ => const GeneralSettings(), + }, + ), + ), ), ), ), diff --git a/lib/widgets/settings/mobile/settings.dart b/lib/widgets/settings/mobile/settings.dart index aa3a3d20..dc7c9b03 100644 --- a/lib/widgets/settings/mobile/settings.dart +++ b/lib/widgets/settings/mobile/settings.dart @@ -41,7 +41,7 @@ import 'package:unity_video_player/unity_video_player.dart'; import 'package:url_launcher/url_launcher.dart'; part 'date_time.dart'; -part 'server_tile.dart'; +part '../shared/server_tile.dart'; class MobileSettings extends StatefulWidget { const MobileSettings({super.key}); diff --git a/lib/widgets/settings/mobile/server_tile.dart b/lib/widgets/settings/shared/server_tile.dart similarity index 99% rename from lib/widgets/settings/mobile/server_tile.dart rename to lib/widgets/settings/shared/server_tile.dart index 141e64d2..a53e7ed1 100644 --- a/lib/widgets/settings/mobile/server_tile.dart +++ b/lib/widgets/settings/shared/server_tile.dart @@ -17,7 +17,7 @@ * along with this program. If not, see . */ -part of 'settings.dart'; +part of '../mobile/settings.dart'; typedef OnRemoveServer = void Function(BuildContext, Server); From 0fcc3365bb4a7b84a46480385f78bdb74fa8fa63 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Sun, 1 Oct 2023 11:00:04 -0300 Subject: [PATCH 03/19] chore: file headers --- lib/widgets/settings/desktop/appearance.dart | 19 +++++++++++++++++++ .../settings/desktop/date_language.dart | 19 +++++++++++++++++++ lib/widgets/settings/desktop/general.dart | 19 +++++++++++++++++++ lib/widgets/settings/desktop/server.dart | 19 +++++++++++++++++++ lib/widgets/settings/desktop/settings.dart | 19 +++++++++++++++++++ lib/widgets/settings/desktop/updates.dart | 19 +++++++++++++++++++ lib/widgets/settings/settings.dart | 19 +++++++++++++++++++ 7 files changed, 133 insertions(+) diff --git a/lib/widgets/settings/desktop/appearance.dart b/lib/widgets/settings/desktop/appearance.dart index b07a6785..d3ef1542 100644 --- a/lib/widgets/settings/desktop/appearance.dart +++ b/lib/widgets/settings/desktop/appearance.dart @@ -1,3 +1,22 @@ +/* + * 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:flutter/material.dart'; class AppearanceSettings extends StatelessWidget { diff --git a/lib/widgets/settings/desktop/date_language.dart b/lib/widgets/settings/desktop/date_language.dart index dbf566f3..6ab9b158 100644 --- a/lib/widgets/settings/desktop/date_language.dart +++ b/lib/widgets/settings/desktop/date_language.dart @@ -1,3 +1,22 @@ +/* + * 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:flutter/material.dart'; class LocalizationSettings extends StatelessWidget { diff --git a/lib/widgets/settings/desktop/general.dart b/lib/widgets/settings/desktop/general.dart index 0281d3ba..ddfbde0b 100644 --- a/lib/widgets/settings/desktop/general.dart +++ b/lib/widgets/settings/desktop/general.dart @@ -1,3 +1,22 @@ +/* + * 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:flutter/material.dart'; class GeneralSettings extends StatelessWidget { diff --git a/lib/widgets/settings/desktop/server.dart b/lib/widgets/settings/desktop/server.dart index 55af5168..a21c3603 100644 --- a/lib/widgets/settings/desktop/server.dart +++ b/lib/widgets/settings/desktop/server.dart @@ -1,3 +1,22 @@ +/* + * 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/utils/extensions.dart'; import 'package:bluecherry_client/widgets/settings/mobile/settings.dart'; diff --git a/lib/widgets/settings/desktop/settings.dart b/lib/widgets/settings/desktop/settings.dart index 7de40be9..19108163 100644 --- a/lib/widgets/settings/desktop/settings.dart +++ b/lib/widgets/settings/desktop/settings.dart @@ -1,3 +1,22 @@ +/* + * 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/widgets/settings/desktop/appearance.dart'; import 'package:bluecherry_client/widgets/settings/desktop/date_language.dart'; import 'package:bluecherry_client/widgets/settings/desktop/general.dart'; diff --git a/lib/widgets/settings/desktop/updates.dart b/lib/widgets/settings/desktop/updates.dart index 0a080dc8..dd31ebf4 100644 --- a/lib/widgets/settings/desktop/updates.dart +++ b/lib/widgets/settings/desktop/updates.dart @@ -1,3 +1,22 @@ +/* + * 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:flutter/material.dart'; class UpdatesSettings extends StatelessWidget { diff --git a/lib/widgets/settings/settings.dart b/lib/widgets/settings/settings.dart index 26eb1845..a784003d 100644 --- a/lib/widgets/settings/settings.dart +++ b/lib/widgets/settings/settings.dart @@ -1,3 +1,22 @@ +/* + * 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/utils/constants.dart'; import 'package:bluecherry_client/widgets/settings/desktop/settings.dart'; import 'package:bluecherry_client/widgets/settings/mobile/settings.dart'; From 5db717f5a6b0a26ca22c6c4de060bc463e86060b Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Sun, 1 Oct 2023 11:31:36 -0300 Subject: [PATCH 04/19] feat: localization settings --- lib/main.dart | 3 + lib/providers/settings_provider.dart | 13 +++ lib/utils/constants.dart | 1 + .../settings/desktop/date_language.dart | 92 ++++++++++++++++++- pubspec.lock | 8 ++ pubspec.yaml | 1 + 6 files changed, 114 insertions(+), 4 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index b0db212a..0b18e893 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -50,6 +50,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_localized_locales/flutter_localized_locales.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; import 'package:unity_video_player/unity_video_player.dart'; @@ -231,11 +232,13 @@ class _UnityAppState extends State with WidgetsBindingObserver { debugShowCheckedModeBanner: false, navigatorKey: navigatorKey, navigatorObservers: [NObserver()], + locale: settings.locale, localizationsDelegates: const [ AppLocalizations.delegate, GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + LocaleNamesLocalizationsDelegate(), ], supportedLocales: AppLocalizations.supportedLocales, themeMode: settings.themeMode, diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 1654f3b7..378998d3 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -50,6 +50,7 @@ class SettingsProvider extends ChangeNotifier { static const kDefaultVideoQuality = UnityVideoQuality.p480; // Getters. + Locale get locale => _locale; ThemeMode get themeMode => _themeMode; DateFormat get dateFormat => _dateFormat; DateFormat get timeFormat => _timeFormat; @@ -65,6 +66,11 @@ class SettingsProvider extends ChangeNotifier { UnityVideoQuality get videoQuality => _videoQuality; // Setters. + set locale(Locale value) { + _locale = value; + _save(); + } + set themeMode(ThemeMode value) { _themeMode = value; _save().then((_) { @@ -132,6 +138,7 @@ class SettingsProvider extends ChangeNotifier { _save(); } + late Locale _locale; late ThemeMode _themeMode; late DateFormat _dateFormat; late DateFormat _timeFormat; @@ -161,6 +168,7 @@ class SettingsProvider extends ChangeNotifier { Future _save({bool notify = true}) async { await settings.write({ + kHiveLocale: locale.toLanguageTag(), kHiveThemeMode: themeMode.index, kHiveDateFormat: dateFormat.pattern!, kHiveTimeFormat: timeFormat.pattern!, @@ -187,6 +195,11 @@ class SettingsProvider extends ChangeNotifier { // To circumvent this, we are closing all the existing opened [Hive] [Box]es and re-opening them again. This fetches the latest data. // Though, changes are still not instant. final data = await settings.read() as Map; + if (data.containsKey(kHiveLocale)) { + _locale = Locale(data[kHiveLocale]!); + } else { + _locale = Locale(Intl.getCurrentLocale()); + } if (data.containsKey(kHiveThemeMode)) { _themeMode = ThemeMode.values[data[kHiveThemeMode]!]; } else { diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index 691811d5..b611fecc 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -38,6 +38,7 @@ 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'; diff --git a/lib/widgets/settings/desktop/date_language.dart b/lib/widgets/settings/desktop/date_language.dart index 6ab9b158..ce63cfcc 100644 --- a/lib/widgets/settings/desktop/date_language.dart +++ b/lib/widgets/settings/desktop/date_language.dart @@ -17,7 +17,12 @@ * along with this program. If not, see . */ +import 'package:bluecherry_client/providers/settings_provider.dart'; +import 'package:bluecherry_client/widgets/settings/mobile/settings.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_localized_locales/flutter_localized_locales.dart'; +import 'package:provider/provider.dart'; class LocalizationSettings extends StatelessWidget { const LocalizationSettings({super.key}); @@ -25,11 +30,90 @@ class LocalizationSettings extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final loc = AppLocalizations.of(context); + return ListView(children: [ - Text( - 'Date and Language', - style: theme.textTheme.titleLarge, - ), + Text('Language', style: theme.textTheme.titleMedium), + const LanguageSection(), + const SizedBox(height: 12.0), + Text(loc.dateFormat, style: theme.textTheme.titleMedium), + const DateFormatSection(), + const SizedBox(height: 12.0), + Text(loc.timeFormat, style: theme.textTheme.titleMedium), + const TimeFormatSection(), ]); } } + +class LanguageSection extends StatelessWidget { + const LanguageSection({super.key}); + + @override + Widget build(BuildContext context) { + final settings = context.watch(); + final currentLocale = Localizations.localeOf(context); + const locales = AppLocalizations.supportedLocales; + + final names = LocaleNames.of(context)!; + + return LayoutBuilder(builder: (context, consts) { + if (consts.maxWidth >= 800) { + final crossAxisCount = consts.maxWidth >= 870 ? 4 : 3; + return Wrap( + children: locales.map((locale) { + final name = + names.nameOf(locale.toLanguageTag()) ?? locale.toLanguageTag(); + final nativeName = LocaleNamesLocalizationsDelegate + .nativeLocaleNames[locale.toLanguageTag()] ?? + locale.toLanguageTag(); + return SizedBox( + width: consts.maxWidth / crossAxisCount, + child: RadioListTile( + value: locale, + groupValue: currentLocale, + onChanged: (value) { + settings.locale = locale; + }, + controlAffinity: ListTileControlAffinity.trailing, + title: Text( + name, + maxLines: 1, + softWrap: false, + ), + subtitle: Text( + nativeName, + ), + ), + ); + }).toList(), + ); + } else { + return Column( + children: locales.map((locale) { + final name = + names.nameOf(locale.toLanguageTag()) ?? locale.toLanguageTag(); + final nativeName = LocaleNamesLocalizationsDelegate + .nativeLocaleNames[locale.toLanguageTag()] ?? + locale.toLanguageTag(); + return RadioListTile( + value: locale, + groupValue: currentLocale, + onChanged: (value) { + settings.locale = locale; + }, + controlAffinity: ListTileControlAffinity.trailing, + title: Padding( + padding: const EdgeInsetsDirectional.only(start: 8.0), + child: Text(name), + ), + subtitle: Padding( + padding: const EdgeInsetsDirectional.only(start: 8.0), + child: Text(nativeName), + ), + ); + }).toList(), + ); + } + }); + } +} diff --git a/pubspec.lock b/pubspec.lock index 849b50dc..85695598 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -291,6 +291,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_localized_locales: + dependency: "direct main" + description: + name: flutter_localized_locales + sha256: "478d10535edf07292e34cb4c757882edeeaf96d5e3dbb04b42733038bd41dd3f" + url: "https://pub.dev" + source: hosted + version: "2.0.5" flutter_plugin_android_lifecycle: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 11c0d037..7031c48f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,7 @@ dependencies: sliver_tools: ^0.2.12 intl: ^0.18.1 + flutter_localized_locales: ^2.0.5 duration: ^3.0.12 firebase_core: 2.10.0 firebase_messaging: ^14.4.1 From 77fb6366c43f3c543b3f274d0e62a83bce85ea05 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Sun, 1 Oct 2023 11:37:59 -0300 Subject: [PATCH 05/19] feat: updates section --- .../settings/desktop/date_language.dart | 1 - lib/widgets/settings/desktop/updates.dart | 30 +++++++++++- lib/widgets/settings/mobile/settings.dart | 36 +-------------- lib/widgets/settings/mobile/update.dart | 46 +++++++++++++++++++ 4 files changed, 75 insertions(+), 38 deletions(-) diff --git a/lib/widgets/settings/desktop/date_language.dart b/lib/widgets/settings/desktop/date_language.dart index ce63cfcc..aed7aefd 100644 --- a/lib/widgets/settings/desktop/date_language.dart +++ b/lib/widgets/settings/desktop/date_language.dart @@ -53,7 +53,6 @@ class LanguageSection extends StatelessWidget { final settings = context.watch(); final currentLocale = Localizations.localeOf(context); const locales = AppLocalizations.supportedLocales; - final names = LocaleNames.of(context)!; return LayoutBuilder(builder: (context, consts) { diff --git a/lib/widgets/settings/desktop/updates.dart b/lib/widgets/settings/desktop/updates.dart index dd31ebf4..4b09c0b1 100644 --- a/lib/widgets/settings/desktop/updates.dart +++ b/lib/widgets/settings/desktop/updates.dart @@ -17,7 +17,14 @@ * along with this program. If not, see . */ +import 'dart:io'; + +import 'package:bluecherry_client/providers/update_provider.dart'; +import 'package:bluecherry_client/widgets/settings/mobile/update.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}); @@ -25,11 +32,30 @@ class UpdatesSettings extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final loc = AppLocalizations.of(context); + final update = context.watch(); + return ListView(children: [ Text( - 'Updates', - style: theme.textTheme.titleLarge, + loc.updates, + style: theme.textTheme.titleMedium, + ), + Text( + loc.runningOn(() { + if (Platform.isLinux) { + return 'Linux ${update.linuxEnvironment}'; + } else if (Platform.isWindows) { + return 'Windows'; + } + + return defaultTargetPlatform.name; + }()), + style: theme.textTheme.labelSmall, ), + const AppUpdateCard(), + const AppUpdateOptions(), + const Divider(), + const About(), ]); } } diff --git a/lib/widgets/settings/mobile/settings.dart b/lib/widgets/settings/mobile/settings.dart index dc7c9b03..31cfbbdd 100644 --- a/lib/widgets/settings/mobile/settings.dart +++ b/lib/widgets/settings/mobile/settings.dart @@ -300,41 +300,7 @@ class _MobileSettingsState extends State { ]), const SliverToBoxAdapter(child: DateTimeSection()), SliverToBoxAdapter(child: SubHeader(loc.about)), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 8.0), - Text(update.packageInfo.version), - const SizedBox(height: 8.0), - Text( - loc.versionText, - style: theme.textTheme.displayMedium, - ), - const SizedBox(height: 8.0), - MaterialButton( - onPressed: () { - launchUrl( - Uri.https('www.bluecherrydvr.com', '/'), - mode: LaunchMode.externalApplication, - ); - }, - padding: EdgeInsets.zero, - minWidth: 0.0, - child: Text( - loc.website, - semanticsLabel: 'www.bluecherrydvr.com', - style: TextStyle( - color: theme.colorScheme.primary, - ), - ), - ), - ], - ), - ), - ), + const SliverToBoxAdapter(child: About()), const SliverToBoxAdapter(child: SizedBox(height: 16.0)), ]), ), diff --git a/lib/widgets/settings/mobile/update.dart b/lib/widgets/settings/mobile/update.dart index 821b90f7..54d7fbd5 100644 --- a/lib/widgets/settings/mobile/update.dart +++ b/lib/widgets/settings/mobile/update.dart @@ -25,6 +25,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/link.dart'; +import 'package:url_launcher/url_launcher.dart'; /// The card that displays the update information. class AppUpdateCard extends StatelessWidget { @@ -337,3 +338,48 @@ class AppUpdateOptions extends StatelessWidget { ); } } + +class About extends StatelessWidget { + const About({super.key}); + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + final theme = Theme.of(context); + final update = context.watch(); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 8.0), + Text(update.packageInfo.version), + const SizedBox(height: 8.0), + Text( + loc.versionText, + style: theme.textTheme.displayMedium, + ), + const SizedBox(height: 8.0), + MaterialButton( + onPressed: () { + launchUrl( + Uri.https('www.bluecherrydvr.com', '/'), + mode: LaunchMode.externalApplication, + ); + }, + padding: EdgeInsets.zero, + minWidth: 0.0, + child: Text( + loc.website, + semanticsLabel: 'www.bluecherrydvr.com', + style: TextStyle( + color: theme.colorScheme.primary, + ), + ), + ), + ], + ), + ); + } +} From 53450d74a8a4ee933d0cd9c9ed768fb0873db5fe Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Sun, 1 Oct 2023 12:26:49 -0300 Subject: [PATCH 06/19] feat: general settings --- lib/widgets/settings/desktop/appearance.dart | 35 ---- .../settings/desktop/date_language.dart | 18 +- lib/widgets/settings/desktop/general.dart | 178 +++++++++++++++++- lib/widgets/settings/desktop/server.dart | 41 ++-- lib/widgets/settings/desktop/settings.dart | 84 ++++----- lib/widgets/settings/desktop/updates.dart | 36 ++-- 6 files changed, 280 insertions(+), 112 deletions(-) delete mode 100644 lib/widgets/settings/desktop/appearance.dart diff --git a/lib/widgets/settings/desktop/appearance.dart b/lib/widgets/settings/desktop/appearance.dart deleted file mode 100644 index d3ef1542..00000000 --- a/lib/widgets/settings/desktop/appearance.dart +++ /dev/null @@ -1,35 +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:flutter/material.dart'; - -class AppearanceSettings extends StatelessWidget { - const AppearanceSettings({super.key}); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return ListView(children: [ - Text( - 'Appearance', - style: theme.textTheme.titleLarge, - ), - ]); - } -} diff --git a/lib/widgets/settings/desktop/date_language.dart b/lib/widgets/settings/desktop/date_language.dart index aed7aefd..908310c8 100644 --- a/lib/widgets/settings/desktop/date_language.dart +++ b/lib/widgets/settings/desktop/date_language.dart @@ -18,6 +18,7 @@ */ import 'package:bluecherry_client/providers/settings_provider.dart'; +import 'package:bluecherry_client/widgets/settings/desktop/settings.dart'; import 'package:bluecherry_client/widgets/settings/mobile/settings.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -32,14 +33,23 @@ class LocalizationSettings extends StatelessWidget { final theme = Theme.of(context); final loc = AppLocalizations.of(context); - return ListView(children: [ - Text('Language', style: theme.textTheme.titleMedium), + return ListView(padding: DesktopSettings.verticalPadding, children: [ + Padding( + padding: DesktopSettings.horizontalPadding, + child: Text('Language', style: theme.textTheme.titleMedium), + ), const LanguageSection(), const SizedBox(height: 12.0), - Text(loc.dateFormat, style: theme.textTheme.titleMedium), + Padding( + padding: DesktopSettings.horizontalPadding, + child: Text(loc.dateFormat, style: theme.textTheme.titleMedium), + ), const DateFormatSection(), const SizedBox(height: 12.0), - Text(loc.timeFormat, style: theme.textTheme.titleMedium), + Padding( + padding: DesktopSettings.horizontalPadding, + child: Text(loc.timeFormat, style: theme.textTheme.titleMedium), + ), const TimeFormatSection(), ]); } diff --git a/lib/widgets/settings/desktop/general.dart b/lib/widgets/settings/desktop/general.dart index ddfbde0b..981ead7a 100644 --- a/lib/widgets/settings/desktop/general.dart +++ b/lib/widgets/settings/desktop/general.dart @@ -17,7 +17,17 @@ * along with this program. If not, see . */ +import 'dart:io'; + +import 'package:bluecherry_client/providers/settings_provider.dart'; +import 'package:bluecherry_client/utils/extensions.dart'; +import 'package:bluecherry_client/widgets/misc.dart'; +import 'package:bluecherry_client/widgets/settings/desktop/settings.dart'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:provider/provider.dart'; +import 'package:unity_video_player/unity_video_player.dart'; class GeneralSettings extends StatelessWidget { const GeneralSettings({super.key}); @@ -25,10 +35,170 @@ class GeneralSettings extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); - return ListView(children: [ - Text( - 'General', - style: theme.textTheme.titleLarge, + final loc = AppLocalizations.of(context); + final settings = context.watch(); + return ListView(padding: DesktopSettings.verticalPadding, children: [ + Padding( + padding: DesktopSettings.horizontalPadding, + child: Text( + 'General', + style: theme.textTheme.titleLarge, + ), + ), + SubHeader( + loc.theme, + subtext: loc.themeDescription, + padding: DesktopSettings.horizontalPadding, + ), + ...ThemeMode.values.map((e) { + return ListTile( + leading: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: Icon(switch (e) { + ThemeMode.system => Icons.brightness_auto, + ThemeMode.light => Icons.light_mode, + ThemeMode.dark => Icons.dark_mode, + }), + ), + onTap: () { + settings.themeMode = e; + }, + trailing: Radio( + value: e, + groupValue: settings.themeMode, + onChanged: (value) { + settings.themeMode = e; + }, + ), + title: Text(switch (e) { + ThemeMode.system => loc.system, + ThemeMode.light => loc.light, + ThemeMode.dark => loc.dark, + }), + ); + }), + SubHeader(loc.miscellaneous, padding: DesktopSettings.horizontalPadding), + CorrectedListTile( + iconData: Icons.notifications_paused, + onTap: () async { + if (settings.snoozedUntil.isAfter(DateTime.now())) { + settings.snoozedUntil = SettingsProvider.defaultSnoozedUntil; + } else { + final timeOfDay = await showTimePicker( + context: context, + helpText: loc.snoozeNotificationsUntil.toUpperCase(), + initialTime: TimeOfDay.fromDateTime(DateTime.now()), + useRootNavigator: false, + ); + if (timeOfDay != null) { + settings.snoozedUntil = DateTime( + DateTime.now().year, + DateTime.now().month, + DateTime.now().day, + timeOfDay.hour, + timeOfDay.minute, + ); + } + } + }, + title: loc.snoozeNotifications, + height: 72.0, + subtitle: settings.snoozedUntil.isAfter(DateTime.now()) + ? loc.snoozedUntil( + [ + if (settings.snoozedUntil.difference(DateTime.now()) > + const Duration(hours: 24)) + settings.formatDate(settings.snoozedUntil), + settings.formatTime(settings.snoozedUntil), + ].join(' '), + ) + : loc.notSnoozed, + ), + ExpansionTile( + leading: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.beenhere_rounded), + ), + title: Text(loc.notificationClickBehavior), + textColor: theme.textTheme.bodyLarge?.color, + subtitle: Text( + settings.notificationClickBehavior.locale(context), + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.textTheme.bodySmall?.color, + ), + ), + children: NotificationClickBehavior.values.map((behavior) { + return RadioListTile( + contentPadding: const EdgeInsetsDirectional.only( + start: 68.0, + end: 16.0, + ), + value: behavior, + groupValue: settings.notificationClickBehavior, + onChanged: (value) { + settings.notificationClickBehavior = behavior; + }, + secondary: Icon(behavior.icon), + controlAffinity: ListTileControlAffinity.trailing, + title: Padding( + padding: const EdgeInsetsDirectional.only(start: 16.0), + child: Text(behavior.locale(context)), + ), + ); + }).toList(), + ), + CorrectedListTile( + iconData: Icons.folder, + trailing: Icons.navigate_next, + title: loc.downloadPath, + subtitle: settings.downloadsDirectory, + height: 72.0, + onTap: () async { + final selectedDirectory = await FilePicker.platform.getDirectoryPath( + dialogTitle: loc.downloadPath, + initialDirectory: settings.downloadsDirectory, + lockParentWindow: true, + ); + + if (selectedDirectory != null) { + settings.downloadsDirectory = Directory(selectedDirectory).path; + } + }, + ), + ExpansionTile( + leading: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.timelapse), + ), + title: Text(loc.cycleTogglePeriod), + textColor: theme.textTheme.bodyLarge?.color, + subtitle: Text( + settings.layoutCyclingTogglePeriod.humanReadable(context), + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.textTheme.bodySmall?.color, + ), + ), + children: [5, 10, 30, 60, 60 * 5].map((e) { + final dur = Duration(seconds: e); + return RadioListTile( + value: dur, + groupValue: settings.layoutCyclingTogglePeriod, + onChanged: (value) { + settings.layoutCyclingTogglePeriod = dur; + }, + secondary: const Icon(null), + controlAffinity: ListTileControlAffinity.trailing, + title: Padding( + padding: const EdgeInsetsDirectional.only(start: 16.0), + child: Text( + dur.humanReadable(context), + ), + ), + ); + }).toList(), ), ]); } diff --git a/lib/widgets/settings/desktop/server.dart b/lib/widgets/settings/desktop/server.dart index a21c3603..a97912ab 100644 --- a/lib/widgets/settings/desktop/server.dart +++ b/lib/widgets/settings/desktop/server.dart @@ -19,6 +19,7 @@ import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/utils/extensions.dart'; +import 'package:bluecherry_client/widgets/settings/desktop/settings.dart'; import 'package:bluecherry_client/widgets/settings/mobile/settings.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -32,25 +33,41 @@ class ServerSettings extends StatelessWidget { Widget build(BuildContext context) { final loc = AppLocalizations.of(context); final theme = Theme.of(context); - return ListView(children: [ - Text( - loc.servers, - style: theme.textTheme.titleMedium, + return ListView(padding: DesktopSettings.verticalPadding, children: [ + Padding( + padding: DesktopSettings.horizontalPadding, + child: Text( + loc.servers, + style: theme.textTheme.titleMedium, + ), ), const ServersList(), - Text( - 'Streaming Settings', - style: theme.textTheme.titleMedium, + const SizedBox(height: 8.0), + Padding( + padding: DesktopSettings.horizontalPadding, + child: Text( + 'Streaming Settings', + style: theme.textTheme.titleMedium, + ), ), const SizedBox(height: 8.0), - const StreamingSettings(), + const Padding( + padding: DesktopSettings.horizontalPadding, + child: StreamingSettings(), + ), const SizedBox(height: 12.0), - Text( - 'Cameras Settings', - style: theme.textTheme.titleMedium, + Padding( + padding: DesktopSettings.horizontalPadding, + child: Text( + 'Cameras Settings', + style: theme.textTheme.titleMedium, + ), ), const SizedBox(height: 8.0), - const CamerasSettings(), + const Padding( + padding: DesktopSettings.horizontalPadding, + child: CamerasSettings(), + ), ]); } } diff --git a/lib/widgets/settings/desktop/settings.dart b/lib/widgets/settings/desktop/settings.dart index 19108163..83983382 100644 --- a/lib/widgets/settings/desktop/settings.dart +++ b/lib/widgets/settings/desktop/settings.dart @@ -17,7 +17,7 @@ * along with this program. If not, see . */ -import 'package:bluecherry_client/widgets/settings/desktop/appearance.dart'; +import 'package:bluecherry_client/utils/constants.dart'; import 'package:bluecherry_client/widgets/settings/desktop/date_language.dart'; import 'package:bluecherry_client/widgets/settings/desktop/general.dart'; import 'package:bluecherry_client/widgets/settings/desktop/server.dart'; @@ -28,6 +28,9 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class DesktopSettings extends StatefulWidget { const DesktopSettings({super.key}); + static const horizontalPadding = EdgeInsets.symmetric(horizontal: 24.0); + static const verticalPadding = EdgeInsets.symmetric(vertical: 16.0); + @override State createState() => _DesktopSettingsState(); } @@ -40,43 +43,41 @@ class _DesktopSettingsState extends State { final loc = AppLocalizations.of(context); final theme = Theme.of(context); - return Row(children: [ - NavigationRail( - destinations: [ - const NavigationRailDestination( - icon: Icon(Icons.dashboard), - label: Text('General'), - ), - NavigationRailDestination( - icon: const Icon(Icons.dns), - label: Text(loc.servers), - ), - const NavigationRailDestination( - icon: Icon(Icons.brightness_auto), - label: Text('Appearance'), - ), - NavigationRailDestination( - icon: const Icon(Icons.update), - label: Text(loc.updates), - ), - const NavigationRailDestination( - icon: Icon(Icons.language), - label: Text('Date and Language'), - ), - ], - selectedIndex: currentIndex, - onDestinationSelected: (index) => setState(() => currentIndex = index), - ), - Expanded( - child: Card( - margin: EdgeInsetsDirectional.zero, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadiusDirectional.only( - topStart: Radius.circular(12.0), + return LayoutBuilder(builder: (context, constraints) { + return Row(children: [ + NavigationRail( + extended: constraints.maxWidth > + kMobileBreakpoint.width + kMobileBreakpoint.width / 4, + destinations: [ + const NavigationRailDestination( + icon: Icon(Icons.dashboard), + label: Text('General'), + ), + NavigationRailDestination( + icon: const Icon(Icons.dns), + label: Text(loc.servers), + ), + NavigationRailDestination( + icon: const Icon(Icons.update), + label: Text(loc.updates), + ), + const NavigationRailDestination( + icon: Icon(Icons.language), + label: Text('Date and Language'), + ), + ], + selectedIndex: currentIndex, + onDestinationSelected: (index) => + setState(() => currentIndex = index), + ), + Expanded( + child: Card( + margin: EdgeInsetsDirectional.zero, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadiusDirectional.only( + topStart: Radius.circular(12.0), + ), ), - ), - child: Padding( - padding: const EdgeInsets.all(16.0), child: DropdownButtonHideUnderline( child: Theme( data: theme.copyWith( @@ -90,9 +91,8 @@ class _DesktopSettingsState extends State { child: switch (currentIndex) { 0 => const GeneralSettings(), 1 => const ServerSettings(), - 2 => const AppearanceSettings(), - 3 => const UpdatesSettings(), - 4 => const LocalizationSettings(), + 2 => const UpdatesSettings(), + 3 => const LocalizationSettings(), _ => const GeneralSettings(), }, ), @@ -100,7 +100,7 @@ class _DesktopSettingsState extends State { ), ), ), - ), - ]); + ]); + }); } } diff --git a/lib/widgets/settings/desktop/updates.dart b/lib/widgets/settings/desktop/updates.dart index 4b09c0b1..1ea6ac5e 100644 --- a/lib/widgets/settings/desktop/updates.dart +++ b/lib/widgets/settings/desktop/updates.dart @@ -20,6 +20,7 @@ import 'dart:io'; import 'package:bluecherry_client/providers/update_provider.dart'; +import 'package:bluecherry_client/widgets/settings/desktop/settings.dart'; import 'package:bluecherry_client/widgets/settings/mobile/update.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -35,22 +36,27 @@ class UpdatesSettings extends StatelessWidget { final loc = AppLocalizations.of(context); final update = context.watch(); - return ListView(children: [ - Text( - loc.updates, - style: theme.textTheme.titleMedium, - ), - Text( - loc.runningOn(() { - if (Platform.isLinux) { - return 'Linux ${update.linuxEnvironment}'; - } else if (Platform.isWindows) { - return 'Windows'; - } + return ListView(padding: DesktopSettings.verticalPadding, children: [ + Padding( + padding: DesktopSettings.horizontalPadding, + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text( + loc.updates, + style: theme.textTheme.titleMedium, + ), + Text( + loc.runningOn(() { + if (Platform.isLinux) { + return 'Linux ${update.linuxEnvironment}'; + } else if (Platform.isWindows) { + return 'Windows'; + } - return defaultTargetPlatform.name; - }()), - style: theme.textTheme.labelSmall, + return defaultTargetPlatform.name; + }()), + style: theme.textTheme.labelSmall, + ), + ]), ), const AppUpdateCard(), const AppUpdateOptions(), From 5eac9ac2fab6cd72926a6845ce6a7fae89c3cb83 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Mon, 2 Oct 2023 09:07:52 -0300 Subject: [PATCH 07/19] ui: tweaks --- lib/providers/settings_provider.dart | 1 + lib/widgets/desktop_buttons.dart | 1 - .../settings/desktop/date_language.dart | 7 ++-- lib/widgets/settings/desktop/general.dart | 8 ---- lib/widgets/settings/desktop/settings.dart | 37 ++++++++++--------- lib/widgets/settings/mobile/date_time.dart | 8 ++-- 6 files changed, 29 insertions(+), 33 deletions(-) diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 378998d3..4a6bbd8d 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -327,6 +327,7 @@ enum NotificationClickBehavior { enum StreamingType { rtsp, hls, + mpeg, } enum RTSPProtocol { diff --git a/lib/widgets/desktop_buttons.dart b/lib/widgets/desktop_buttons.dart index e4e3f1b6..8435d178 100644 --- a/lib/widgets/desktop_buttons.dart +++ b/lib/widgets/desktop_buttons.dart @@ -129,7 +129,6 @@ class _WindowButtonsState extends State with WindowListener { final canPop = navigatorKey.currentState?.canPop() ?? false; return Material( - color: theme.appBarTheme.backgroundColor, child: Stack(children: [ DragToMoveArea( child: Row(children: [ diff --git a/lib/widgets/settings/desktop/date_language.dart b/lib/widgets/settings/desktop/date_language.dart index 908310c8..e454c583 100644 --- a/lib/widgets/settings/desktop/date_language.dart +++ b/lib/widgets/settings/desktop/date_language.dart @@ -32,22 +32,23 @@ class LocalizationSettings extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); final loc = AppLocalizations.of(context); + const horizontalPadding = EdgeInsetsDirectional.symmetric(horizontal: 16.0); return ListView(padding: DesktopSettings.verticalPadding, children: [ Padding( - padding: DesktopSettings.horizontalPadding, + padding: horizontalPadding, child: Text('Language', style: theme.textTheme.titleMedium), ), const LanguageSection(), const SizedBox(height: 12.0), Padding( - padding: DesktopSettings.horizontalPadding, + padding: horizontalPadding, child: Text(loc.dateFormat, style: theme.textTheme.titleMedium), ), const DateFormatSection(), const SizedBox(height: 12.0), Padding( - padding: DesktopSettings.horizontalPadding, + padding: horizontalPadding, child: Text(loc.timeFormat, style: theme.textTheme.titleMedium), ), const TimeFormatSection(), diff --git a/lib/widgets/settings/desktop/general.dart b/lib/widgets/settings/desktop/general.dart index 981ead7a..92c0ce64 100644 --- a/lib/widgets/settings/desktop/general.dart +++ b/lib/widgets/settings/desktop/general.dart @@ -27,7 +27,6 @@ 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'; -import 'package:unity_video_player/unity_video_player.dart'; class GeneralSettings extends StatelessWidget { const GeneralSettings({super.key}); @@ -38,13 +37,6 @@ class GeneralSettings extends StatelessWidget { final loc = AppLocalizations.of(context); final settings = context.watch(); return ListView(padding: DesktopSettings.verticalPadding, children: [ - Padding( - padding: DesktopSettings.horizontalPadding, - child: Text( - 'General', - style: theme.textTheme.titleLarge, - ), - ), SubHeader( loc.theme, subtext: loc.themeDescription, diff --git a/lib/widgets/settings/desktop/settings.dart b/lib/widgets/settings/desktop/settings.dart index 83983382..1a0376d1 100644 --- a/lib/widgets/settings/desktop/settings.dart +++ b/lib/widgets/settings/desktop/settings.dart @@ -79,23 +79,26 @@ class _DesktopSettingsState extends State { ), ), child: DropdownButtonHideUnderline( - child: Theme( - data: theme.copyWith( - colorScheme: theme.colorScheme.copyWith( - surface: theme.colorScheme.background, - background: theme.colorScheme.surface, - ), - ), - child: AnimatedSwitcher( - duration: kThemeChangeDuration, - child: switch (currentIndex) { - 0 => const GeneralSettings(), - 1 => const ServerSettings(), - 2 => const UpdatesSettings(), - 3 => const LocalizationSettings(), - _ => const GeneralSettings(), - }, - ), + child: AnimatedSwitcher( + duration: kThemeChangeDuration, + child: switch (currentIndex) { + 0 => const GeneralSettings(), + 1 => Theme( + data: theme.copyWith( + cardTheme: CardTheme( + color: ElevationOverlay.applySurfaceTint( + theme.colorScheme.background, + theme.colorScheme.surfaceTint, + 4, + ), + ), + ), + child: const ServerSettings(), + ), + 2 => const UpdatesSettings(), + 3 => const LocalizationSettings(), + _ => const GeneralSettings(), + }, ), ), ), diff --git a/lib/widgets/settings/mobile/date_time.dart b/lib/widgets/settings/mobile/date_time.dart index 6c3f1bcf..56490961 100644 --- a/lib/widgets/settings/mobile/date_time.dart +++ b/lib/widgets/settings/mobile/date_time.dart @@ -81,6 +81,7 @@ class DateFormatSection extends StatelessWidget { maxLines: 1, softWrap: false, ), + subtitle: Text(format.pattern ?? ''), ), ); }).toList(), @@ -101,6 +102,7 @@ class DateFormatSection extends StatelessWidget { format.format(DateTime.utc(1969, 7, 20, 14, 18, 04)), ), ), + subtitle: Text(format.pattern ?? ''), ); }).toList(), ); @@ -129,10 +131,8 @@ class TimeFormatSection extends StatelessWidget { groupValue: settings.timeFormat.pattern, onChanged: (value) => settings.timeFormat = format, ), - title: Padding( - padding: const EdgeInsetsDirectional.only(start: 8.0), - child: Text(format.format(date)), - ), + title: Text(format.format(date)), + subtitle: Text(format.pattern ?? ''), ); }).toList(), ); From 2f736e1050edea9494cdc21cc4ea80d0a0dafde0 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Mon, 2 Oct 2023 09:22:19 -0300 Subject: [PATCH 08/19] feat: rtsp protocol and streaming type settings effectively work --- lib/models/device.dart | 4 +-- lib/providers/settings_provider.dart | 36 +++++++++++++------ lib/utils/video_player.dart | 18 ++++++++-- .../device_grid/video_status_label.dart | 4 +-- lib/widgets/settings/desktop/server.dart | 7 ++-- .../lib/unity_video_player_main.dart | 6 ++++ ...unity_video_player_platform_interface.dart | 8 +++++ 7 files changed, 64 insertions(+), 19 deletions(-) diff --git a/lib/models/device.dart b/lib/models/device.dart index 7c8004a8..631abb06 100644 --- a/lib/models/device.dart +++ b/lib/models/device.dart @@ -82,7 +82,7 @@ class Device { /// If the app is running on the web, then HLS is used, otherwise RTSP is used. String get streamURL { if (kIsWeb) { - return hslURL; + return hlsURL; } else { return rtspURL; } @@ -113,7 +113,7 @@ class Device { ).toString(); } - String get hslURL { + String get hlsURL { return Uri( scheme: 'https', userInfo: '${Uri.encodeComponent(server.login)}' diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 4a6bbd8d..efbe1197 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -47,7 +47,7 @@ class SettingsProvider extends ChangeNotifier { DownloadsManager.kDefaultDownloadsDirectory; static const kDefaultStreamingType = StreamingType.rtsp; static const kDefaultRTSPProtocol = RTSPProtocol.tcp; - static const kDefaultVideoQuality = UnityVideoQuality.p480; + static const kDefaultVideoQuality = RenderingQuality.automatic; // Getters. Locale get locale => _locale; @@ -63,7 +63,7 @@ class SettingsProvider extends ChangeNotifier { Duration get layoutCyclingTogglePeriod => _layoutCyclingTogglePeriod; StreamingType get streamingType => _streamingType; RTSPProtocol get rtspProtocol => _rtspProtocol; - UnityVideoQuality get videoQuality => _videoQuality; + RenderingQuality get videoQuality => _videoQuality; // Setters. set locale(Locale value) { @@ -133,7 +133,7 @@ class SettingsProvider extends ChangeNotifier { _save(); } - set videoQuality(UnityVideoQuality value) { + set videoQuality(RenderingQuality value) { _videoQuality = value; _save(); } @@ -150,7 +150,7 @@ class SettingsProvider extends ChangeNotifier { late Duration _layoutCyclingTogglePeriod; late StreamingType _streamingType; late RTSPProtocol _rtspProtocol; - late UnityVideoQuality _videoQuality; + late RenderingQuality _videoQuality; /// Initializes the [SettingsProvider] instance & fetches state from `async` /// `package:hive` method-calls. Called before [runApp]. @@ -270,7 +270,7 @@ class SettingsProvider extends ChangeNotifier { } if (data.containsKey(kHiveVideoQuality)) { - _videoQuality = UnityVideoQuality.values[data[kHiveVideoQuality]!]; + _videoQuality = RenderingQuality.values[data[kHiveVideoQuality]!]; } else { _videoQuality = kDefaultVideoQuality; } @@ -324,13 +324,29 @@ enum NotificationClickBehavior { } } +enum RenderingQuality { + automatic, + p1080, + p720, + p480, + p360, + p240; + + String locale(BuildContext context) { + final loc = AppLocalizations.of(context); + return switch (this) { + RenderingQuality.p1080 => loc.p1080, + RenderingQuality.p720 => loc.p720, + RenderingQuality.p480 => loc.p480, + RenderingQuality.p360 => loc.p360, + RenderingQuality.p240 => loc.p240, + RenderingQuality.automatic => 'Automatic', + }; + } +} + enum StreamingType { rtsp, hls, mpeg, } - -enum RTSPProtocol { - tcp, - udp, -} diff --git a/lib/utils/video_player.dart b/lib/utils/video_player.dart index 744879f5..fcc1e308 100644 --- a/lib/utils/video_player.dart +++ b/lib/utils/video_player.dart @@ -18,6 +18,7 @@ */ import 'package:bluecherry_client/models/device.dart'; +import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:flutter/foundation.dart'; import 'package:unity_video_player/unity_video_player.dart'; @@ -37,10 +38,23 @@ class UnityPlayers with ChangeNotifier { /// Helper method to create a video player with required configuration for a [Device]. static UnityVideoPlayer forDevice(Device device) { debugPrint(device.streamURL); + final settings = SettingsProvider.instance; final controller = UnityVideoPlayer.create( - quality: UnityVideoQuality.qualityForResolutionY(device.resolutionY), + quality: switch (settings.videoQuality) { + RenderingQuality.p1080 => UnityVideoQuality.p1080, + RenderingQuality.p720 => UnityVideoQuality.p720, + RenderingQuality.p480 => UnityVideoQuality.p480, + RenderingQuality.p360 => UnityVideoQuality.p360, + RenderingQuality.p240 => UnityVideoQuality.p240, + RenderingQuality.automatic => + UnityVideoQuality.qualityForResolutionY(device.resolutionY), + }, ) - ..setDataSource(device.streamURL) + ..setDataSource(switch (settings.streamingType) { + StreamingType.rtsp => device.rtspURL, + StreamingType.hls => device.hlsURL, + StreamingType.mpeg => device.mjpegURL, + }) ..setVolume(0.0) ..setSpeed(1.0); diff --git a/lib/widgets/device_grid/video_status_label.dart b/lib/widgets/device_grid/video_status_label.dart index 96de7404..f170f965 100644 --- a/lib/widgets/device_grid/video_status_label.dart +++ b/lib/widgets/device_grid/video_status_label.dart @@ -57,10 +57,10 @@ class _VideoStatusLabelState extends State { String get _source => widget.video.player.dataSource!; bool get isLive => widget.video.player.dataSource != null && - // It is only LIVE if it starts with rtsp or is hsl + // It is only LIVE if it starts with rtsp or is hls (_source.startsWith('rtsp') || _source.contains('media/mjpeg.php') || - _source.endsWith('index.m3u8') /* hsl */); + _source.endsWith('index.m3u8') /* hls */); _VideoLabel get status => widget.video.error != null ? _VideoLabel.error diff --git a/lib/widgets/settings/desktop/server.dart b/lib/widgets/settings/desktop/server.dart index a97912ab..2c22166e 100644 --- a/lib/widgets/settings/desktop/server.dart +++ b/lib/widgets/settings/desktop/server.dart @@ -140,16 +140,17 @@ class CamerasSettings extends StatelessWidget { child: ListTile( title: const Text('Rendering quality'), subtitle: const Text( - 'The quality of the video rendering. The higher the quality, the more resources it takes.', + 'The quality of the video rendering. The higher the quality, the more resources it takes.' + '\nWhen automatic, the quality is selected based on the camera resolution.', ), - trailing: DropdownButton( + trailing: DropdownButton( value: settings.videoQuality, onChanged: (v) { if (v != null) { settings.videoQuality = v; } }, - items: UnityVideoQuality.values.map((q) { + items: RenderingQuality.values.map((q) { return DropdownMenuItem( value: q, child: Text(q.locale(context)), diff --git a/packages/unity_video_player/unity_video_player_main/lib/unity_video_player_main.dart b/packages/unity_video_player/unity_video_player_main/lib/unity_video_player_main.dart index 504ccf13..cf984a91 100644 --- a/packages/unity_video_player/unity_video_player_main/lib/unity_video_player_main.dart +++ b/packages/unity_video_player/unity_video_player_main/lib/unity_video_player_main.dart @@ -25,6 +25,7 @@ class UnityVideoPlayerMediaKitInterface extends UnityVideoPlayerInterface { int? width, int? height, bool enableCache = false, + RTSPProtocol? rtspProtocol, }) { final player = UnityVideoPlayerMediaKit( width: width, @@ -119,6 +120,7 @@ class UnityVideoPlayerMediaKit extends UnityVideoPlayer { int? width, int? height, bool enableCache = false, + RTSPProtocol? rtspProtocol, }) { final pixelRatio = PlatformDispatcher.instance.views.first.devicePixelRatio; if (width != null) width = (width * pixelRatio).toInt(); @@ -139,6 +141,10 @@ class UnityVideoPlayerMediaKit extends UnityVideoPlayer { _fpsStreamController.add(_fps); }); + if (rtspProtocol != null) { + platform.setProperty('rtsp-transport', rtspProtocol.name); + } + if (enableCache) { // https://mpv.io/manual/stable/#options-cache platform diff --git a/packages/unity_video_player/unity_video_player_platform_interface/lib/unity_video_player_platform_interface.dart b/packages/unity_video_player/unity_video_player_platform_interface/lib/unity_video_player_platform_interface.dart index 04eaee6b..0b1cf79d 100644 --- a/packages/unity_video_player/unity_video_player_platform_interface/lib/unity_video_player_platform_interface.dart +++ b/packages/unity_video_player/unity_video_player_platform_interface/lib/unity_video_player_platform_interface.dart @@ -29,6 +29,11 @@ enum UnityVideoFit { } } +enum RTSPProtocol { + tcp, + udp, +} + abstract class UnityVideoPlayerInterface extends PlatformInterface { UnityVideoPlayerInterface() : super(token: _token); @@ -53,6 +58,7 @@ abstract class UnityVideoPlayerInterface extends PlatformInterface { int? width, int? height, bool enableCache = false, + RTSPProtocol? rtspProtocol, }); /// Creates a video view @@ -325,11 +331,13 @@ abstract class UnityVideoPlayer { static UnityVideoPlayer create({ UnityVideoQuality quality = UnityVideoQuality.p360, bool enableCache = false, + RTSPProtocol? rtspProtocol, }) { return UnityVideoPlayerInterface.instance.createPlayer( width: quality.resolution.width.toInt(), height: quality.resolution.height.toInt(), enableCache: enableCache, + rtspProtocol: rtspProtocol, )..quality = quality; } From 9b1eb6d1e866267b999ff5b13b01021a75dd6b42 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Mon, 2 Oct 2023 09:40:55 -0300 Subject: [PATCH 09/19] feat: update settings localization --- lib/l10n/app_en.arb | 82 ++++++++++++------- .../settings/desktop/date_language.dart | 2 +- lib/widgets/settings/desktop/server.dart | 30 +++---- lib/widgets/settings/desktop/settings.dart | 12 +-- lib/widgets/settings/desktop/updates.dart | 4 +- lib/widgets/settings/mobile/settings.dart | 4 +- 6 files changed, 75 insertions(+), 59 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 0f16dfd1..05a75ee6 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -75,11 +75,6 @@ "fps": "FPS", "date": "Date", "lastUpdate": "Last Update", - "theme": "Theme", - "themeDescription": "Change the appearance of the app", - "system": "System", - "light": "Light", - "dark": "Dark", "screens": "Screens", "directCamera": "Direct Camera", "addServer": "Add Server", @@ -128,37 +123,16 @@ "no": "No", "about": "About", "versionText": "Copyright © 2022, Bluecherry LLC.\nAll rights reserved.", - "snooze15": "15 minutes", - "snooze30": "30 minutes", - "snooze60": "1 hour", - "miscellaneous": "Miscellaneous", - "snoozeNotifications": "Snooze Notifications", - "notSnoozed": "Not snoozing", - "snoozeNotificationsUntil": "Snooze notifications until", - "snoozedUntil": "Snoozed until {time}", - "@snoozedUntil": { - "placeholders": { - "time": { - "type": "String" - } - } - }, - "cameraViewFit": "Camera Image Fit", - "contain": "Contain", - "fill": "Fill", - "cover": "Cover", "gettingDevices": "Getting devices...", "noDevices": "No devices", "noEventsLoaded": "NO EVENTS LOADED", "noEventsLoadedTips": "• Select the cameras you want to see the events\n• Use the calendar to select a specific date or a date range \n• Use the \"Filter\" button to perform the search", "invalidResponse": "Invalid response received from the server", "cameraOptions": "Options", - "notificationClickBehavior": "Notification Click Behavior", "showFullscreenCamera": "Show in fullscreen", "openInANewWindow": "Open in a new window", "enableAudio": "Enable audio", "disableAudio": "Disable audio", - "showEventsScreen": "Show events history", "addNewServer": "Add new server", "disconnectServer": "Disconnect", "serverOptions": "Server options", @@ -212,6 +186,7 @@ "downloaded": "Downloaded", "downloading": "Downloading", "seeInDownloads": "See in Downloads", + "downloadPath": "Download directory", "delete": "Delete", "showInFiles": "Show in Files", "noDownloads": "You haven't downloaded anything yet :/", @@ -233,7 +208,6 @@ } } }, - "downloadPath": "Download directory", "playbackOptions": "PLAYBACK OPTIONS", "play": "Play", "pause": "Pause", @@ -366,10 +340,62 @@ } } }, + "windows": "Windows", + "linux": "Linux {env}", + "@linux": { + "placeholders": { + "env": { + "type": "String" + } + } + }, "@CURRENT TASKS": {}, "currentTasks": "Current tasks", "noCurrentTasks": "No tasks", "taskFetchingEvent": "Fetching events", "taskFetchingEventsPlayback": "Fetching events playback", - "taskDownloadingEvent": "Downloading event" + "taskDownloadingEvent": "Downloading event", + "@@@SETTINGS": {}, + "@@APPEARANCE": {}, + "theme": "Theme", + "themeDescription": "Change the appearance of the app", + "system": "System", + "light": "Light", + "dark": "Dark", + "@@MISC": {}, + "general": "General", + "miscellaneous": "Miscellaneous", + "@Snoozing": {}, + "snooze15": "15 minutes", + "snooze30": "30 minutes", + "snooze60": "1 hour", + "snoozeNotifications": "Snooze Notifications", + "notSnoozed": "Not snoozing", + "snoozeNotificationsUntil": "Snooze notifications until", + "snoozedUntil": "Snoozed until {time}", + "@snoozedUntil": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "@Notification click": {}, + "notificationClickBehavior": "Notification Click Behavior", + "showEventsScreen": "Show events history", + "@@STREAMING": {}, + "streamingSetings": "Streaming settings", + "streamingType": "Streaming type", + "rtspProtocol": "RTSP Protocol", + "camerasSettings": "Cameras settings", + "renderingQuality": "Rendering quality", + "renderingQualityDescription": "The quality of the video rendering. The higher the quality, the more resources it takes.\nWhen automatic, the quality is selected based on the camera resolution.", + "cameraViewFit": "Camera Image Fit", + "cameraViewFitDescription": "The way the video is displayed in the view.", + "contain": "Contain", + "fill": "Fill", + "cover": "Cover", + "@@LOCALIZATION": {}, + "dateLanguage": "Date and Language", + "language": "Language" } diff --git a/lib/widgets/settings/desktop/date_language.dart b/lib/widgets/settings/desktop/date_language.dart index e454c583..f961c0a1 100644 --- a/lib/widgets/settings/desktop/date_language.dart +++ b/lib/widgets/settings/desktop/date_language.dart @@ -37,7 +37,7 @@ class LocalizationSettings extends StatelessWidget { return ListView(padding: DesktopSettings.verticalPadding, children: [ Padding( padding: horizontalPadding, - child: Text('Language', style: theme.textTheme.titleMedium), + child: Text(loc.language, style: theme.textTheme.titleMedium), ), const LanguageSection(), const SizedBox(height: 12.0), diff --git a/lib/widgets/settings/desktop/server.dart b/lib/widgets/settings/desktop/server.dart index 2c22166e..7d6000c1 100644 --- a/lib/widgets/settings/desktop/server.dart +++ b/lib/widgets/settings/desktop/server.dart @@ -36,19 +36,13 @@ class ServerSettings extends StatelessWidget { return ListView(padding: DesktopSettings.verticalPadding, children: [ Padding( padding: DesktopSettings.horizontalPadding, - child: Text( - loc.servers, - style: theme.textTheme.titleMedium, - ), + child: Text(loc.servers, style: theme.textTheme.titleMedium), ), const ServersList(), const SizedBox(height: 8.0), Padding( padding: DesktopSettings.horizontalPadding, - child: Text( - 'Streaming Settings', - style: theme.textTheme.titleMedium, - ), + child: Text(loc.streamingSetings, style: theme.textTheme.titleMedium), ), const SizedBox(height: 8.0), const Padding( @@ -58,10 +52,7 @@ class ServerSettings extends StatelessWidget { const SizedBox(height: 12.0), Padding( padding: DesktopSettings.horizontalPadding, - child: Text( - 'Cameras Settings', - style: theme.textTheme.titleMedium, - ), + child: Text(loc.camerasSettings, style: theme.textTheme.titleMedium), ), const SizedBox(height: 8.0), const Padding( @@ -78,11 +69,13 @@ class StreamingSettings extends StatelessWidget { @override Widget build(BuildContext context) { final settings = context.watch(); + final loc = AppLocalizations.of(context); + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Material( borderRadius: BorderRadius.circular(6.0), child: ListTile( - title: const Text('Streaming type'), + title: Text(loc.streamingType), trailing: DropdownButton( value: settings.streamingType, onChanged: (v) { @@ -104,7 +97,7 @@ class StreamingSettings extends StatelessWidget { borderRadius: BorderRadius.circular(6.0), child: ListTile( enabled: settings.streamingType == StreamingType.rtsp, - title: const Text('RTSP protocol'), + title: Text(loc.rtspProtocol), trailing: DropdownButton( value: settings.rtspProtocol, onChanged: settings.streamingType == StreamingType.rtsp @@ -138,11 +131,8 @@ class CamerasSettings extends StatelessWidget { Material( borderRadius: BorderRadius.circular(6.0), child: ListTile( - title: const Text('Rendering quality'), - subtitle: const Text( - 'The quality of the video rendering. The higher the quality, the more resources it takes.' - '\nWhen automatic, the quality is selected based on the camera resolution.', - ), + title: Text(loc.renderingQuality), + subtitle: Text(loc.renderingQualityDescription), trailing: DropdownButton( value: settings.videoQuality, onChanged: (v) { @@ -164,7 +154,7 @@ class CamerasSettings extends StatelessWidget { borderRadius: BorderRadius.circular(6.0), child: ListTile( title: Text(loc.cameraViewFit), - subtitle: const Text('The way the video is displayed in the view.'), + subtitle: Text(loc.cameraViewFitDescription), trailing: DropdownButton( value: settings.cameraViewFit, onChanged: (v) { diff --git a/lib/widgets/settings/desktop/settings.dart b/lib/widgets/settings/desktop/settings.dart index 1a0376d1..19f93a3c 100644 --- a/lib/widgets/settings/desktop/settings.dart +++ b/lib/widgets/settings/desktop/settings.dart @@ -49,9 +49,9 @@ class _DesktopSettingsState extends State { extended: constraints.maxWidth > kMobileBreakpoint.width + kMobileBreakpoint.width / 4, destinations: [ - const NavigationRailDestination( - icon: Icon(Icons.dashboard), - label: Text('General'), + NavigationRailDestination( + icon: const Icon(Icons.dashboard), + label: Text(loc.general), ), NavigationRailDestination( icon: const Icon(Icons.dns), @@ -61,9 +61,9 @@ class _DesktopSettingsState extends State { icon: const Icon(Icons.update), label: Text(loc.updates), ), - const NavigationRailDestination( - icon: Icon(Icons.language), - label: Text('Date and Language'), + NavigationRailDestination( + icon: const Icon(Icons.language), + label: Text(loc.dateLanguage), ), ], selectedIndex: currentIndex, diff --git a/lib/widgets/settings/desktop/updates.dart b/lib/widgets/settings/desktop/updates.dart index 1ea6ac5e..e16faf81 100644 --- a/lib/widgets/settings/desktop/updates.dart +++ b/lib/widgets/settings/desktop/updates.dart @@ -47,9 +47,9 @@ class UpdatesSettings extends StatelessWidget { Text( loc.runningOn(() { if (Platform.isLinux) { - return 'Linux ${update.linuxEnvironment}'; + return loc.linux(update.linuxEnvironment ?? ''); } else if (Platform.isWindows) { - return 'Windows'; + return loc.windows; } return defaultTargetPlatform.name; diff --git a/lib/widgets/settings/mobile/settings.dart b/lib/widgets/settings/mobile/settings.dart index 31cfbbdd..c75f180c 100644 --- a/lib/widgets/settings/mobile/settings.dart +++ b/lib/widgets/settings/mobile/settings.dart @@ -124,9 +124,9 @@ class _MobileSettingsState extends State { loc.updates, subtext: loc.runningOn(() { if (Platform.isLinux) { - return 'Linux ${update.linuxEnvironment}'; + return loc.linux(update.linuxEnvironment ?? ''); } else if (Platform.isWindows) { - return 'Windows'; + return loc.windows; } return defaultTargetPlatform.name; From 44e5756d6cb94b6b2c43d8d9a167911a6c3046fc Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Mon, 2 Oct 2023 10:06:25 -0300 Subject: [PATCH 10/19] fix: update version is correctly managed --- lib/providers/update_provider.dart | 9 ++--- lib/widgets/settings/desktop/settings.dart | 38 +++++++++++----------- lib/widgets/settings/mobile/date_time.dart | 7 ++-- lib/widgets/settings/mobile/settings.dart | 36 ++++++++++---------- lib/widgets/settings/mobile/update.dart | 15 +++++---- 5 files changed, 53 insertions(+), 52 deletions(-) diff --git a/lib/providers/update_provider.dart b/lib/providers/update_provider.dart index 5cea2075..97d08128 100644 --- a/lib/providers/update_provider.dart +++ b/lib/providers/update_provider.dart @@ -384,7 +384,7 @@ class UpdateManager extends ChangeNotifier { return; } - final versions = []; + var versions = []; final doc = XmlDocument.parse(response.body); for (final item in doc.findAllElements('item')) { late String version; @@ -410,9 +410,10 @@ class UpdateManager extends ChangeNotifier { publishedAt: publishedAt, )); } - versions.sort( - (a, b) => Version.parse(a.version).compareTo(Version.parse(b.version)), - ); + // versions.sort( + // (a, b) => a.publishedAt.compareTo(b.publishedAt), + // ); + versions = versions.reversed.toList(); if (versions != this.versions) this.versions = versions; diff --git a/lib/widgets/settings/desktop/settings.dart b/lib/widgets/settings/desktop/settings.dart index 19f93a3c..d301b0e3 100644 --- a/lib/widgets/settings/desktop/settings.dart +++ b/lib/widgets/settings/desktop/settings.dart @@ -79,26 +79,26 @@ class _DesktopSettingsState extends State { ), ), child: DropdownButtonHideUnderline( - child: AnimatedSwitcher( - duration: kThemeChangeDuration, - child: switch (currentIndex) { - 0 => const GeneralSettings(), - 1 => Theme( - data: theme.copyWith( - cardTheme: CardTheme( - color: ElevationOverlay.applySurfaceTint( - theme.colorScheme.background, - theme.colorScheme.surfaceTint, - 4, - ), - ), - ), - child: const ServerSettings(), + child: Theme( + data: theme.copyWith( + cardTheme: CardTheme( + color: ElevationOverlay.applySurfaceTint( + theme.colorScheme.background, + theme.colorScheme.surfaceTint, + 4, ), - 2 => const UpdatesSettings(), - 3 => const LocalizationSettings(), - _ => const GeneralSettings(), - }, + ), + ), + child: AnimatedSwitcher( + duration: kThemeChangeDuration, + child: switch (currentIndex) { + 0 => const GeneralSettings(), + 1 => const ServerSettings(), + 2 => const UpdatesSettings(), + 3 => const LocalizationSettings(), + _ => const GeneralSettings(), + }, + ), ), ), ), diff --git a/lib/widgets/settings/mobile/date_time.dart b/lib/widgets/settings/mobile/date_time.dart index 56490961..d055a4bc 100644 --- a/lib/widgets/settings/mobile/date_time.dart +++ b/lib/widgets/settings/mobile/date_time.dart @@ -96,11 +96,8 @@ class DateFormatSection extends StatelessWidget { settings.dateFormat = format; }, controlAffinity: ListTileControlAffinity.trailing, - title: Padding( - padding: const EdgeInsetsDirectional.only(start: 8.0), - child: Text( - format.format(DateTime.utc(1969, 7, 20, 14, 18, 04)), - ), + title: Text( + format.format(DateTime.utc(1969, 7, 20, 14, 18, 04)), ), subtitle: Text(format.pattern ?? ''), ); diff --git a/lib/widgets/settings/mobile/settings.dart b/lib/widgets/settings/mobile/settings.dart index c75f180c..7102f4bd 100644 --- a/lib/widgets/settings/mobile/settings.dart +++ b/lib/widgets/settings/mobile/settings.dart @@ -118,24 +118,6 @@ class _MobileSettingsState extends State { }), ); }).toList()), - if (update.isUpdatingSupported) ...[ - SliverToBoxAdapter( - child: SubHeader( - loc.updates, - subtext: loc.runningOn(() { - if (Platform.isLinux) { - return loc.linux(update.linuxEnvironment ?? ''); - } else if (Platform.isWindows) { - return loc.windows; - } - - return defaultTargetPlatform.name; - }()), - ), - ), - const SliverToBoxAdapter(child: AppUpdateCard()), - const SliverToBoxAdapter(child: AppUpdateOptions()), - ], SliverToBoxAdapter(child: SubHeader(loc.miscellaneous)), SliverList.list(children: [ CorrectedListTile( @@ -299,6 +281,24 @@ class _MobileSettingsState extends State { ), ]), const SliverToBoxAdapter(child: DateTimeSection()), + if (update.isUpdatingSupported) ...[ + SliverToBoxAdapter( + child: SubHeader( + loc.updates, + subtext: loc.runningOn(() { + if (Platform.isLinux) { + return loc.linux(update.linuxEnvironment ?? ''); + } else if (Platform.isWindows) { + return loc.windows; + } + + return defaultTargetPlatform.name; + }()), + ), + ), + const SliverToBoxAdapter(child: AppUpdateCard()), + const SliverToBoxAdapter(child: AppUpdateOptions()), + ], SliverToBoxAdapter(child: SubHeader(loc.about)), const SliverToBoxAdapter(child: About()), const SliverToBoxAdapter(child: SizedBox(height: 16.0)), diff --git a/lib/widgets/settings/mobile/update.dart b/lib/widgets/settings/mobile/update.dart index 54d7fbd5..26810e18 100644 --- a/lib/widgets/settings/mobile/update.dart +++ b/lib/widgets/settings/mobile/update.dart @@ -19,6 +19,7 @@ import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/providers/update_provider.dart'; +import 'package:bluecherry_client/widgets/settings/desktop/settings.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -41,9 +42,10 @@ class AppUpdateCard extends StatelessWidget { if (update.hasUpdateAvailable) { final executable = update.executableFor(update.latestVersion!.version); return Card( - margin: const EdgeInsetsDirectional.only( - start: 10.0, - end: 10.0, + margin: EdgeInsetsDirectional.only( + top: 8.0, + start: DesktopSettings.horizontalPadding.left, + end: DesktopSettings.horizontalPadding.right, bottom: 6.0, ), child: Padding( @@ -107,10 +109,11 @@ class AppUpdateCard extends StatelessWidget { ); } else { return Card( - margin: const EdgeInsetsDirectional.only( - start: 10.0, - end: 10.0, + margin: EdgeInsetsDirectional.only( + top: 8.0, bottom: 6.0, + start: DesktopSettings.horizontalPadding.left, + end: DesktopSettings.horizontalPadding.right, ), child: Padding( padding: const EdgeInsets.all(8.0), From a900391971c41c0e157c66a53e4699ea0635e4aa Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Mon, 2 Oct 2023 10:22:23 -0300 Subject: [PATCH 11/19] feat: l10n organizer --- bin/l10n_organizer.dart | 27 +++ lib/l10n/app_en.arb | 10 +- lib/l10n/app_fr.arb | 188 +++++++++++++++--- lib/l10n/app_pl.arb | 92 ++++++--- .../device_grid/desktop/layout_manager.dart | 2 +- 5 files changed, 259 insertions(+), 60 deletions(-) create mode 100644 bin/l10n_organizer.dart diff --git a/bin/l10n_organizer.dart b/bin/l10n_organizer.dart new file mode 100644 index 00000000..c6b03089 --- /dev/null +++ b/bin/l10n_organizer.dart @@ -0,0 +1,27 @@ +import 'dart:convert'; +import 'dart:io'; + +void main() { + final files = Directory('${Directory.current.path}/lib/l10n') + .listSync() + .whereType(); + final mirrorFile = File('${Directory.current.path}/lib/l10n/app_en.arb'); + + final mirrorContent = mirrorFile.readAsStringSync(); + final mirrorMap = Map.from(json.decode(mirrorContent)); + + for (final file in files) { + if (file.path == mirrorFile.path) continue; + + final content = file.readAsStringSync(); + final contentMap = Map.from(json.decode(content)); + + final newContentMap = { + for (final key in mirrorMap.keys) key: contentMap[key] ?? mirrorMap[key], + }; + + final newContent = + const JsonEncoder.withIndent(' ').convert(newContentMap); + file.writeAsString(newContent); + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 05a75ee6..b07d8126 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -156,6 +156,14 @@ }, "newLayout": "New layout", "editLayout": "Edit layout", + "editSpecificLayout": "Edit {layoutName}", + "@editSpecificLayout": { + "placeholders": { + "layoutName": { + "type": "String" + } + } + }, "exportLayout": "Export layout", "importLayout": "Import layout", "failedToImportMessage": "While attempting to import {layoutName}, we found a device that is connected to a server you are not connected to. Please, connect to the server and try again.\nServer: {server_ip}:{server_port}", @@ -398,4 +406,4 @@ "@@LOCALIZATION": {}, "dateLanguage": "Date and Language", "language": "Language" -} +} \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index d7e749f0..f285d07b 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -39,29 +39,38 @@ "serverName": {} } }, + "serverNotAddedErrorDescription": "Please check the entered details and ensure the server is online.\n\nIf you are connecting remote, make sure the 7001 and 7002 ports are open to the Bluecherry server!", "noServersAvailable": "Aucun serveur disponible", "error": "Erreur", "ok": "OK", + "retry": "Retry", "removeCamera": "Enlever caméra", "replaceCamera": "Remplacer caméra", "reloadCamera": "Recharger caméra", "selectACamera": "Sélectionner une caméra", + "switchCamera": "Switch camera", "online": "En ligne", "offline": "Hors ligne", + "live": "LIVE", + "timedOut": "TIMED OUT", + "loading": "LOADING", + "recorded": "RECORDED", "removeFromView": "Retirer de la vue", "addToView": "Ajouter à la vue", + "addAllToView": "Add all to view", "eventBrowser": "Historique d'événements", + "eventsTimeline": "Timeline of Events", "server": "Serveur", "device": "Appareil", "event": "Évènement", "duration": "Durée", "priority": "Priorité", + "next": "Next", + "previous": "Previous", + "lastImageUpdate": "Last Image Update", + "fps": "FPS", "date": "Date", "lastUpdate": "Dernière mise à jour", - "theme": "Thème", - "system": "Système", - "light": "Clair", - "dark": "Sombre", "screens": "Écrans", "directCamera": "Caméra direct", "addServer": "Ajouter serveur", @@ -76,6 +85,15 @@ }, "pressBackAgainToExit": "Appuyez sur le bouton retour de nouveau pour quitter", "servers": "Serveurs", + "nServers": "{n, plural, =0{No servers} =1{1 server} other{{n} servers}}", + "@nServers": { + "placeholders": { + "n": { + "type": "int", + "example": "1" + } + } + }, "dateFormat": "Format de la date", "timeFormat": "Format de l'heure", "nDevices": "{n} appareils", @@ -93,35 +111,18 @@ }, "yes": "Oui", "no": "Non", - "version": "Version", + "about": "About", "versionText": "Copyright © 2022, Bluecherry LLC.\nTout droit réservé.", - "snooze15": "15 minutes", - "snooze30": "30 minutes", - "snooze60": "1 heure", - "miscellaneous": "Divers", - "snoozeNotifications": "Mise en pause des notifications", - "notSnoozed": "Notifications actives", - "snoozeNotificationsUntil": "Notifications en pause jusqu'à", - "snoozedUntil": "Mis en pause jusqu'à {time}", - "@snoozedUntil": { - "placeholders": { - "time": {} - } - }, - "cameraViewFit": "Ajustement de la vue caméra", - "contain": "Contenir", - "fill": "Remplir", - "cover": "Couvrir", "gettingDevices": "Obtention des appareils...", "noDevices": "Aucun appareil", - "noEventsFound": "Aucun événement trouvé", + "noEventsLoaded": "NO EVENTS LOADED", + "noEventsLoadedTips": "• Select the cameras you want to see the events\n• Use the calendar to select a specific date or a date range \n• Use the \"Filter\" button to perform the search", "invalidResponse": "Réponse invalide reçu du serveur", - "notificationClickBehavior": "Action de clic sur les notifications", + "cameraOptions": "Options", "showFullscreenCamera": "Montrer en plein écran", "openInANewWindow": "Ouvrir dans une nouvelle fenêtre", "enableAudio": "Activer l'audio", "disableAudio": "Désactiver l'audio", - "showEventsScreen": "Montrer l'historique d'événements", "addNewServer": "Ajouter un nouveau serveur", "disconnectServer": "Déconnecter", "serverOptions": "Options serveur", @@ -132,22 +133,62 @@ "refreshServer": "Actualiser le serveur", "refresh": "Actualiser", "view": "Vue", + "@Layouts": {}, "cycle": "Cycle", "cycleTogglePeriod": "Période de basculement de cycle", + "fallbackLayoutName": "Layout {layout}", + "@fallbackLayoutName": { + "placeholders": { + "layout": { + "type": "int" + } + } + }, "newLayout": "Nouvelle disposition", + "editLayout": "Edit layout", + "editSpecificLayout": "Edit {layoutName}", + "@editSpecificLayout": { + "placeholders": { + "layoutName": { + "type": "String" + } + } + }, + "exportLayout": "Export layout", + "importLayout": "Import layout", + "failedToImportMessage": "While attempting to import {layoutName}, we found a device that is connected to a server you are not connected to. Please, connect to the server and try again.\nServer: {server_ip}:{server_port}", + "@failedToImportMessage": { + "placeholders": { + "layoutName": { + "type": "String" + }, + "server_ip": { + "type": "String" + }, + "server_port": { + "type": "int" + } + } + }, + "layoutImportFileCorrupted": "The file you attempted to import is corrupted or missing information.", + "layoutImportFileCorruptedWithMessage": "The file you attempted to import is corrupted or missing information: \"{message}\"", "singleView": "Vue unique", "multipleView": "Vue multiple", "compactView": "Vue compacte", "createNewLayout": "Créer une nouvelle disposition", "layoutNameHint": "Nom de la disposition", "layoutTypeHint": "Type de disposition", + "@Downloads": {}, "downloads": "Téléchargements", "download": "Télécharger", "downloaded": "Téléchargé", + "downloading": "Downloading", "seeInDownloads": "Voir dans téléchargements", + "downloadPath": "Emplacement de téléchargement", "delete": "Supprimer", "showInFiles": "Voir dans les fichiers", "noDownloads": "Vous n'avez aucun téléchargements", + "howToDownload": "Go to the \"Events History\" screen to download events.", "downloadTitle": "{event} sur {device} du serveur {server} à {date}", "@downloadTitle": { "placeholders": { @@ -157,7 +198,6 @@ "date": {} } }, - "downloadPath": "Emplacement de téléchargement", "playbackOptions": "OPTION DE LECTURE", "play": "Jouer", "pause": "Pause", @@ -175,12 +215,33 @@ }, "noRecords": "Cette caméra n'a aucun enregistrement dans la période actuelle", "filter": "Filtrer", + "timeFilter": "Time filter", "fromDate": "De", "toDate": "À", "today": "Today", "yesterday": "Yesterday", "never": "never", + "fromToDate": "{from} to {to}", + "@fromToDate": { + "placeholders": { + "from": { + "type": "String" + }, + "to": { + "type": "String" + } + } + }, "allowAlarms": "Permettre les alarmes", + "nextEvents": "Next events", + "nEvents": "{n, plural, =0{No events} =1{1 event} other{{n} events}}", + "@nEvents": { + "placeholders": { + "n": { + "type": "int" + } + } + }, "@Event Priorities": {}, "info": "Information", "warn": "Avertissement", @@ -199,10 +260,12 @@ "systemReboot": "Redémarrage", "systemPowerOutage": "Perte de courant", "unknown": "Inconnu", - "@": {}, "close": "Ouvert", "open": "Fermé", + "collapse": "Collapse", + "expand": "Expand", "@PTZ": {}, + "ptzSupported": "PTZ is supported", "enabledPTZ": "PTZ est activé", "disabledPTZ": "PTZ est désactivé", "move": "Mouvement", @@ -222,6 +285,7 @@ "deletePreset": "Delete preset", "refreshPresets": "Refresh presets", "@Resolution": {}, + "resolution": "Resolution", "selectResolution": "Select resolution", "setResolution": "Set resolution", "setResolutionDescription": "The resolution of the video stream can highly impact the performance of the app. Set the resolution to a lower value to improve performance, or to a higher value to improve quality. You can set the default resolution to every camera in the settings", @@ -249,5 +313,71 @@ "newVersionAvailable": "New version available", "installVersion": "Install", "downloadVersion": "Download", - "learnMore": "Learn more" -} + "learnMore": "Learn more", + "failedToUpdate": "Failed to update", + "executableNotFound": "Executable not found", + "runningOn": "Running on {platform}", + "@runningOn": { + "placeholders": { + "platform": { + "type": "String" + } + } + }, + "windows": "Windows", + "linux": "Linux {env}", + "@linux": { + "placeholders": { + "env": { + "type": "String" + } + } + }, + "@CURRENT TASKS": {}, + "currentTasks": "Current tasks", + "noCurrentTasks": "No tasks", + "taskFetchingEvent": "Fetching events", + "taskFetchingEventsPlayback": "Fetching events playback", + "taskDownloadingEvent": "Downloading event", + "@@@SETTINGS": {}, + "@@APPEARANCE": {}, + "theme": "Thème", + "themeDescription": "Change the appearance of the app", + "system": "Système", + "light": "Clair", + "dark": "Sombre", + "@@MISC": {}, + "general": "General", + "miscellaneous": "Divers", + "@Snoozing": {}, + "snooze15": "15 minutes", + "snooze30": "30 minutes", + "snooze60": "1 heure", + "snoozeNotifications": "Mise en pause des notifications", + "notSnoozed": "Notifications actives", + "snoozeNotificationsUntil": "Notifications en pause jusqu'à", + "snoozedUntil": "Mis en pause jusqu'à {time}", + "@snoozedUntil": { + "placeholders": { + "time": {} + } + }, + "@Notification click": {}, + "notificationClickBehavior": "Action de clic sur les notifications", + "showEventsScreen": "Montrer l'historique d'événements", + "@@STREAMING": {}, + "streamingSetings": "Streaming settings", + "streamingType": "Streaming type", + "rtspProtocol": "RTSP Protocol", + "camerasSettings": "Cameras settings", + "renderingQuality": "Rendering quality", + "renderingQualityDescription": "The quality of the video rendering. The higher the quality, the more resources it takes.\nWhen automatic, the quality is selected based on the camera resolution.", + "cameraViewFit": "Ajustement de la vue caméra", + "cameraViewFitDescription": "The way the video is displayed in the view.", + "contain": "Contenir", + "fill": "Remplir", + "cover": "Couvrir", + "@@LOCALIZATION": {}, + "dateLanguage": "Date and Language", + "language": "Language" +} \ No newline at end of file diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index a3ced982..eb194f4f 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -75,11 +75,6 @@ "fps": "FPS", "date": "Data", "lastUpdate": "Ostatnia aktualizacja", - "theme": "Motyw", - "themeDescription": "Zmień wygląd aplikacji", - "system": "Systemowy", - "light": "Jasny", - "dark": "Ciemny", "screens": "Ekrany", "directCamera": "Kamera bezpośrednia", "addServer": "Dodaj serwer", @@ -128,37 +123,16 @@ "no": "Nie", "about": "O programie", "versionText": "Copyright © 2022, Bluecherry LLC.\nAll rights reserved.", - "snooze15": "15 minut", - "snooze30": "30 minut", - "snooze60": "1 godzina", - "miscellaneous": "Różne", - "snoozeNotifications": "Uśpij powiadomienia", - "notSnoozed": "Nie usypiaj", - "snoozeNotificationsUntil": "Uśpij powiadomienia do", - "snoozedUntil": "Uśpiono do {time}", - "@snoozedUntil": { - "placeholders": { - "time": { - "type": "String" - } - } - }, - "cameraViewFit": "Dopasowanie obrazu kamery", - "contain": "Zawartość", - "fill": "Wypełnienie", - "cover": "Pokrycie", "gettingDevices": "Pobieranie urządzeń...", "noDevices": "Brak urządzeń", "noEventsLoaded": "NIE ZAŁADOWANO ZDARZEŃ", "noEventsLoadedTips": "• Wybież kamery do podglądu zdarzeń\n• Użyj kalnedarza żeby wybrać konkretną datę lub zakres \n• Użyj przycisku \"Filtr\" aby wyszukiwać", "invalidResponse": "Odebrano nieprawidłową odpowiedź z serwera", "cameraOptions": "Opcje", - "notificationClickBehavior": "Zachowanie po kliknięciu na powiadomienie", "showFullscreenCamera": "Pokaż na pełnym ekranie", "openInANewWindow": "Otwórz w nowym oknie", "enableAudio": "Włącz dźwięk", "disableAudio": "Wyłącz dźwięk", - "showEventsScreen": "Pokaż historię zdarzeń", "addNewServer": "Dodaj nowy serwer", "disconnectServer": "Rozłącz", "serverOptions": "Opcje serwera", @@ -182,6 +156,14 @@ }, "newLayout": "Nowy układ", "editLayout": "Zmień układ", + "editSpecificLayout": "Edit {layoutName}", + "@editSpecificLayout": { + "placeholders": { + "layoutName": { + "type": "String" + } + } + }, "exportLayout": "Eksportuj układ", "importLayout": "Importuj układ", "failedToImportMessage": "Podczas próby importu {layoutName}, zostało odnalezione urządzenie podłączone do serwera, z którym nie ma połączenia. Podłącz się do tego serwera i spróbuj ponownie.\nSerwer: {server_ip}:{server_port}", @@ -212,6 +194,7 @@ "downloaded": "Pobrane", "downloading": "Pobieranie", "seeInDownloads": "Zobacz w Pobranych", + "downloadPath": "Katalog pobranych", "delete": "Usuń", "showInFiles": "Pokaż w Plikach", "noDownloads": "Jeszcze niczego nie pobrano :/", @@ -233,7 +216,6 @@ } } }, - "downloadPath": "Katalog pobranych", "playbackOptions": "OPCJE ODTWARZANIA", "play": "Odtwarzaj", "pause": "Pauza", @@ -366,10 +348,62 @@ } } }, + "windows": "Windows", + "linux": "Linux {env}", + "@linux": { + "placeholders": { + "env": { + "type": "String" + } + } + }, "@CURRENT TASKS": {}, "currentTasks": "Bieżące zadania", "noCurrentTasks": "Brak zadań", "taskFetchingEvent": "Pobieranie zdarzeń", "taskFetchingEventsPlayback": "Pobieranie zdarzeń odtwarania", - "taskDownloadingEvent": "Pobieranie zdarzenia" -} + "taskDownloadingEvent": "Pobieranie zdarzenia", + "@@@SETTINGS": {}, + "@@APPEARANCE": {}, + "theme": "Motyw", + "themeDescription": "Zmień wygląd aplikacji", + "system": "Systemowy", + "light": "Jasny", + "dark": "Ciemny", + "@@MISC": {}, + "general": "General", + "miscellaneous": "Różne", + "@Snoozing": {}, + "snooze15": "15 minut", + "snooze30": "30 minut", + "snooze60": "1 godzina", + "snoozeNotifications": "Uśpij powiadomienia", + "notSnoozed": "Nie usypiaj", + "snoozeNotificationsUntil": "Uśpij powiadomienia do", + "snoozedUntil": "Uśpiono do {time}", + "@snoozedUntil": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "@Notification click": {}, + "notificationClickBehavior": "Zachowanie po kliknięciu na powiadomienie", + "showEventsScreen": "Pokaż historię zdarzeń", + "@@STREAMING": {}, + "streamingSetings": "Streaming settings", + "streamingType": "Streaming type", + "rtspProtocol": "RTSP Protocol", + "camerasSettings": "Cameras settings", + "renderingQuality": "Rendering quality", + "renderingQualityDescription": "The quality of the video rendering. The higher the quality, the more resources it takes.\nWhen automatic, the quality is selected based on the camera resolution.", + "cameraViewFit": "Dopasowanie obrazu kamery", + "cameraViewFitDescription": "The way the video is displayed in the view.", + "contain": "Zawartość", + "fill": "Wypełnienie", + "cover": "Pokrycie", + "@@LOCALIZATION": {}, + "dateLanguage": "Date and Language", + "language": "Language" +} \ No newline at end of file diff --git a/lib/widgets/device_grid/desktop/layout_manager.dart b/lib/widgets/device_grid/desktop/layout_manager.dart index b4e5b147..8d162096 100644 --- a/lib/widgets/device_grid/desktop/layout_manager.dart +++ b/lib/widgets/device_grid/desktop/layout_manager.dart @@ -571,7 +571,7 @@ class _EditLayoutDialogState extends State { return AlertDialog( title: Row( children: [ - Expanded(child: Text('Edit ${widget.layout.name}')), + Expanded(child: Text(loc.editSpecificLayout(widget.layout.name))), if (view.layouts.length > 1) IconButton( icon: Icon( From 7b07a2f98e2fce93891b472411af7a78a794ffa1 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Mon, 2 Oct 2023 19:54:23 -0300 Subject: [PATCH 12/19] feat: portuguese translations --- lib/l10n/app_en.arb | 2 +- lib/l10n/app_fr.arb | 2 +- lib/l10n/app_pl.arb | 2 +- lib/l10n/app_pt.arb | 409 ++++++++++++++++++++++++ lib/providers/settings_provider.dart | 6 +- lib/widgets/settings/mobile/update.dart | 2 +- 6 files changed, 418 insertions(+), 5 deletions(-) create mode 100644 lib/l10n/app_pt.arb diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b07d8126..6c182797 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -89,7 +89,6 @@ } } }, - "pressBackAgainToExit": "Press back button again to exit", "servers": "Servers", "nServers": "{n, plural, =0{No servers} =1{1 server} other{{n} servers}}", "@nServers": { @@ -313,6 +312,7 @@ "setResolutionDescription": "The resolution of the video stream can highly impact the performance of the app. Set the resolution to a lower value to improve performance, or to a higher value to improve quality. You can set the default resolution to every camera in the settings", "hd": "High definition", "defaultResolution": "Default resolution", + "automaticResolution": "Automatic", "p1080": "1080p", "p720": "720p", "p480": "480p", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index f285d07b..bd9b4a6c 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -83,7 +83,6 @@ "serverName": {} } }, - "pressBackAgainToExit": "Appuyez sur le bouton retour de nouveau pour quitter", "servers": "Serveurs", "nServers": "{n, plural, =0{No servers} =1{1 server} other{{n} servers}}", "@nServers": { @@ -291,6 +290,7 @@ "setResolutionDescription": "The resolution of the video stream can highly impact the performance of the app. Set the resolution to a lower value to improve performance, or to a higher value to improve quality. You can set the default resolution to every camera in the settings", "hd": "High definition", "defaultResolution": "Default resolution", + "automaticResolution": "Automatic", "p1080": "1080p", "p720": "720p", "p480": "480p", diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index eb194f4f..9cdf8d4e 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -89,7 +89,6 @@ } } }, - "pressBackAgainToExit": "Naciśnij ponownie przycisk wstecz aby wyjść", "servers": "Serwery", "nServers": "{n, plural, =0{Brak serwerów} =1{1 serwer} other{{n} serwerów}}", "@nServers": { @@ -313,6 +312,7 @@ "setResolutionDescription": "Rozdzielczość strumienia wideo może mieć duży wpływ na wydajność aplikacji. Ustaw niższą rozdzielczość aby przyspieszyć działanie lub wyższą żeby zwiększyć jakość obrazu. Można ustawić rozdzielczość domyślną dla każdej kamery w ustawieniach.", "hd": "Wysoka jakość", "defaultResolution": "Rozdzielczość domyślna", + "automaticResolution": "Automatic", "p1080": "1080p", "p720": "720p", "p480": "480p", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb new file mode 100644 index 00000000..c847f210 --- /dev/null +++ b/lib/l10n/app_pt.arb @@ -0,0 +1,409 @@ +{ + "@@locale": "pt", + "welcome": "Bem vindo!", + "welcomeDescription": "Bem vindo ao Bluecherry Surveillance DVR!\nVamos conectar ao seu servidor DVR em un instante.", + "configure": "Configure um Servidor DVR", + "configureDescription": "Configure uma conexão com seu servidor DVR remoto", + "hostname": "Hostname", + "port": "Porta", + "name": "Nome", + "username": "Nome de usuário", + "password": "Senha", + "savePassword": "Salvar senha", + "useDefault": "Usar Padrão", + "connect": "Conectar", + "connectAutomaticallyAtStartup": "Conectar automaticamente ao iniciar", + "skip": "Pular", + "cancel": "Cancelar", + "letsGo": "Vamos lá!", + "finish": "Concluir", + "letsGoDescription": "Aqui algumas dicas de como começar", + "projectName": "Bluecherry", + "projectDescription": "Powerful Video Surveillance Software", + "website": "Website", + "purchase": "Compras", + "tip0": "Câmeras são mostradas à esquerda. Você pode dar dois cliques ou arrastar a câmera até a visualização para vê-la.", + "tip1": "Use os botões acima das câmeras para criar, salvar e alterar layouts - mesmo com câmeras de múltiplos servidores.", + "tip2": "Dê dois cliques em um servidor para abrir sua página de configuração em uma nova janela, onde você pode configurar câmeras e gravações.", + "tip3": "Aperte o ícone de eventos para abrir o histórico e assistir ou baixar as gravações.", + "errorTextField": "{field} não pode estar vazio.", + "@errorTextField": { + "placeholders": { + "field": { + "type": "String" + } + } + }, + "serverAdded": "Servidor adicionado", + "serverNotAddedError": "{serverName} não pôde ser adicionado.", + "@serverNotAddedError": { + "placeholders": { + "serverName": { + "type": "String" + } + } + }, + "serverNotAddedErrorDescription": "Por favor verifique os dados inseridos e certifique-se que o servidor está online.\n\nSe você está conectando remotamente, certifique-se que as portas 7001 e 7002 estão abertas para o servidor Bluecherry!", + "noServersAvailable": "Nenhum servidor disponível.", + "error": "Erro", + "ok": "OK", + "retry": "Tentar novamente", + "removeCamera": "Remover Câmera", + "replaceCamera": "Substituir Câmera", + "reloadCamera": "Recarregar Câmera", + "selectACamera": "Selecione uma câmera", + "switchCamera": "Trocar câmera", + "online": "Online", + "offline": "Offline", + "live": "AO VIVO", + "timedOut": "EXPIRADO", + "loading": "CARREGANDO", + "recorded": "GRAVADO", + "removeFromView": "Remover do layout", + "addToView": "Adicionar ao layout", + "addAllToView": "Adicionar tudo ao layout", + "eventBrowser": "Histórico de eventos", + "eventsTimeline": "Linha do tempo de eventos", + "server": "Servidor", + "device": "Dispositivo", + "event": "Evento", + "duration": "Duração", + "priority": "Prioridade", + "next": "Próximo", + "previous": "Anterior", + "lastImageUpdate": "Última atualização da imagem", + "fps": "FPS", + "date": "Data", + "lastUpdate": "Última atualização", + "screens": "Câmeras", + "directCamera": "Câmera específica", + "addServer": "Adicionar servidor", + "settings": "Configurações", + "noServersAdded": "Nenhum servidor adicionado", + "editServerInfo": "Editar informações do servidor", + "editServer": "Editar servidor {serverName}", + "@editServer": { + "placeholders": { + "serverName": { + "type": "String" + } + } + }, + "servers": "Servidores", + "nServers": "{n, plural, =0{Nenhum servidor} =1{1 servidor} other{{n} servidores}}", + "@nServers": { + "placeholders": { + "n": { + "type": "int", + "example": "1" + } + } + }, + "dateFormat": "Formato de Data", + "timeFormat": "Formato de Hora", + "nDevices": "{n, plural, =0{Nenhum dispositivo} =1{1 dispositivo} other{{n} dispositivos}}", + "@nDevices": { + "placeholders": { + "n": { + "type": "int" + } + } + }, + "remove": "Remover ?", + "removeServerDescription": "{serverName} será removido. Você não poderá mais ver as câmeras deste servidor e não receberá mais notificações.", + "@removeServerDescription": { + "placeholders": { + "serverName": { + "type": "String" + } + } + }, + "yes": "Sim", + "no": "Não", + "about": "Sobre", + "versionText": "Copyright © 2022, Bluecherry LLC.\nTodos os direitos reservados.", + "gettingDevices": "Carregando dispositivos...", + "noDevices": "Nenhum dispositivo", + "noEventsLoaded": "NENHUM EVENTO CARREGADO", + "noEventsLoadedTips": "• Selecione as câmeras cujas você quer ver os eventos\n• Utilize o calendário para selecionar uma data específica ou intervalo de datas \n• Use o botão \"Filtrar\" para pesquisar", + "invalidResponse": "Resposta inválida recebida do servidor", + "cameraOptions": "Opções", + "showFullscreenCamera": "Ver em tela cheia", + "openInANewWindow": "Abrir em nova janela", + "enableAudio": "Ativar audio", + "disableAudio": "Desativar audio", + "addNewServer": "Adicionar novo servidor", + "disconnectServer": "Desconectar", + "serverOptions": "Opções do servidor", + "browseEvents": "Ver eventos", + "eventType": "Tipo do evento", + "configureServer": "Configurar servidor", + "refreshDevices": "Recarregar dispositivos", + "refreshServer": "Recarregar servidor", + "refresh": "Recarregar", + "view": "View", + "@Layouts": {}, + "cycle": "Ciclo", + "cycleTogglePeriod": "Duração da alternância de layouts", + "fallbackLayoutName": "Layout {layout}", + "@fallbackLayoutName": { + "placeholders": { + "layout": { + "type": "int" + } + } + }, + "newLayout": "Novo layout", + "editLayout": "Editar layout", + "editSpecificLayout": "Editar {layoutName}", + "@editSpecificLayout": { + "placeholders": { + "layoutName": { + "type": "String" + } + } + }, + "exportLayout": "Exportar layout", + "importLayout": "Importar layout", + "failedToImportMessage": "Ao tentar importar {layoutName}, achamos um dispositívo que está conectando a um servidor que você não está conectado. Por favor, conecte-se ao servidor e tente novamente.\nServer: {server_ip}:{server_port}", + "@failedToImportMessage": { + "placeholders": { + "layoutName": { + "type": "String" + }, + "server_ip": { + "type": "String" + }, + "server_port": { + "type": "int" + } + } + }, + "layoutImportFileCorrupted": "O arquivo que você tentou importar está corrompido ou faltando informações.", + "layoutImportFileCorruptedWithMessage": "O arquivo que você tentou importar está corrompido ou faltando informações: \"{message}\"", + "singleView": "Câmera única", + "multipleView": "Múltiplas câmeras", + "compactView": "Visualização compacta", + "createNewLayout": "Criar novo layout", + "layoutNameHint": "Nome do layout", + "layoutTypeHint": "Tipo do layout", + "@Downloads": {}, + "downloads": "Downloads", + "download": "Baixar", + "downloaded": "Baixado", + "downloading": "Baixando", + "seeInDownloads": "Ver nos Downloads", + "downloadPath": "Diretório de Download", + "delete": "Deletar", + "showInFiles": "Ver no Explorador de Arquivos", + "noDownloads": "Você ainda não fez o download de nenhum evento :/", + "howToDownload": "Và ao \"Histórico de Eventos\" para fazer o download de eventos.", + "downloadTitle": "{event} de {device} do servidor {server} em {date}", + "@downloadTitle": { + "placeholders": { + "event": { + "type": "String" + }, + "device": { + "type": "String" + }, + "server": { + "type": "String" + }, + "date": { + "type": "String" + } + } + }, + "playbackOptions": "OPÇÕES DE REPRODUÇÃO", + "play": "Reproduzir", + "pause": "Pausar", + "volume": "Volume • {v}", + "@volume": { + "placeholders": { + "v": { + "type": "String" + } + } + }, + "speed": "Velocidade • {s}", + "@speed": { + "placeholders": { + "s": { + "type": "String" + } + } + }, + "noRecords": "Essa câmera não tem gravações neste período.", + "filter": "Filtrar", + "timeFilter": "Filtro de tempo", + "fromDate": "De", + "toDate": "à", + "today": "Hoje", + "yesterday": "Ontem", + "never": "Nunca", + "fromToDate": "{from} à {to}", + "@fromToDate": { + "placeholders": { + "from": { + "type": "String" + }, + "to": { + "type": "String" + } + } + }, + "allowAlarms": "Permitir alarmes", + "nextEvents": "Próximos eventos", + "nEvents": "{n, plural, =0{Nenhum evento} =1{1 evento} other{{n} eventos}}", + "@nEvents": { + "placeholders": { + "n": { + "type": "int" + } + } + }, + "@Event Priorities": {}, + "info": "Info", + "warn": "Aviso", + "alarm": "Alarme", + "critical": "Crítico", + "@Event Types": {}, + "motion": "Movimento", + "continuous": "Contínuo", + "notFound": "Não encontrado", + "cameraVideoLost": "Video Perdido", + "cameraAudioLost": "Audio Perdido", + "systemDiskSpace": "Disk Space", + "systemCrash": "Crash", + "systemBoot": "Startup", + "systemShutdown": "Desligamento", + "systemReboot": "Reincialização", + "systemPowerOutage": "Perda de energia", + "unknown": "Desconhecido", + "close": "Fechar", + "open": "Abrir", + "collapse": "Fechar", + "expand": "Expandir", + "@PTZ": {}, + "ptzSupported": "PTZ é suportado", + "enabledPTZ": "PTZ está ativado", + "disabledPTZ": "PTZ está desativado", + "move": "Movimento", + "stop": "Parar", + "noMovement": "Nenhum movimento", + "moveNorth": "Move up", + "moveSouth": "Move down", + "moveWest": "Move west", + "moveEast": "Move east", + "moveWide": "Afastar", + "moveTele": "Aproximar", + "presets": "Presets", + "noPresets": "Nenhum preset encontado", + "newPreset": "Novo preset", + "goToPreset": "Ir ao preset", + "renamePreset": "Renomear preset", + "deletePreset": "Deletar preset", + "refreshPresets": "Atualizar presets", + "@Resolution": {}, + "resolution": "Resolução", + "selectResolution": "Selecionar resolução", + "setResolution": "Definir resolução", + "setResolutionDescription": "A resolução da renderização do vídeo pode impactar fortemente o desempenho do aplicativo. Defina a resolução para um valor mais baixo para melhorar o desempenho ou para um valor mais alto para melhorar a qualidade. Você pode definir a resolução padrão nas configurações", + "hd": "Alta definição", + "defaultResolution": "Resolução padrão", + "automaticResolution": "Automático", + "p1080": "1080p", + "p720": "720p", + "p480": "480p", + "p360": "360p", + "p240": "240p", + "@updates": {}, + "updates": "Atualizações", + "upToDate": "Você está atualizado.", + "lastChecked": "Última verificação: {date}", + "@lastChecked": { + "placeholders": { + "date": { + "type": "String" + } + } + }, + "checkForUpdates": "Procurar atualizações", + "checkingForUpdates": "Procurando atualizações", + "automaticDownloadUpdates": "Baixar atualizações automaticamente", + "automaticDownloadUpdatesDescription": "Seja um dos primeiros a receber as atualizações, correções e melhorias mais recentes assim que lançadas.", + "updateHistory": "Histórico de atualizações", + "newVersionAvailable": "Nova versão disponível!", + "installVersion": "Instalar", + "downloadVersion": "Baixar", + "learnMore": "Saiba mais", + "failedToUpdate": "Falha ao atualizar", + "executableNotFound": "Executável não encontrado", + "runningOn": "Rodando no {platform}", + "@runningOn": { + "placeholders": { + "platform": { + "type": "String" + } + } + }, + "windows": "Windows", + "linux": "Linux {env}", + "@linux": { + "placeholders": { + "env": { + "type": "String" + } + } + }, + "@CURRENT TASKS": {}, + "currentTasks": "Tarefas", + "noCurrentTasks": "Nenhuma tarefa", + "taskFetchingEvent": "Buscando eventos", + "taskFetchingEventsPlayback": "Fetching events playback", + "taskDownloadingEvent": "Baixando evento", + "@@@SETTINGS": {}, + "@@APPEARANCE": {}, + "theme": "Aparência", + "themeDescription": "Mude a aparência do aplicativo", + "system": "Padrão do Sistema", + "light": "Claro", + "dark": "Escuro", + "@@MISC": {}, + "general": "Geral", + "miscellaneous": "Outros", + "@Snoozing": {}, + "snooze15": "15 minutos", + "snooze30": "30 minutos", + "snooze60": "1 hora", + "snoozeNotifications": "Silenciar notificações", + "notSnoozed": "Não silenciado", + "snoozeNotificationsUntil": "Silenciar notificações até", + "snoozedUntil": "Silenciado até {time}", + "@snoozedUntil": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "@Notification click": {}, + "notificationClickBehavior": "Ação ao clicar na notificação", + "showEventsScreen": "Mostar histórico de eventos", + "@@STREAMING": {}, + "streamingSetings": "Configurações de streaming", + "streamingType": "Tipo de streaming", + "rtspProtocol": "Protocolo RTSP", + "camerasSettings": "Configurações das câmeras", + "renderingQuality": "Qualidade de renderização", + "renderingQualityDescription": "A qualidade de renderização. Quanto maior a qualidade, mais recursos são usados.\nQuando automatico, a resolução é selecionada baseada na resolução da câmera.", + "cameraViewFit": "Ajuste de imagem da câmera", + "cameraViewFitDescription": "Como o vídeo é renderizado na visualização.", + "contain": "Limitar", + "fill": "Preencher", + "cover": "Cobrir", + "@@LOCALIZATION": {}, + "dateLanguage": "Data e Idioma", + "language": "Idioma" +} \ No newline at end of file diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index efbe1197..a21ca4b7 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -25,6 +25,7 @@ import 'package:bluecherry_client/utils/constants.dart'; import 'package:bluecherry_client/utils/storage.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'; @@ -206,6 +207,9 @@ class SettingsProvider extends ChangeNotifier { _themeMode = kDefaultThemeMode; } final format = SystemDateTimeFormat(); + initializeDateFormatting(_locale.languageCode); + Intl.defaultLocale = _locale.toLanguageTag(); + final systemLocale = Intl.getCurrentLocale(); final timePattern = await format.getTimePattern(); if (data.containsKey(kHiveDateFormat)) { @@ -340,7 +344,7 @@ enum RenderingQuality { RenderingQuality.p480 => loc.p480, RenderingQuality.p360 => loc.p360, RenderingQuality.p240 => loc.p240, - RenderingQuality.automatic => 'Automatic', + RenderingQuality.automatic => loc.automaticResolution, }; } } diff --git a/lib/widgets/settings/mobile/update.dart b/lib/widgets/settings/mobile/update.dart index 26810e18..3cf9f960 100644 --- a/lib/widgets/settings/mobile/update.dart +++ b/lib/widgets/settings/mobile/update.dart @@ -304,7 +304,7 @@ class AppUpdateOptions extends StatelessWidget { const TextSpan(text: ' '), TextSpan( text: SettingsProvider.instance.dateFormat.format( - DateFormat('EEE, d MMM yyyy') + DateFormat('EEE, d MMM yyyy', 'en_US') .parse(version.publishedAt), ), style: theme.textTheme.labelSmall, From a548dbde28655d22ba2613c0995e220df3daa075 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Mon, 2 Oct 2023 19:59:11 -0300 Subject: [PATCH 13/19] feat: show platform brightness on system theme mode --- lib/l10n/app_pt.arb | 2 +- lib/widgets/settings/desktop/general.dart | 10 +++++++--- lib/widgets/settings/mobile/settings.dart | 10 +++++++--- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index c847f210..25458f57 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -141,7 +141,7 @@ "refreshDevices": "Recarregar dispositivos", "refreshServer": "Recarregar servidor", "refresh": "Recarregar", - "view": "View", + "view": "Layouts", "@Layouts": {}, "cycle": "Ciclo", "cycleTogglePeriod": "Duração da alternância de layouts", diff --git a/lib/widgets/settings/desktop/general.dart b/lib/widgets/settings/desktop/general.dart index 92c0ce64..722d71cc 100644 --- a/lib/widgets/settings/desktop/general.dart +++ b/lib/widgets/settings/desktop/general.dart @@ -59,15 +59,19 @@ class GeneralSettings extends StatelessWidget { trailing: Radio( value: e, groupValue: settings.themeMode, - onChanged: (value) { - settings.themeMode = e; - }, + onChanged: (_) => settings.themeMode = e, ), title: Text(switch (e) { ThemeMode.system => loc.system, ThemeMode.light => loc.light, ThemeMode.dark => loc.dark, }), + subtitle: e == ThemeMode.system + ? Text(switch (MediaQuery.platformBrightnessOf(context)) { + Brightness.dark => loc.dark, + Brightness.light => loc.light, + }) + : null, ); }), SubHeader(loc.miscellaneous, padding: DesktopSettings.horizontalPadding), diff --git a/lib/widgets/settings/mobile/settings.dart b/lib/widgets/settings/mobile/settings.dart index 7102f4bd..f7f0ede5 100644 --- a/lib/widgets/settings/mobile/settings.dart +++ b/lib/widgets/settings/mobile/settings.dart @@ -54,9 +54,7 @@ class _MobileSettingsState extends State { @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - SettingsProvider.instance.reload(); - }); + SettingsProvider.instance.reload(); } @override @@ -116,6 +114,12 @@ class _MobileSettingsState extends State { ThemeMode.light => loc.light, ThemeMode.dark => loc.dark, }), + subtitle: e == ThemeMode.system + ? Text(switch (MediaQuery.platformBrightnessOf(context)) { + Brightness.dark => loc.dark, + Brightness.light => loc.light, + }) + : null, ); }).toList()), SliverToBoxAdapter(child: SubHeader(loc.miscellaneous)), From b4b3c40ccfa078a7dca0545b5c5a2466736da335 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Tue, 3 Oct 2023 10:01:24 -0300 Subject: [PATCH 14/19] ui: reestructure date and language section --- .../settings/desktop/date_language.dart | 131 +++++++++--------- 1 file changed, 65 insertions(+), 66 deletions(-) diff --git a/lib/widgets/settings/desktop/date_language.dart b/lib/widgets/settings/desktop/date_language.dart index f961c0a1..806c18e6 100644 --- a/lib/widgets/settings/desktop/date_language.dart +++ b/lib/widgets/settings/desktop/date_language.dart @@ -18,6 +18,7 @@ */ import 'package:bluecherry_client/providers/settings_provider.dart'; +import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/widgets/settings/desktop/settings.dart'; import 'package:bluecherry_client/widgets/settings/mobile/settings.dart'; import 'package:flutter/material.dart'; @@ -32,26 +33,41 @@ class LocalizationSettings extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); final loc = AppLocalizations.of(context); - const horizontalPadding = EdgeInsetsDirectional.symmetric(horizontal: 16.0); return ListView(padding: DesktopSettings.verticalPadding, children: [ - Padding( - padding: horizontalPadding, - child: Text(loc.language, style: theme.textTheme.titleMedium), - ), const LanguageSection(), const SizedBox(height: 12.0), Padding( - padding: horizontalPadding, + padding: DesktopSettings.horizontalPadding, child: Text(loc.dateFormat, style: theme.textTheme.titleMedium), ), - const DateFormatSection(), + 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: horizontalPadding, + padding: DesktopSettings.horizontalPadding, child: Text(loc.timeFormat, style: theme.textTheme.titleMedium), ), - const TimeFormatSection(), + const SizedBox(height: 8.0), + Padding( + padding: DesktopSettings.horizontalPadding, + child: Material( + clipBehavior: Clip.hardEdge, + shape: RoundedRectangleBorder( + borderRadius: BorderRadiusDirectional.circular(8.0), + ), + child: const TimeFormatSection(), + ), + ), ]); } } @@ -61,69 +77,52 @@ class LanguageSection extends StatelessWidget { @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 LayoutBuilder(builder: (context, consts) { - if (consts.maxWidth >= 800) { - final crossAxisCount = consts.maxWidth >= 870 ? 4 : 3; - return Wrap( - children: locales.map((locale) { - final name = - names.nameOf(locale.toLanguageTag()) ?? locale.toLanguageTag(); - final nativeName = LocaleNamesLocalizationsDelegate - .nativeLocaleNames[locale.toLanguageTag()] ?? - locale.toLanguageTag(); - return SizedBox( - width: consts.maxWidth / crossAxisCount, - child: RadioListTile( + return Padding( + padding: DesktopSettings.horizontalPadding, + child: Material( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + child: ListTile( + title: Text(loc.language, style: theme.textTheme.titleMedium), + 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, - groupValue: currentLocale, - onChanged: (value) { - settings.locale = locale; - }, - controlAffinity: ListTileControlAffinity.trailing, - title: Text( - name, - maxLines: 1, - softWrap: false, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + name.uppercaseFirst(), + maxLines: 1, + softWrap: false, + ), + Text( + nativeName.uppercaseFirst(), + style: theme.textTheme.labelSmall, + ), + ], ), - subtitle: Text( - nativeName, - ), - ), - ); - }).toList(), - ); - } else { - return Column( - children: locales.map((locale) { - final name = - names.nameOf(locale.toLanguageTag()) ?? locale.toLanguageTag(); - final nativeName = LocaleNamesLocalizationsDelegate - .nativeLocaleNames[locale.toLanguageTag()] ?? - locale.toLanguageTag(); - return RadioListTile( - value: locale, - groupValue: currentLocale, - onChanged: (value) { - settings.locale = locale; - }, - controlAffinity: ListTileControlAffinity.trailing, - title: Padding( - padding: const EdgeInsetsDirectional.only(start: 8.0), - child: Text(name), - ), - subtitle: Padding( - padding: const EdgeInsetsDirectional.only(start: 8.0), - child: Text(nativeName), - ), - ); - }).toList(), - ); - } - }); + ); + }).toList(), + ), + ), + ), + ); } } From 975d7ae3a23a260249f5c168d9015b1df127edc4 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Tue, 3 Oct 2023 10:31:10 -0300 Subject: [PATCH 15/19] ui(mobile): split date and language settings into a different screen --- .../settings/desktop/date_language.dart | 78 ++++++++++--------- lib/widgets/settings/mobile/date_time.dart | 25 ------ lib/widgets/settings/mobile/settings.dart | 61 ++++++++++----- 3 files changed, 82 insertions(+), 82 deletions(-) diff --git a/lib/widgets/settings/desktop/date_language.dart b/lib/widgets/settings/desktop/date_language.dart index 806c18e6..dba90336 100644 --- a/lib/widgets/settings/desktop/date_language.dart +++ b/lib/widgets/settings/desktop/date_language.dart @@ -35,7 +35,15 @@ class LocalizationSettings extends StatelessWidget { final loc = AppLocalizations.of(context); return ListView(padding: DesktopSettings.verticalPadding, children: [ - const LanguageSection(), + 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, @@ -84,43 +92,37 @@ class LanguageSection extends StatelessWidget { const locales = AppLocalizations.supportedLocales; final names = LocaleNames.of(context)!; - return Padding( - padding: DesktopSettings.horizontalPadding, - child: Material( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - child: ListTile( - title: Text(loc.language, style: theme.textTheme.titleMedium), - 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: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - name.uppercaseFirst(), - maxLines: 1, - softWrap: false, - ), - Text( - nativeName.uppercaseFirst(), - style: theme.textTheme.labelSmall, - ), - ], - ), - ); - }).toList(), - ), + return DropdownButtonHideUnderline( + child: ListTile( + title: Text(loc.language, style: theme.textTheme.titleMedium), + 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: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + name.uppercaseFirst(), + maxLines: 1, + softWrap: false, + ), + Text( + nativeName.uppercaseFirst(), + style: theme.textTheme.labelSmall, + ), + ], + ), + ); + }).toList(), ), ), ); diff --git a/lib/widgets/settings/mobile/date_time.dart b/lib/widgets/settings/mobile/date_time.dart index d055a4bc..f9db8a4a 100644 --- a/lib/widgets/settings/mobile/date_time.dart +++ b/lib/widgets/settings/mobile/date_time.dart @@ -19,31 +19,6 @@ part of 'settings.dart'; -class DateTimeSection extends StatelessWidget { - const DateTimeSection({super.key}); - - @override - Widget build(BuildContext context) { - final loc = AppLocalizations.of(context); - return Column(children: [ - // SubHeader('Language'), - // SliverList( - // delegate: SliverChildListDelegate( - // AppLocalizations.supportedLocales.map((locale) { - // return ListTile( - // title: Text(locale.languageCode), - // ); - // }).toList(), - // ), - // ), - SubHeader(loc.dateFormat), - const DateFormatSection(), - SubHeader(loc.timeFormat), - const TimeFormatSection(), - ]); - } -} - class DateFormatSection extends StatelessWidget { const DateFormatSection({super.key}); diff --git a/lib/widgets/settings/mobile/settings.dart b/lib/widgets/settings/mobile/settings.dart index f7f0ede5..ae502a08 100644 --- a/lib/widgets/settings/mobile/settings.dart +++ b/lib/widgets/settings/mobile/settings.dart @@ -30,11 +30,13 @@ import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/utils/methods.dart'; import 'package:bluecherry_client/widgets/edit_server.dart'; import 'package:bluecherry_client/widgets/misc.dart'; +import 'package:bluecherry_client/widgets/settings/desktop/date_language.dart'; import 'package:bluecherry_client/widgets/settings/mobile/update.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_localized_locales/flutter_localized_locales.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:unity_video_player/unity_video_player.dart'; @@ -99,9 +101,7 @@ class _MobileSettingsState extends State { ThemeMode.dark => Icons.dark_mode, }), ), - onTap: () { - settings.themeMode = e; - }, + onTap: () => settings.themeMode = e, trailing: Radio( value: e, groupValue: settings.themeMode, @@ -151,15 +151,12 @@ class _MobileSettingsState extends State { 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.snoozedUntil([ + if (settings.snoozedUntil.difference(DateTime.now()) > + const Duration(hours: 24)) + settings.formatDate(settings.snoozedUntil), + settings.formatTime(settings.snoozedUntil), + ].join(' ')) : loc.notSnoozed, ), ExpansionTile( @@ -218,9 +215,7 @@ class _MobileSettingsState extends State { ), value: e, groupValue: settings.cameraViewFit, - onChanged: (value) { - settings.cameraViewFit = e; - }, + onChanged: (_) => settings.cameraViewFit = e, secondary: Icon(e.icon), controlAffinity: ListTileControlAffinity.trailing, title: Padding( @@ -276,15 +271,43 @@ class _MobileSettingsState extends State { controlAffinity: ListTileControlAffinity.trailing, title: Padding( padding: const EdgeInsetsDirectional.only(start: 16.0), - child: Text( - dur.humanReadable(context), - ), + child: Text(dur.humanReadable(context)), ), ); }).toList(), ), ]), - const SliverToBoxAdapter(child: DateTimeSection()), + 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: 72.0, + onTap: () { + showModalBottomSheet( + context: context, + showDragHandle: true, + isScrollControlled: true, + builder: (context) { + return DraggableScrollableSheet( + expand: false, + minChildSize: 0.8, + initialChildSize: 0.8, + builder: (context, controller) { + return PrimaryScrollController( + controller: controller, + child: const LocalizationSettings(), + ); + }, + ); + }, + ); + }, + ), + ), if (update.isUpdatingSupported) ...[ SliverToBoxAdapter( child: SubHeader( From acc6cae840d494c48595e405e2265d91e28b5303 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Tue, 3 Oct 2023 10:46:34 -0300 Subject: [PATCH 16/19] feat: reload all live players when streaming type, rtsp protocol or video quality settings change --- lib/providers/settings_provider.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index a21ca4b7..391c7713 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -23,6 +23,7 @@ import 'package:bluecherry_client/providers/downloads_provider.dart'; import 'package:bluecherry_client/providers/home_provider.dart'; import 'package:bluecherry_client/utils/constants.dart'; import 'package:bluecherry_client/utils/storage.dart'; +import 'package:bluecherry_client/utils/video_player.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:intl/date_symbol_data_local.dart'; @@ -127,16 +128,19 @@ class SettingsProvider extends ChangeNotifier { 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(); + UnityPlayers.reloadAll(); } late Locale _locale; From 7dd4749461052182bd8d515ed55e54143dfdde72 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Tue, 3 Oct 2023 20:12:18 -0300 Subject: [PATCH 17/19] native: tweaks --- lib/models/device.dart | 62 ++++++++++++++++--- lib/providers/settings_provider.dart | 3 +- lib/utils/video_player.dart | 17 +++-- .../device_grid/video_status_label.dart | 2 +- lib/widgets/error_warning.dart | 2 +- .../lib/unity_video_player_main.dart | 8 +++ 6 files changed, 74 insertions(+), 20 deletions(-) diff --git a/lib/models/device.dart b/lib/models/device.dart index 631abb06..4c842131 100644 --- a/lib/models/device.dart +++ b/lib/models/device.dart @@ -17,8 +17,11 @@ * along with this program. If not, see . */ +import 'dart:convert'; + import 'package:bluecherry_client/models/server.dart'; import 'package:flutter/foundation.dart'; +import 'package:http/http.dart' as http; /// A [Device] present on a server. class Device { @@ -89,7 +92,7 @@ class Device { } String get rtspURL { - return Uri( + return Uri.encodeFull(Uri( scheme: 'rtsp', userInfo: '${Uri.encodeComponent(server.login)}' ':' @@ -97,32 +100,71 @@ class Device { host: server.ip, port: server.rtspPort, path: uri, - ).toString(); + ).toString()); } String get mjpegURL { - return Uri( + return Uri.encodeFull(Uri( scheme: 'https', userInfo: '${Uri.encodeComponent(server.login)}' ':' '${Uri.encodeComponent(server.password)}', host: server.ip, - port: server.rtspPort, - path: 'media/mjpeg.php', - query: 'id=$id&multipart=true', - ).toString(); + port: server.port, + pathSegments: ['media', 'mjpeg'], + queryParameters: { + 'multipart': 'true', + 'id': '$id', + }, + ).toString()); } String get hlsURL { - return Uri( + return Uri.encodeFull(Uri( scheme: 'https', userInfo: '${Uri.encodeComponent(server.login)}' ':' '${Uri.encodeComponent(server.password)}', host: server.ip, port: server.port, - path: 'hls/$id/index.m3u8', - ).toString(); + pathSegments: ['hls', '$id', 'index.m3u8'], + ).toString()); + } + + Future getHLSUrl([Device? device]) async { + // return hlsURL; + device ??= this; + var data = { + 'id': device.id.toString(), + 'hostname': device.server.ip, + 'port': device.server.port.toString(), + }; + + final uri = Uri( + scheme: 'https', + userInfo: '${Uri.encodeComponent(device.server.login)}' + ':' + '${Uri.encodeComponent(device.server.password)}', + host: device.server.ip, + port: device.server.port, + path: 'media/hls', + queryParameters: data, + ); + + var response = await http.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}'); + } + + return null; } /// Returns the full name of this device, including the server name. diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 391c7713..1c63d86e 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -140,7 +140,6 @@ class SettingsProvider extends ChangeNotifier { set videoQuality(RenderingQuality value) { _videoQuality = value; _save(); - UnityPlayers.reloadAll(); } late Locale _locale; @@ -356,5 +355,5 @@ enum RenderingQuality { enum StreamingType { rtsp, hls, - mpeg, + mjpeg, } diff --git a/lib/utils/video_player.dart b/lib/utils/video_player.dart index fcc1e308..4a489c54 100644 --- a/lib/utils/video_player.dart +++ b/lib/utils/video_player.dart @@ -37,7 +37,6 @@ class UnityPlayers with ChangeNotifier { /// Helper method to create a video player with required configuration for a [Device]. static UnityVideoPlayer forDevice(Device device) { - debugPrint(device.streamURL); final settings = SettingsProvider.instance; final controller = UnityVideoPlayer.create( quality: switch (settings.videoQuality) { @@ -50,14 +49,20 @@ class UnityPlayers with ChangeNotifier { UnityVideoQuality.qualityForResolutionY(device.resolutionY), }, ) - ..setDataSource(switch (settings.streamingType) { - StreamingType.rtsp => device.rtspURL, - StreamingType.hls => device.hlsURL, - StreamingType.mpeg => device.mjpegURL, - }) ..setVolume(0.0) ..setSpeed(1.0); + Future setSource() async { + final source = switch (settings.streamingType) { + StreamingType.rtsp => device.rtspURL, + StreamingType.hls => (await device.getHLSUrl()) ?? device.hlsURL, + StreamingType.mjpeg => device.mjpegURL, + }; + controller.setDataSource(source); + } + + setSource(); + return controller; } diff --git a/lib/widgets/device_grid/video_status_label.dart b/lib/widgets/device_grid/video_status_label.dart index f170f965..1f2dde83 100644 --- a/lib/widgets/device_grid/video_status_label.dart +++ b/lib/widgets/device_grid/video_status_label.dart @@ -59,7 +59,7 @@ class _VideoStatusLabelState extends State { widget.video.player.dataSource != null && // It is only LIVE if it starts with rtsp or is hls (_source.startsWith('rtsp') || - _source.contains('media/mjpeg.php') || + _source.contains('media/mjpeg') || _source.endsWith('index.m3u8') /* hls */); _VideoLabel get status => widget.video.error != null diff --git a/lib/widgets/error_warning.dart b/lib/widgets/error_warning.dart index 3618edcd..965d3bd4 100644 --- a/lib/widgets/error_warning.dart +++ b/lib/widgets/error_warning.dart @@ -39,7 +39,7 @@ class ErrorWarning extends StatelessWidget { if (message.isNotEmpty) ...[ const SizedBox(height: 8.0), Text( - message.toUpperCase(), + message, textAlign: TextAlign.center, style: const TextStyle( color: Colors.white70, diff --git a/packages/unity_video_player/unity_video_player_main/lib/unity_video_player_main.dart b/packages/unity_video_player/unity_video_player_main/lib/unity_video_player_main.dart index cf984a91..76c83594 100644 --- a/packages/unity_video_player/unity_video_player_main/lib/unity_video_player_main.dart +++ b/packages/unity_video_player/unity_video_player_main/lib/unity_video_player_main.dart @@ -140,6 +140,14 @@ class UnityVideoPlayerMediaKit extends UnityVideoPlayer { _fps = double.parse(fps); _fpsStreamController.add(_fps); }); + platform.setProperty('msg-level', 'all=v'); + + mkPlayer.stream.log.listen((event) { + debugPrint('${event.level} / ${event.prefix}: ${event.text}'); + }); + + platform.setProperty('tls-verify', 'no'); + platform.setProperty('insecure', 'yes'); if (rtspProtocol != null) { platform.setProperty('rtsp-transport', rtspProtocol.name); From 556d7749d3254000b506b782806483cb98f6e4b8 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Thu, 5 Oct 2023 19:08:04 -0300 Subject: [PATCH 18/19] fix: emit error when a 'fatal' log is detected --- .../lib/unity_video_player_main.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/unity_video_player/unity_video_player_main/lib/unity_video_player_main.dart b/packages/unity_video_player/unity_video_player_main/lib/unity_video_player_main.dart index 76c83594..a2700704 100644 --- a/packages/unity_video_player/unity_video_player_main/lib/unity_video_player_main.dart +++ b/packages/unity_video_player/unity_video_player_main/lib/unity_video_player_main.dart @@ -143,7 +143,11 @@ class UnityVideoPlayerMediaKit extends UnityVideoPlayer { platform.setProperty('msg-level', 'all=v'); mkPlayer.stream.log.listen((event) { - debugPrint('${event.level} / ${event.prefix}: ${event.text}'); + // debugPrint('${event.level} / ${event.prefix}: ${event.text}'); + if (event.level == 'fatal') { + // ignore: invalid_use_of_protected_member + platform.errorController.add(event.text); + } }); platform.setProperty('tls-verify', 'no'); @@ -199,7 +203,7 @@ class UnityVideoPlayerMediaKit extends UnityVideoPlayer { } @override - Stream get onError => mkPlayer.stream.error.map((event) => event); + Stream get onError => mkPlayer.stream.error; @override Duration get duration => mkPlayer.state.duration; From 88ab7d6c58335ac3960222870dd4b24af23bcb87 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Thu, 5 Oct 2023 19:30:12 -0300 Subject: [PATCH 19/19] feat: update error view --- lib/l10n/app_en.arb | 1 + lib/l10n/app_fr.arb | 1 + lib/l10n/app_pl.arb | 1 + lib/l10n/app_pt.arb | 1 + .../desktop/desktop_device_grid.dart | 223 ++++++++---------- .../device_grid/video_status_label.dart | 2 +- lib/widgets/error_warning.dart | 45 ++-- 7 files changed, 134 insertions(+), 140 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 6c182797..7238afe2 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -46,6 +46,7 @@ "serverNotAddedErrorDescription": "Please check the entered details and ensure the server is online.\n\nIf you are connecting remote, make sure the 7001 and 7002 ports are open to the Bluecherry server!", "noServersAvailable": "No servers available", "error": "Error", + "videoError": "An error happened while trying to play the video.", "ok": "OK", "retry": "Retry", "removeCamera": "Remove Camera", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index bd9b4a6c..688f6f0c 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -42,6 +42,7 @@ "serverNotAddedErrorDescription": "Please check the entered details and ensure the server is online.\n\nIf you are connecting remote, make sure the 7001 and 7002 ports are open to the Bluecherry server!", "noServersAvailable": "Aucun serveur disponible", "error": "Erreur", + "videoError": "An error happened while trying to play the video.", "ok": "OK", "retry": "Retry", "removeCamera": "Enlever caméra", diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 9cdf8d4e..9fb99d32 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -46,6 +46,7 @@ "serverNotAddedErrorDescription": "Sprawdź wprowadzone dane i upewnij się, że serwer jest online.\n\nJeśli łączysz się zdalnie to upewnij się, że porty na serwerzy Blueberry: 7001 i 7002, są otwarte!", "noServersAvailable": "Brak dostępnych serwerów", "error": "Błąd", + "videoError": "An error happened while trying to play the video.", "ok": "OK", "retry": "Spróbuj ponownie", "removeCamera": "Usuń kamerę", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 25458f57..b4b0cfc3 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -46,6 +46,7 @@ "serverNotAddedErrorDescription": "Por favor verifique os dados inseridos e certifique-se que o servidor está online.\n\nSe você está conectando remotamente, certifique-se que as portas 7001 e 7002 estão abertas para o servidor Bluecherry!", "noServersAvailable": "Nenhum servidor disponível.", "error": "Erro", + "videoError": "Ocorreu um erro ao tentar reproduzir o vídeo.", "ok": "OK", "retry": "Tentar novamente", "removeCamera": "Remover Câmera", diff --git a/lib/widgets/device_grid/desktop/desktop_device_grid.dart b/lib/widgets/device_grid/desktop/desktop_device_grid.dart index 5acc5be8..41fc5a24 100644 --- a/lib/widgets/device_grid/desktop/desktop_device_grid.dart +++ b/lib/widgets/device_grid/desktop/desktop_device_grid.dart @@ -435,29 +435,23 @@ class _DesktopTileViewportState extends State { ); final error = UnityVideoView.maybeOf(context)?.error; - if (error != null) { - return Stack(children: [ - Positioned.fill(child: ErrorWarning(message: error)), - PositionedDirectional( - top: 4, - end: 4, - child: closeButton, - ), - PositionedDirectional( - bottom: 6.0, - end: 6.0, - child: VideoStatusLabel( - video: UnityVideoView.of(context), - device: widget.device, - ), - ), - ]); - } - final video = UnityVideoView.maybeOf(context); - final isSubView = AlternativeWindow.maybeOf(context) != null; + final reloadButton = IconButton( + icon: Icon( + Icons.replay_outlined, + shadows: outlinedText(), + ), + tooltip: loc.reloadCamera, + color: Colors.white, + iconSize: 18.0, + onPressed: () async { + await UnityPlayers.reloadDevice(widget.device); + setState(() {}); + }, + ); + Widget foreground = PTZController( enabled: ptzEnabled, device: widget.device, @@ -465,6 +459,8 @@ class _DesktopTileViewportState extends State { final states = HoverButton.of(context).states; return Stack(children: [ + if (error != null) + Positioned.fill(child: ErrorWarning(message: error)), Padding( padding: const EdgeInsetsDirectional.symmetric( horizontal: 12.0, @@ -496,7 +492,7 @@ class _DesktopTileViewportState extends State { child: PTZData(commands: commands), ), if (video != null) ...[ - if (!widget.controller!.isSeekable) + if (!widget.controller!.isSeekable && error == null) const Center( child: SizedBox( height: 20.0, @@ -511,118 +507,105 @@ class _DesktopTileViewportState extends State { end: 0, start: 0, bottom: 4.0, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - if (states.isHovering) ...[ - const SizedBox(width: 12.0), - if (widget.device.hasPTZ) - PTZToggleButton( - ptzEnabled: ptzEnabled, - onChanged: (enabled) => - setState(() => ptzEnabled = enabled), - ), - const Spacer(), - () { - final isMuted = volume == 0.0; - - return IconButton( - icon: Icon( - isMuted - ? Icons.volume_mute_rounded - : Icons.volume_up_rounded, - shadows: outlinedText(), - ), - tooltip: isMuted ? loc.enableAudio : loc.disableAudio, - color: Colors.white, - iconSize: 18.0, - onPressed: () async { - if (isMuted) { - await widget.controller!.setVolume(1.0); - } else { - await widget.controller!.setVolume(0.0); - } - - updateVolume(); - }, - ); - }(), - if (isDesktopPlatform && !isSubView) - IconButton( - icon: Icon( - Icons.open_in_new_sharp, - shadows: outlinedText(), - ), - tooltip: loc.openInANewWindow, - color: Colors.white, - iconSize: 18.0, - onPressed: () { - widget.device.openInANewWindow(); - }, + child: Row(crossAxisAlignment: CrossAxisAlignment.end, children: [ + if (states.isHovering && error == null) ...[ + const SizedBox(width: 12.0), + if (widget.device.hasPTZ) + PTZToggleButton( + ptzEnabled: ptzEnabled, + onChanged: (enabled) => + setState(() => ptzEnabled = enabled), + ), + const Spacer(), + () { + final isMuted = volume == 0.0; + + return IconButton( + icon: Icon( + isMuted + ? Icons.volume_mute_rounded + : Icons.volume_up_rounded, + shadows: outlinedText(), ), - if (!isSubView) - IconButton( - icon: Icon( - Icons.fullscreen_rounded, - shadows: outlinedText(), - ), - tooltip: loc.showFullscreenCamera, - color: Colors.white, - iconSize: 18.0, - onPressed: () async { - var player = UnityPlayers.players[widget.device]; - var isLocalController = false; - if (player == null) { - player = UnityPlayers.forDevice(widget.device); - isLocalController = true; - } - - await Navigator.of(context).pushNamed( - '/fullscreen', - arguments: { - 'device': widget.device, - 'player': player, - 'ptzEnabled': ptzEnabled, - }, - ); - if (isLocalController) await player.release(); - }, + tooltip: isMuted ? loc.enableAudio : loc.disableAudio, + color: Colors.white, + iconSize: 18.0, + onPressed: () async { + if (isMuted) { + await widget.controller!.setVolume(1.0); + } else { + await widget.controller!.setVolume(0.0); + } + + updateVolume(); + }, + ); + }(), + if (isDesktopPlatform && !isSubView) + IconButton( + icon: Icon( + Icons.open_in_new_sharp, + shadows: outlinedText(), ), + tooltip: loc.openInANewWindow, + color: Colors.white, + iconSize: 18.0, + onPressed: () { + widget.device.openInANewWindow(); + }, + ), + if (!isSubView) IconButton( icon: Icon( - Icons.replay_outlined, + Icons.fullscreen_rounded, shadows: outlinedText(), ), - tooltip: loc.reloadCamera, + tooltip: loc.showFullscreenCamera, color: Colors.white, iconSize: 18.0, onPressed: () async { - await UnityPlayers.reloadDevice(widget.device); - setState(() {}); + var player = UnityPlayers.players[widget.device]; + var isLocalController = false; + if (player == null) { + player = UnityPlayers.forDevice(widget.device); + isLocalController = true; + } + + await Navigator.of(context).pushNamed( + '/fullscreen', + arguments: { + 'device': widget.device, + 'player': player, + 'ptzEnabled': ptzEnabled, + }, + ); + if (isLocalController) await player.release(); }, ), - CameraViewFitButton( - fit: context - .findAncestorWidgetOfExactType() - ?.fit ?? - SettingsProvider.instance.cameraViewFit, - onChanged: widget.onFitChanged, - ), - ], - const SizedBox(width: 12.0), - Padding( - padding: const EdgeInsetsDirectional.only( - end: 6.0, - bottom: 6.0, - ), - child: VideoStatusLabel( - video: video, - device: widget.device, - ), + reloadButton, + CameraViewFitButton( + fit: context + .findAncestorWidgetOfExactType() + ?.fit ?? + SettingsProvider.instance.cameraViewFit, + onChanged: widget.onFitChanged, ), + ] else ...[ + const Spacer(), + if (states.isHovering) reloadButton, ], - ), + const SizedBox(width: 12.0), + Padding( + padding: const EdgeInsetsDirectional.only( + end: 6.0, + bottom: 6.0, + ), + child: VideoStatusLabel( + video: video, + device: widget.device, + ), + ), + ]), ), if (!isSubView && view.currentLayout.devices.contains(widget.device)) diff --git a/lib/widgets/device_grid/video_status_label.dart b/lib/widgets/device_grid/video_status_label.dart index 1f2dde83..b191a5ed 100644 --- a/lib/widgets/device_grid/video_status_label.dart +++ b/lib/widgets/device_grid/video_status_label.dart @@ -60,7 +60,7 @@ class _VideoStatusLabelState extends State { // It is only LIVE if it starts with rtsp or is hls (_source.startsWith('rtsp') || _source.contains('media/mjpeg') || - _source.endsWith('index.m3u8') /* hls */); + _source.contains('.m3u8') /* hls */); _VideoLabel get status => widget.video.error != null ? _VideoLabel.error diff --git a/lib/widgets/error_warning.dart b/lib/widgets/error_warning.dart index 965d3bd4..4c000f16 100644 --- a/lib/widgets/error_warning.dart +++ b/lib/widgets/error_warning.dart @@ -17,6 +17,7 @@ * along with this program. If not, see . */ +import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -27,27 +28,33 @@ class ErrorWarning extends StatelessWidget { @override Widget build(BuildContext context) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.warning, - color: Colors.white70, - size: 32.0, - ), - if (message.isNotEmpty) ...[ - const SizedBox(height: 8.0), - Text( - message, - textAlign: TextAlign.center, - style: const TextStyle( - color: Colors.white70, - fontSize: 12.0, - ), + final loc = AppLocalizations.of(context); + return IgnorePointer( + child: ColoredBox( + color: Colors.black38, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.warning, color: Colors.white, size: 32.0), + AutoSizeText( + loc.videoError, + style: const TextStyle(color: Colors.white), + maxLines: 1, ), + if (message.isNotEmpty) ...[ + const FractionallySizedBox( + widthFactor: 0.5, + child: Divider(color: Colors.white), + ), + const SizedBox(height: 8.0), + Text( + message, + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.white, fontSize: 12.0), + ), + ], ], - ], + ), ), ); }