diff --git a/README.md b/README.md index 43157aa4..28fd9834 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,8 @@ lib │ ├───video_player.dart │ └───window.dart │ +├───screens [All the screens of the application.] +│ ├───widgets [UI/UX & widgets used to display content.] │ ├───firebase_messaging_background_handler.dart [handles in-app notifications, snoozing, thumbnails etc. & other Firebase related hooks.] diff --git a/lib/firebase_messaging_background_handler.dart b/lib/firebase_messaging_background_handler.dart index 2b174b9b..e1af5bb9 100644 --- a/lib/firebase_messaging_background_handler.dart +++ b/lib/firebase_messaging_background_handler.dart @@ -28,11 +28,11 @@ import 'package:bluecherry_client/main.dart'; import 'package:bluecherry_client/models/device.dart'; import 'package:bluecherry_client/providers/server_provider.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; +import 'package:bluecherry_client/screens/events_browser/events_screen.dart'; +import 'package:bluecherry_client/screens/players/live_player.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:bluecherry_client/widgets/events/events_screen.dart'; -import 'package:bluecherry_client/widgets/player/live_player.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 864de8c8..50df9e9f 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -53,7 +53,17 @@ } } }, - "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!", + "serverNotAddedErrorDescription": "Please check the entered details and ensure the server is online.\n\nIf you are connecting remote, make sure the {port} and {rtspPort} ports are open to the Bluecherry server!", + "@serverNotAddedErrorDescription": { + "placeholders": { + "port": { + "type": "String" + }, + "rtspPort": { + "type": "String" + } + } + }, "serverAlreadyAdded": "The {serverName} server is already added", "@serverAlreadyAdded": { "placeholders": { diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 0b5ee540..81ea5567 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -49,7 +49,17 @@ "serverName": {} } }, - "serverNotAddedErrorDescription": "S.V.P. vérifiez les informations entrées et assurez-vous que le serveur est en ligne.\n\nSi vous tentez de vous connectez à distance, assurez-vous que les ports 7001 et 7002 sont ouverts vers le serveur Bluecherry!", + "serverNotAddedErrorDescription": "Please check the entered details and ensure the server is online.\n\nIf you are connecting remote, make sure the {port} and {rtspPort} ports are open to the Bluecherry server!", + "@serverNotAddedErrorDescription": { + "placeholders": { + "port": { + "type": "String" + }, + "rtspPort": { + "type": "String" + } + } + }, "serverAlreadyAdded": "Le serveur {serverName} a déjà été ajouté", "@serverAlreadyAdded": { "placeholders": { diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 951a2604..08d06486 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -53,7 +53,17 @@ } } }, - "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!", + "serverNotAddedErrorDescription": "Please check the entered details and ensure the server is online.\n\nIf you are connecting remote, make sure the {port} and {rtspPort} ports are open to the Bluecherry server!", + "@serverNotAddedErrorDescription": { + "placeholders": { + "port": { + "type": "String" + }, + "rtspPort": { + "type": "String" + } + } + }, "serverAlreadyAdded": "The {serverName} server is already added", "@serverAlreadyAdded": { "placeholders": { diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 77358ce4..123af9fe 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -53,7 +53,17 @@ } } }, - "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!", + "serverNotAddedErrorDescription": "Please check the entered details and ensure the server is online.\n\nIf you are connecting remote, make sure the {port} and {rtspPort} ports are open to the Bluecherry server!", + "@serverNotAddedErrorDescription": { + "placeholders": { + "port": { + "type": "String" + }, + "rtspPort": { + "type": "String" + } + } + }, "serverAlreadyAdded": "O {serverName} servidor já foi adicionado.", "@serverAlreadyAdded": { "placeholders": { diff --git a/lib/main.dart b/lib/main.dart index 425e5439..8bb6d838 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -36,6 +36,13 @@ import 'package:bluecherry_client/providers/mobile_view_provider.dart'; import 'package:bluecherry_client/providers/server_provider.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/providers/update_provider.dart'; +import 'package:bluecherry_client/screens/downloads/close_dialog.dart'; +import 'package:bluecherry_client/screens/events_browser/events_screen.dart'; +import 'package:bluecherry_client/screens/home.dart'; +import 'package:bluecherry_client/screens/multi_window/single_camera_window.dart'; +import 'package:bluecherry_client/screens/multi_window/single_layout_window.dart'; +import 'package:bluecherry_client/screens/multi_window/window.dart'; +import 'package:bluecherry_client/screens/players/live_player.dart'; import 'package:bluecherry_client/utils/app_links/app_links.dart' as app_links; import 'package:bluecherry_client/utils/logging.dart' as logging; import 'package:bluecherry_client/utils/methods.dart'; @@ -44,13 +51,6 @@ import 'package:bluecherry_client/utils/theme.dart'; import 'package:bluecherry_client/utils/video_player.dart'; import 'package:bluecherry_client/utils/window.dart'; import 'package:bluecherry_client/widgets/desktop_buttons.dart'; -import 'package:bluecherry_client/widgets/downloads_manager.dart'; -import 'package:bluecherry_client/widgets/events/events_screen.dart'; -import 'package:bluecherry_client/widgets/home.dart'; -import 'package:bluecherry_client/widgets/multi_window/single_camera_window.dart'; -import 'package:bluecherry_client/widgets/multi_window/single_layout_window.dart'; -import 'package:bluecherry_client/widgets/multi_window/window.dart'; -import 'package:bluecherry_client/widgets/player/live_player.dart'; import 'package:bluecherry_client/widgets/splash_screen.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -251,11 +251,7 @@ class _UnityAppState extends State if (isPreventClose && mounted && context.mounted) { final downloadsManager = context.read(); if (downloadsManager.downloading.isNotEmpty) { - final result = await showDialog( - context: context, - barrierDismissible: false, - builder: (context) => const CloseDownloadsDialog(), - ); + final result = await showCloseDownloadsDialog(context); if (result == null || !result) { return; } diff --git a/lib/models/device.dart b/lib/models/device.dart index 01ea4901..c4c510f4 100644 --- a/lib/models/device.dart +++ b/lib/models/device.dart @@ -23,9 +23,9 @@ import 'package:bluecherry_client/api/api.dart'; import 'package:bluecherry_client/models/server.dart'; import 'package:bluecherry_client/providers/server_provider.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; +import 'package:bluecherry_client/screens/layouts/desktop/external_stream.dart'; import 'package:bluecherry_client/utils/config.dart'; import 'package:bluecherry_client/utils/extensions.dart'; -import 'package:bluecherry_client/widgets/device_grid/desktop/external_stream.dart'; import 'package:flutter/foundation.dart'; class ExternalDeviceData { diff --git a/lib/models/event.dart b/lib/models/event.dart index 8287e88c..191e8661 100644 --- a/lib/models/event.dart +++ b/lib/models/event.dart @@ -67,7 +67,7 @@ class Event { .last .trim() .split(' ') - .map((e) => e.uppercaseFirst()) + .map((e) => e.uppercaseFirst) .join(' '); } diff --git a/lib/widgets/direct_camera.dart b/lib/screens/direct_camera.dart similarity index 98% rename from lib/widgets/direct_camera.dart rename to lib/screens/direct_camera.dart index 8941dd0b..88095be0 100644 --- a/lib/widgets/direct_camera.dart +++ b/lib/screens/direct_camera.dart @@ -24,6 +24,7 @@ import 'package:bluecherry_client/utils/constants.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/utils/theme.dart'; import 'package:bluecherry_client/utils/video_player.dart'; +import 'package:bluecherry_client/widgets/drawer_button.dart'; import 'package:bluecherry_client/widgets/error_warning.dart'; import 'package:bluecherry_client/widgets/misc.dart'; import 'package:bluecherry_client/widgets/search.dart'; @@ -201,7 +202,7 @@ class _DevicesForServer extends StatelessWidget { : Icons.videocam_off_outlined, ), ), - title: Text(device.name.uppercaseFirst()), + title: Text(device.name.uppercaseFirst), subtitle: Text([ device.uri, '${device.resolutionX}x${device.resolutionY}', diff --git a/lib/screens/downloads/close_dialog.dart b/lib/screens/downloads/close_dialog.dart new file mode 100644 index 00000000..7ca36302 --- /dev/null +++ b/lib/screens/downloads/close_dialog.dart @@ -0,0 +1,93 @@ +/* + * 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/downloads_provider.dart'; +import 'package:bluecherry_client/screens/downloads/indicators.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:provider/provider.dart'; + +Future showCloseDownloadsDialog(BuildContext context) async { + return showDialog( + context: context, + barrierDismissible: false, + builder: (context) => const CloseDownloadsDialog(), + ); +} + +/// The dialog that appears when the user tries to close the app when there are +/// downloads in progress. +class CloseDownloadsDialog extends StatefulWidget { + const CloseDownloadsDialog({super.key}); + + @override + State createState() => _CloseDownloadsDialogState(); +} + +class _CloseDownloadsDialogState extends State { + bool _closeWhenDone = false; + + @override + Widget build(BuildContext context) { + final downloadsManager = context.watch(); + final loc = AppLocalizations.of(context); + final navigator = Navigator.of(context); + + return AlertDialog( + title: Text(loc.nDownloadsProgress(downloadsManager.downloading.length)), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (final entry in downloadsManager.downloading.entries) + ListTile( + dense: true, + contentPadding: EdgeInsets.zero, + trailing: SizedBox.fromSize( + size: const Size(40.0, 40.0), + child: DownloadProgressIndicator(progress: entry.value), + ), + title: Text(entry.key.deviceName), + subtitle: Text(entry.key.server.name), + ) + ], + ), + actions: [ + TextButton( + onPressed: () => navigator.pop(false), + child: Text(loc.cancel), + ), + OutlinedButton( + onPressed: _closeWhenDone ? null : () => navigator.pop(true), + child: Text(loc.closeAnyway), + ), + FilledButton( + onPressed: _closeWhenDone + ? null + : () async { + setState(() => _closeWhenDone = true); + await downloadsManager.downloadsCompleter?.future; + navigator.pop(true); + }, + child: Text(loc.closeWhenDone), + ), + ], + ); + } +} diff --git a/lib/widgets/downloads_manager.dart b/lib/screens/downloads/downloads_manager.dart similarity index 71% rename from lib/widgets/downloads_manager.dart rename to lib/screens/downloads/downloads_manager.dart index d5539ec3..539c3e16 100644 --- a/lib/widgets/downloads_manager.dart +++ b/lib/screens/downloads/downloads_manager.dart @@ -23,13 +23,12 @@ import 'package:bluecherry_client/models/event.dart'; import 'package:bluecherry_client/providers/downloads_provider.dart'; import 'package:bluecherry_client/providers/home_provider.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; +import 'package:bluecherry_client/screens/downloads/indicators.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/utils/methods.dart'; import 'package:bluecherry_client/utils/theme.dart'; -import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; import 'package:bluecherry_client/utils/window.dart'; -import 'package:bluecherry_client/widgets/misc.dart'; -import 'package:flutter/cupertino.dart'; +import 'package:bluecherry_client/widgets/drawer_button.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -155,7 +154,7 @@ class _DownloadTileState extends State { final loc = AppLocalizations.of(context); final settings = context.watch(); - final eventType = widget.event.type.locale(context).uppercaseFirst(); + final eventType = widget.event.type.locale(context).uppercaseFirst; final at = settings.formatDate(widget.event.published); final shape = RoundedRectangleBorder( @@ -336,128 +335,6 @@ class _DownloadTileState extends State { } } -class DownloadProgressIndicator extends StatelessWidget { - const DownloadProgressIndicator({ - super.key, - required this.progress, - this.color, - }); - - final DownloadProgress progress; - - /// The color of the indicator. - /// - /// If not provided, the primary color is used instead. - final Color? color; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - if (isCupertino) { - return CupertinoActivityIndicator.partiallyRevealed( - progress: progress, - ); - } - - return Stack(children: [ - Padding( - padding: const EdgeInsetsDirectional.all(8.0), - child: CircularProgressIndicator( - value: progress, - strokeWidth: 2.0, - color: color, - ), - ), - Center( - child: Icon( - Icons.download, - size: 14.0, - color: color ?? theme.colorScheme.primary, - ), - ), - ]); - } -} - -/// The download indicator for the given event -/// -/// See also: -/// * [EventsScreenDesktop], which uses this to display the download status -class DownloadIndicator extends StatelessWidget { - final Event event; - - /// Whether to highlight the indicator with a white color and an outline border - final bool highlight; - - /// Whether the indicator is small - final bool small; - - const DownloadIndicator({ - super.key, - required this.event, - this.highlight = false, - this.small = false, - }); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final loc = AppLocalizations.of(context); - - return SizedBox( - height: 40.0, - width: 40.0, - child: () { - if (event.isAlarm) { - return Icon( - Icons.warning, - color: theme.extension()!.warningColor, - size: small ? 18.0 : null, - ); - } - - final downloads = context.watch(); - - if (downloads.isEventDownloaded(event.id)) { - return SquaredIconButton( - onPressed: () { - context.read().toDownloads(event.id, context); - }, - tooltip: loc.seeInDownloads, - icon: Icon( - Icons.download_done, - size: small ? 18.0 : null, - color: theme.extension()!.successColor, - ), - ); - } - - if (downloads.isEventDownloading(event.id)) { - return DownloadProgressIndicator( - progress: downloads.downloading[downloads.downloading.keys - .firstWhere((e) => e.id == event.id)]!, - color: highlight ? Colors.amber : null, - ); - } - - if (event.mediaURL != null) { - return SquaredIconButton( - tooltip: loc.download, - onPressed: () => downloads.download(event), - icon: Icon( - Icons.download, - size: small ? 18.0 : 22.0, - color: highlight ? Colors.white : null, - shadows: highlight ? outlinedText() : null, - ), - ); - } - }(), - ); - } -} - class NoDownloads extends StatelessWidget { const NoDownloads({super.key}); @@ -489,64 +366,3 @@ class NoDownloads extends StatelessWidget { ); } } - -/// The dialog that appears when the user tries to close the app when there are -/// downloads in progress. -class CloseDownloadsDialog extends StatefulWidget { - const CloseDownloadsDialog({super.key}); - - @override - State createState() => _CloseDownloadsDialogState(); -} - -class _CloseDownloadsDialogState extends State { - bool _closeWhenDone = false; - - @override - Widget build(BuildContext context) { - final downloadsManager = context.watch(); - final loc = AppLocalizations.of(context); - final navigator = Navigator.of(context); - - return AlertDialog( - title: Text(loc.nDownloadsProgress(downloadsManager.downloading.length)), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - for (final entry in downloadsManager.downloading.entries) - ListTile( - dense: true, - contentPadding: EdgeInsets.zero, - trailing: SizedBox.fromSize( - size: const Size(40.0, 40.0), - child: DownloadProgressIndicator(progress: entry.value), - ), - title: Text(entry.key.deviceName), - subtitle: Text(entry.key.server.name), - ) - ], - ), - actions: [ - TextButton( - onPressed: () => navigator.pop(false), - child: Text(loc.cancel), - ), - OutlinedButton( - onPressed: _closeWhenDone ? null : () => navigator.pop(true), - child: Text(loc.closeAnyway), - ), - FilledButton( - onPressed: _closeWhenDone - ? null - : () async { - setState(() => _closeWhenDone = true); - await downloadsManager.downloadsCompleter?.future; - navigator.pop(true); - }, - child: Text(loc.closeWhenDone), - ), - ], - ); - } -} diff --git a/lib/screens/downloads/indicators.dart b/lib/screens/downloads/indicators.dart new file mode 100644 index 00000000..dbb8f2d5 --- /dev/null +++ b/lib/screens/downloads/indicators.dart @@ -0,0 +1,152 @@ +/* + * 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/models/event.dart'; +import 'package:bluecherry_client/providers/downloads_provider.dart'; +import 'package:bluecherry_client/providers/home_provider.dart'; +import 'package:bluecherry_client/utils/methods.dart'; +import 'package:bluecherry_client/utils/theme.dart'; +import 'package:bluecherry_client/widgets/misc.dart'; +import 'package:bluecherry_client/widgets/squared_icon_button.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:provider/provider.dart'; + +class DownloadProgressIndicator extends StatelessWidget { + const DownloadProgressIndicator({ + super.key, + required this.progress, + this.color, + }); + + final DownloadProgress progress; + + /// The color of the indicator. + /// + /// If not provided, the primary color is used instead. + final Color? color; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + if (isCupertino) { + return CupertinoActivityIndicator.partiallyRevealed( + progress: progress, + ); + } + + return Stack(children: [ + Padding( + padding: const EdgeInsetsDirectional.all(8.0), + child: CircularProgressIndicator( + value: progress, + strokeWidth: 2.0, + color: color, + ), + ), + Center( + child: Icon( + Icons.download, + size: 14.0, + color: color ?? theme.colorScheme.primary, + ), + ), + ]); + } +} + +/// The download indicator for the given event +/// +/// See also: +/// * [EventsScreenDesktop], which uses this to display the download status +class DownloadIndicator extends StatelessWidget { + final Event event; + + /// Whether to highlight the indicator with a white color and an outline border + final bool highlight; + + /// Whether the indicator is small + final bool small; + + const DownloadIndicator({ + super.key, + required this.event, + this.highlight = false, + this.small = false, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final loc = AppLocalizations.of(context); + + return SizedBox( + height: 40.0, + width: 40.0, + child: () { + if (event.isAlarm) { + return Icon( + Icons.warning, + color: theme.extension()!.warningColor, + size: small ? 18.0 : null, + ); + } + + final downloads = context.watch(); + + if (downloads.isEventDownloaded(event.id)) { + return SquaredIconButton( + onPressed: () { + context.read().toDownloads(event.id, context); + }, + tooltip: loc.seeInDownloads, + icon: Icon( + Icons.download_done, + size: small ? 18.0 : null, + color: theme.extension()!.successColor, + ), + ); + } + + if (downloads.isEventDownloading(event.id)) { + return DownloadProgressIndicator( + progress: downloads.downloading[downloads.downloading.keys + .firstWhere((e) => e.id == event.id)]!, + color: highlight ? Colors.amber : null, + ); + } + + if (event.mediaURL != null) { + return SquaredIconButton( + tooltip: loc.download, + onPressed: () => downloads.download(event), + icon: Icon( + Icons.download, + size: small ? 18.0 : 22.0, + color: highlight ? Colors.white : null, + shadows: highlight ? outlinedText() : null, + ), + ); + } + }(), + ); + } +} diff --git a/lib/widgets/events/events_screen.dart b/lib/screens/events_browser/events_screen.dart similarity index 97% rename from lib/widgets/events/events_screen.dart rename to lib/screens/events_browser/events_screen.dart index 04029959..e5b212c9 100644 --- a/lib/widgets/events/events_screen.dart +++ b/lib/screens/events_browser/events_screen.dart @@ -27,18 +27,19 @@ import 'package:bluecherry_client/providers/downloads_provider.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/screens/downloads/indicators.dart'; +import 'package:bluecherry_client/screens/events_browser/filter.dart'; +import 'package:bluecherry_client/screens/players/event_player_desktop.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/utils/widgets/squared_icon_button.dart'; import 'package:bluecherry_client/widgets/collapsable_sidebar.dart'; import 'package:bluecherry_client/widgets/desktop_buttons.dart'; -import 'package:bluecherry_client/widgets/downloads_manager.dart'; +import 'package:bluecherry_client/widgets/drawer_button.dart'; import 'package:bluecherry_client/widgets/error_warning.dart'; -import 'package:bluecherry_client/widgets/events/event_player_desktop.dart'; -import 'package:bluecherry_client/widgets/events/filter.dart'; import 'package:bluecherry_client/widgets/misc.dart'; import 'package:bluecherry_client/widgets/search.dart'; +import 'package:bluecherry_client/widgets/squared_icon_button.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -47,7 +48,7 @@ import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:unity_video_player/unity_video_player.dart'; -part 'event_player_mobile.dart'; +part '../players/event_player_mobile.dart'; part 'events_screen_desktop.dart'; part 'events_screen_mobile.dart'; @@ -262,7 +263,7 @@ class EventsScreenState extends State { // items: EventsMinLevelFilter.values.map((level) { // return DropdownMenuItem( // value: level, - // child: Text(level.name.uppercaseFirst()), + // child: Text(level.name.uppercaseFirst), // ); // }).toList(), // onChanged: (v) => setState( diff --git a/lib/widgets/events/events_screen_desktop.dart b/lib/screens/events_browser/events_screen_desktop.dart similarity index 97% rename from lib/widgets/events/events_screen_desktop.dart rename to lib/screens/events_browser/events_screen_desktop.dart index 87413af1..bd5d12c5 100644 --- a/lib/widgets/events/events_screen_desktop.dart +++ b/lib/screens/events_browser/events_screen_desktop.dart @@ -95,16 +95,15 @@ class EventsScreenDesktop extends StatelessWidget { _buildTilePart(child: Text(event.server.name), flex: 2), _buildTilePart(child: Text(event.deviceName)), _buildTilePart( - child: Text(event.type.locale(context).uppercaseFirst()), + child: Text(event.type.locale(context).uppercaseFirst), ), _buildTilePart( child: Text(event.duration .humanReadableCompact(context) - .uppercaseFirst()), + .uppercaseFirst), ), _buildTilePart( - child: - Text(event.priority.locale(context).uppercaseFirst()), + child: Text(event.priority.locale(context).uppercaseFirst), ), _buildTilePart( child: Text( diff --git a/lib/widgets/events/events_screen_mobile.dart b/lib/screens/events_browser/events_screen_mobile.dart similarity index 100% rename from lib/widgets/events/events_screen_mobile.dart rename to lib/screens/events_browser/events_screen_mobile.dart diff --git a/lib/widgets/events/filter.dart b/lib/screens/events_browser/filter.dart similarity index 97% rename from lib/widgets/events/filter.dart rename to lib/screens/events_browser/filter.dart index 23c07ecd..bea2b908 100644 --- a/lib/widgets/events/filter.dart +++ b/lib/screens/events_browser/filter.dart @@ -18,11 +18,11 @@ */ import 'package:bluecherry_client/providers/server_provider.dart'; +import 'package:bluecherry_client/screens/events_browser/events_screen.dart'; import 'package:bluecherry_client/utils/extensions.dart'; -import 'package:bluecherry_client/utils/widgets/tree_view.dart'; -import 'package:bluecherry_client/widgets/events/events_screen.dart'; import 'package:bluecherry_client/widgets/misc.dart'; import 'package:bluecherry_client/widgets/search.dart'; +import 'package:bluecherry_client/widgets/tree_view.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; @@ -194,7 +194,7 @@ class _MobileFilterSheetState extends State { items: EventsMinLevelFilter.values.map((level) { return DropdownMenuItem( value: level, - child: Text(level.name.uppercaseFirst()), + child: Text(level.name.uppercaseFirst), ); }).toList(), onChanged: (filter) { diff --git a/lib/widgets/events_timeline/desktop/timeline.dart b/lib/screens/events_timeline/desktop/timeline.dart similarity index 99% rename from lib/widgets/events_timeline/desktop/timeline.dart rename to lib/screens/events_timeline/desktop/timeline.dart index 469ae253..353fcf54 100644 --- a/lib/widgets/events_timeline/desktop/timeline.dart +++ b/lib/screens/events_timeline/desktop/timeline.dart @@ -24,17 +24,17 @@ import 'package:bluecherry_client/models/device.dart'; import 'package:bluecherry_client/models/event.dart'; import 'package:bluecherry_client/providers/home_provider.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; +import 'package:bluecherry_client/screens/events_browser/events_screen.dart'; +import 'package:bluecherry_client/screens/events_timeline/desktop/timeline_card.dart'; +import 'package:bluecherry_client/screens/layouts/device_grid.dart' + show calculateCrossAxisCount; 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/utils/widgets/squared_icon_button.dart'; import 'package:bluecherry_client/widgets/collapsable_sidebar.dart'; -import 'package:bluecherry_client/widgets/device_grid/device_grid.dart' - show calculateCrossAxisCount; -import 'package:bluecherry_client/widgets/events/events_screen.dart'; -import 'package:bluecherry_client/widgets/events_timeline/desktop/timeline_card.dart'; import 'package:bluecherry_client/widgets/misc.dart'; import 'package:bluecherry_client/widgets/reorderable_static_grid.dart'; +import 'package:bluecherry_client/widgets/squared_icon_button.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/events_timeline/desktop/timeline_card.dart b/lib/screens/events_timeline/desktop/timeline_card.dart similarity index 96% rename from lib/widgets/events_timeline/desktop/timeline_card.dart rename to lib/screens/events_timeline/desktop/timeline_card.dart index 4eb6576d..871b49ec 100644 --- a/lib/widgets/events_timeline/desktop/timeline_card.dart +++ b/lib/screens/events_timeline/desktop/timeline_card.dart @@ -20,14 +20,13 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:bluecherry_client/providers/downloads_provider.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; +import 'package:bluecherry_client/screens/downloads/indicators.dart'; +import 'package:bluecherry_client/screens/events_timeline/desktop/timeline.dart'; +import 'package:bluecherry_client/screens/layouts/video_status_label.dart'; import 'package:bluecherry_client/utils/extensions.dart'; -import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; -import 'package:bluecherry_client/widgets/device_grid/video_status_label.dart'; -import 'package:bluecherry_client/widgets/downloads_manager.dart'; -import 'package:bluecherry_client/widgets/events_timeline/desktop/timeline.dart'; import 'package:bluecherry_client/widgets/hover_button.dart'; import 'package:bluecherry_client/widgets/misc.dart'; -import 'package:bluecherry_client/widgets/player/widgets.dart'; +import 'package:bluecherry_client/widgets/squared_icon_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/events_timeline/desktop/timeline_sidebar.dart b/lib/screens/events_timeline/desktop/timeline_sidebar.dart similarity index 97% rename from lib/widgets/events_timeline/desktop/timeline_sidebar.dart rename to lib/screens/events_timeline/desktop/timeline_sidebar.dart index 2e8ced7b..3883bd05 100644 --- a/lib/widgets/events_timeline/desktop/timeline_sidebar.dart +++ b/lib/screens/events_timeline/desktop/timeline_sidebar.dart @@ -18,9 +18,9 @@ */ import 'package:auto_size_text/auto_size_text.dart'; +import 'package:bluecherry_client/screens/events_browser/filter.dart'; +import 'package:bluecherry_client/screens/events_timeline/events_playback.dart'; import 'package:bluecherry_client/widgets/collapsable_sidebar.dart'; -import 'package:bluecherry_client/widgets/events/filter.dart'; -import 'package:bluecherry_client/widgets/events_timeline/events_playback.dart'; import 'package:bluecherry_client/widgets/misc.dart'; import 'package:bluecherry_client/widgets/search.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/events_timeline/events_playback.dart b/lib/screens/events_timeline/events_playback.dart similarity index 96% rename from lib/widgets/events_timeline/events_playback.dart rename to lib/screens/events_timeline/events_playback.dart index dfd0b3e1..a57248ba 100644 --- a/lib/widgets/events_timeline/events_playback.dart +++ b/lib/screens/events_timeline/events_playback.dart @@ -22,12 +22,12 @@ import 'dart:io'; import 'package:bluecherry_client/models/device.dart'; import 'package:bluecherry_client/models/event.dart'; import 'package:bluecherry_client/providers/downloads_provider.dart'; +import 'package:bluecherry_client/screens/events_browser/events_screen.dart'; +import 'package:bluecherry_client/screens/events_timeline/desktop/timeline.dart'; +import 'package:bluecherry_client/screens/events_timeline/desktop/timeline_sidebar.dart'; +import 'package:bluecherry_client/screens/events_timeline/mobile/timeline_device_view.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/utils/methods.dart'; -import 'package:bluecherry_client/widgets/events/events_screen.dart'; -import 'package:bluecherry_client/widgets/events_timeline/desktop/timeline.dart'; -import 'package:bluecherry_client/widgets/events_timeline/desktop/timeline_sidebar.dart'; -import 'package:bluecherry_client/widgets/events_timeline/mobile/timeline_device_view.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/events_timeline/mobile/timeline_device_view.dart b/lib/screens/events_timeline/mobile/timeline_device_view.dart similarity index 98% rename from lib/widgets/events_timeline/mobile/timeline_device_view.dart rename to lib/screens/events_timeline/mobile/timeline_device_view.dart index 22753b2c..82890392 100644 --- a/lib/widgets/events_timeline/mobile/timeline_device_view.dart +++ b/lib/screens/events_timeline/mobile/timeline_device_view.dart @@ -24,15 +24,15 @@ import 'package:bluecherry_client/models/event.dart'; import 'package:bluecherry_client/providers/downloads_provider.dart'; import 'package:bluecherry_client/providers/home_provider.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; +import 'package:bluecherry_client/screens/downloads/indicators.dart'; +import 'package:bluecherry_client/screens/events_timeline/desktop/timeline.dart'; +import 'package:bluecherry_client/screens/events_timeline/events_playback.dart'; import 'package:bluecherry_client/utils/constants.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/utils/theme.dart'; -import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; -import 'package:bluecherry_client/widgets/device_selector_screen.dart'; -import 'package:bluecherry_client/widgets/downloads_manager.dart'; -import 'package:bluecherry_client/widgets/events_timeline/desktop/timeline.dart'; -import 'package:bluecherry_client/widgets/events_timeline/events_playback.dart'; +import 'package:bluecherry_client/widgets/device_selector.dart'; import 'package:bluecherry_client/widgets/misc.dart'; +import 'package:bluecherry_client/widgets/squared_icon_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; @@ -86,7 +86,7 @@ class _TimelineDeviceViewState extends State { /// Select a device to show on the timeline Future selectDevice(BuildContext context) async { - device = await showDeviceSelectorScreen( + device = await showDeviceSelector( context, available: widget.timeline.tiles.map((t) => t.device), selected: [if (tile != null) tile!.device], diff --git a/lib/widgets/home.dart b/lib/screens/home.dart similarity index 96% rename from lib/widgets/home.dart rename to lib/screens/home.dart index e258a4e9..08cf2cc2 100644 --- a/lib/widgets/home.dart +++ b/lib/screens/home.dart @@ -19,17 +19,17 @@ import 'package:animations/animations.dart'; import 'package:bluecherry_client/providers/home_provider.dart'; +import 'package:bluecherry_client/screens/direct_camera.dart'; +import 'package:bluecherry_client/screens/downloads/downloads_manager.dart'; +import 'package:bluecherry_client/screens/events_browser/events_screen.dart'; +import 'package:bluecherry_client/screens/events_timeline/events_playback.dart'; +import 'package:bluecherry_client/screens/layouts/device_grid.dart'; +import 'package:bluecherry_client/screens/servers/wizard.dart'; +import 'package:bluecherry_client/screens/settings/settings.dart'; import 'package:bluecherry_client/utils/constants.dart'; import 'package:bluecherry_client/utils/methods.dart'; import 'package:bluecherry_client/widgets/desktop_buttons.dart'; -import 'package:bluecherry_client/widgets/device_grid/device_grid.dart'; -import 'package:bluecherry_client/widgets/direct_camera.dart'; -import 'package:bluecherry_client/widgets/downloads_manager.dart'; -import 'package:bluecherry_client/widgets/events/events_screen.dart'; -import 'package:bluecherry_client/widgets/events_timeline/events_playback.dart'; import 'package:bluecherry_client/widgets/search.dart'; -import 'package:bluecherry_client/widgets/servers/add_server.dart'; -import 'package:bluecherry_client/widgets/settings/settings.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; diff --git a/lib/widgets/device_grid/desktop/desktop_device_grid.dart b/lib/screens/layouts/desktop/desktop_device_grid.dart similarity index 100% rename from lib/widgets/device_grid/desktop/desktop_device_grid.dart rename to lib/screens/layouts/desktop/desktop_device_grid.dart diff --git a/lib/widgets/device_grid/desktop/desktop_sidebar.dart b/lib/screens/layouts/desktop/desktop_sidebar.dart similarity index 99% rename from lib/widgets/device_grid/desktop/desktop_sidebar.dart rename to lib/screens/layouts/desktop/desktop_sidebar.dart index 653a8499..a005ed74 100644 --- a/lib/widgets/device_grid/desktop/desktop_sidebar.dart +++ b/lib/screens/layouts/desktop/desktop_sidebar.dart @@ -344,7 +344,7 @@ class _DesktopDeviceSelectorTileState extends State { ), Expanded( child: AutoSizeText( - widget.device.name.uppercaseFirst(), + widget.device.name.uppercaseFirst, maxLines: 1, style: theme.textTheme.titleMedium!.copyWith( color: widget.selected diff --git a/lib/widgets/device_grid/desktop/device_info_dialog.dart b/lib/screens/layouts/desktop/device_info_dialog.dart similarity index 98% rename from lib/widgets/device_grid/desktop/device_info_dialog.dart rename to lib/screens/layouts/desktop/device_info_dialog.dart index b002dc54..cc76d7fb 100644 --- a/lib/widgets/device_grid/desktop/device_info_dialog.dart +++ b/lib/screens/layouts/desktop/device_info_dialog.dart @@ -19,7 +19,7 @@ import 'package:bluecherry_client/models/device.dart'; import 'package:bluecherry_client/utils/theme.dart'; -import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; +import 'package:bluecherry_client/widgets/squared_icon_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; diff --git a/lib/widgets/device_grid/desktop/external_stream.dart b/lib/screens/layouts/desktop/external_stream.dart similarity index 100% rename from lib/widgets/device_grid/desktop/external_stream.dart rename to lib/screens/layouts/desktop/external_stream.dart diff --git a/lib/widgets/device_grid/desktop/layout_manager.dart b/lib/screens/layouts/desktop/layout_manager.dart similarity index 99% rename from lib/widgets/device_grid/desktop/layout_manager.dart rename to lib/screens/layouts/desktop/layout_manager.dart index 16fc682e..0889471b 100644 --- a/lib/widgets/device_grid/desktop/layout_manager.dart +++ b/lib/screens/layouts/desktop/layout_manager.dart @@ -24,11 +24,11 @@ import 'package:bluecherry_client/models/layout.dart'; import 'package:bluecherry_client/providers/desktop_view_provider.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/utils/methods.dart'; -import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; import 'package:bluecherry_client/utils/window.dart'; import 'package:bluecherry_client/widgets/hover_button.dart'; import 'package:bluecherry_client/widgets/misc.dart'; import 'package:bluecherry_client/widgets/search.dart'; +import 'package:bluecherry_client/widgets/squared_icon_button.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/device_grid/desktop/multicast_view.dart b/lib/screens/layouts/desktop/multicast_view.dart similarity index 100% rename from lib/widgets/device_grid/desktop/multicast_view.dart rename to lib/screens/layouts/desktop/multicast_view.dart diff --git a/lib/widgets/device_grid/desktop/stream_data.dart b/lib/screens/layouts/desktop/stream_data.dart similarity index 99% rename from lib/widgets/device_grid/desktop/stream_data.dart rename to lib/screens/layouts/desktop/stream_data.dart index cbb7fec6..3826a2d9 100644 --- a/lib/widgets/device_grid/desktop/stream_data.dart +++ b/lib/screens/layouts/desktop/stream_data.dart @@ -21,9 +21,9 @@ import 'dart:async'; import 'package:bluecherry_client/models/device.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; +import 'package:bluecherry_client/screens/layouts/desktop/external_stream.dart'; import 'package:bluecherry_client/utils/config.dart'; import 'package:bluecherry_client/utils/extensions.dart'; -import 'package:bluecherry_client/widgets/device_grid/desktop/external_stream.dart'; import 'package:bluecherry_client/widgets/ptz.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/device_grid/device_grid.dart b/lib/screens/layouts/device_grid.dart similarity index 81% rename from lib/widgets/device_grid/device_grid.dart rename to lib/screens/layouts/device_grid.dart index c1898aa6..e1ec73ee 100644 --- a/lib/widgets/device_grid/device_grid.dart +++ b/lib/screens/layouts/device_grid.dart @@ -29,28 +29,29 @@ import 'package:bluecherry_client/providers/home_provider.dart'; import 'package:bluecherry_client/providers/mobile_view_provider.dart'; import 'package:bluecherry_client/providers/server_provider.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; +import 'package:bluecherry_client/screens/layouts/desktop/device_info_dialog.dart'; +import 'package:bluecherry_client/screens/layouts/desktop/external_stream.dart'; +import 'package:bluecherry_client/screens/layouts/desktop/layout_manager.dart'; +import 'package:bluecherry_client/screens/layouts/desktop/multicast_view.dart'; +import 'package:bluecherry_client/screens/layouts/desktop/stream_data.dart'; +import 'package:bluecherry_client/screens/layouts/mobile/device_view.dart'; +import 'package:bluecherry_client/screens/layouts/video_status_label.dart'; +import 'package:bluecherry_client/screens/multi_window/window.dart'; import 'package:bluecherry_client/utils/app_links/app_links.dart' as app_links; 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/utils/theme.dart'; import 'package:bluecherry_client/utils/video_player.dart'; -import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; import 'package:bluecherry_client/utils/window.dart'; import 'package:bluecherry_client/widgets/collapsable_sidebar.dart'; -import 'package:bluecherry_client/widgets/device_grid/desktop/device_info_dialog.dart'; -import 'package:bluecherry_client/widgets/device_grid/desktop/external_stream.dart'; -import 'package:bluecherry_client/widgets/device_grid/desktop/layout_manager.dart'; -import 'package:bluecherry_client/widgets/device_grid/desktop/multicast_view.dart'; -import 'package:bluecherry_client/widgets/device_grid/desktop/stream_data.dart'; -import 'package:bluecherry_client/widgets/device_grid/mobile/device_view.dart'; -import 'package:bluecherry_client/widgets/device_grid/video_status_label.dart'; +import 'package:bluecherry_client/widgets/drawer_button.dart'; import 'package:bluecherry_client/widgets/error_warning.dart'; import 'package:bluecherry_client/widgets/hover_button.dart'; import 'package:bluecherry_client/widgets/misc.dart'; -import 'package:bluecherry_client/widgets/multi_window/window.dart'; import 'package:bluecherry_client/widgets/ptz.dart'; import 'package:bluecherry_client/widgets/reorderable_static_grid.dart'; +import 'package:bluecherry_client/widgets/squared_icon_button.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; diff --git a/lib/widgets/device_grid/mobile/device_view.dart b/lib/screens/layouts/mobile/device_view.dart similarity index 96% rename from lib/widgets/device_grid/mobile/device_view.dart rename to lib/screens/layouts/mobile/device_view.dart index ac9ef59a..a6faeb4d 100644 --- a/lib/widgets/device_grid/mobile/device_view.dart +++ b/lib/screens/layouts/mobile/device_view.dart @@ -20,13 +20,13 @@ import 'package:bluecherry_client/models/device.dart'; import 'package:bluecherry_client/providers/mobile_view_provider.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; +import 'package:bluecherry_client/screens/layouts/video_status_label.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/utils/video_player.dart'; -import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; -import 'package:bluecherry_client/widgets/device_grid/video_status_label.dart'; -import 'package:bluecherry_client/widgets/device_selector_screen.dart'; +import 'package:bluecherry_client/widgets/device_selector.dart'; import 'package:bluecherry_client/widgets/error_warning.dart'; import 'package:bluecherry_client/widgets/misc.dart'; +import 'package:bluecherry_client/widgets/squared_icon_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; @@ -124,8 +124,7 @@ class _MobileDeviceViewState extends State { child: Text(loc.replaceCamera), onTap: () async { if (mounted) { - final device = - await showDeviceSelectorScreen(context); + final device = await showDeviceSelector(context); if (device != null) { view.replace(widget.tab, widget.index, device); } @@ -164,7 +163,7 @@ class _MobileDeviceViewState extends State { onTap: () async { final result = await Navigator.of(context).push( MaterialPageRoute( - builder: (context) => DeviceSelectorScreen(selected: selected), + builder: (context) => DeviceSelector(selected: selected), ), ); if (result is Device) { @@ -331,7 +330,7 @@ class DeviceTileState extends State { Text( widget.device.name .split(' ') - .map((word) => word.uppercaseFirst()) + .map((word) => word.uppercaseFirst) .join(' '), style: theme.textTheme.displayLarge?.copyWith( color: Colors.white, diff --git a/lib/widgets/device_grid/mobile/mobile_device_grid.dart b/lib/screens/layouts/mobile/mobile_device_grid.dart similarity index 100% rename from lib/widgets/device_grid/mobile/mobile_device_grid.dart rename to lib/screens/layouts/mobile/mobile_device_grid.dart diff --git a/lib/widgets/device_grid/video_status_label.dart b/lib/screens/layouts/video_status_label.dart similarity index 100% rename from lib/widgets/device_grid/video_status_label.dart rename to lib/screens/layouts/video_status_label.dart diff --git a/lib/widgets/multi_window/single_camera_window.dart b/lib/screens/multi_window/single_camera_window.dart similarity index 97% rename from lib/widgets/multi_window/single_camera_window.dart rename to lib/screens/multi_window/single_camera_window.dart index 6710fef1..c3d63a3f 100644 --- a/lib/widgets/multi_window/single_camera_window.dart +++ b/lib/screens/multi_window/single_camera_window.dart @@ -21,8 +21,8 @@ import 'dart:async'; import 'package:bluecherry_client/models/device.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; +import 'package:bluecherry_client/screens/layouts/device_grid.dart'; import 'package:bluecherry_client/widgets/desktop_buttons.dart'; -import 'package:bluecherry_client/widgets/device_grid/device_grid.dart'; import 'package:flutter/material.dart'; import 'package:unity_video_player/unity_video_player.dart'; diff --git a/lib/widgets/multi_window/single_layout_window.dart b/lib/screens/multi_window/single_layout_window.dart similarity index 95% rename from lib/widgets/multi_window/single_layout_window.dart rename to lib/screens/multi_window/single_layout_window.dart index 407a6085..7880f7a2 100644 --- a/lib/widgets/multi_window/single_layout_window.dart +++ b/lib/screens/multi_window/single_layout_window.dart @@ -18,8 +18,8 @@ */ import 'package:bluecherry_client/models/layout.dart'; +import 'package:bluecherry_client/screens/layouts/device_grid.dart'; import 'package:bluecherry_client/widgets/desktop_buttons.dart'; -import 'package:bluecherry_client/widgets/device_grid/device_grid.dart'; import 'package:flutter/material.dart'; class AlternativeLayoutView extends StatelessWidget { diff --git a/lib/widgets/multi_window/window.dart b/lib/screens/multi_window/window.dart similarity index 100% rename from lib/widgets/multi_window/window.dart rename to lib/screens/multi_window/window.dart diff --git a/lib/widgets/events/event_player_desktop.dart b/lib/screens/players/event_player_desktop.dart similarity index 98% rename from lib/widgets/events/event_player_desktop.dart rename to lib/screens/players/event_player_desktop.dart index d337edde..f6d6a54e 100644 --- a/lib/widgets/events/event_player_desktop.dart +++ b/lib/screens/players/event_player_desktop.dart @@ -25,15 +25,14 @@ import 'package:bluecherry_client/models/device.dart'; import 'package:bluecherry_client/models/event.dart'; import 'package:bluecherry_client/providers/downloads_provider.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; +import 'package:bluecherry_client/screens/downloads/indicators.dart'; +import 'package:bluecherry_client/screens/layouts/video_status_label.dart'; import 'package:bluecherry_client/utils/extensions.dart'; -import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; import 'package:bluecherry_client/widgets/collapsable_sidebar.dart'; import 'package:bluecherry_client/widgets/desktop_buttons.dart'; -import 'package:bluecherry_client/widgets/device_grid/video_status_label.dart'; -import 'package:bluecherry_client/widgets/downloads_manager.dart'; import 'package:bluecherry_client/widgets/error_warning.dart'; import 'package:bluecherry_client/widgets/misc.dart'; -import 'package:bluecherry_client/widgets/player/widgets.dart'; +import 'package:bluecherry_client/widgets/squared_icon_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -489,7 +488,7 @@ class EventTile extends StatelessWidget { final settings = context.watch(); final loc = AppLocalizations.of(context); - final eventType = event.type.locale(context).uppercaseFirst(); + final eventType = event.type.locale(context).uppercaseFirst; final at = settings.formatDate(event.published); return SizedBox( diff --git a/lib/widgets/events/event_player_mobile.dart b/lib/screens/players/event_player_mobile.dart similarity index 99% rename from lib/widgets/events/event_player_mobile.dart rename to lib/screens/players/event_player_mobile.dart index 59014614..4fa21c98 100644 --- a/lib/widgets/events/event_player_mobile.dart +++ b/lib/screens/players/event_player_mobile.dart @@ -17,7 +17,7 @@ * along with this program. If not, see . */ -part of 'events_screen.dart'; +part of '../events_browser/events_screen.dart'; class EventPlayerScreen extends StatelessWidget { final Event event; diff --git a/lib/widgets/player/live_player.dart b/lib/screens/players/live_player.dart similarity index 97% rename from lib/widgets/player/live_player.dart rename to lib/screens/players/live_player.dart index 1cae2275..2ac7d06a 100644 --- a/lib/widgets/player/live_player.dart +++ b/lib/screens/players/live_player.dart @@ -22,18 +22,17 @@ import 'dart:async'; import 'package:bluecherry_client/models/device.dart'; import 'package:bluecherry_client/providers/home_provider.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; +import 'package:bluecherry_client/screens/layouts/desktop/multicast_view.dart'; +import 'package:bluecherry_client/screens/layouts/video_status_label.dart'; +import 'package:bluecherry_client/screens/multi_window/window.dart'; import 'package:bluecherry_client/utils/methods.dart'; -import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; import 'package:bluecherry_client/utils/window.dart'; import 'package:bluecherry_client/widgets/desktop_buttons.dart'; -import 'package:bluecherry_client/widgets/device_grid/desktop/multicast_view.dart'; -import 'package:bluecherry_client/widgets/device_grid/video_status_label.dart'; import 'package:bluecherry_client/widgets/error_warning.dart'; import 'package:bluecherry_client/widgets/hover_button.dart'; import 'package:bluecherry_client/widgets/misc.dart'; -import 'package:bluecherry_client/widgets/multi_window/window.dart'; -import 'package:bluecherry_client/widgets/player/widgets.dart'; import 'package:bluecherry_client/widgets/ptz.dart'; +import 'package:bluecherry_client/widgets/squared_icon_button.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; diff --git a/lib/screens/servers/add_server_info.dart b/lib/screens/servers/add_server_info.dart new file mode 100644 index 00000000..a7636b78 --- /dev/null +++ b/lib/screens/servers/add_server_info.dart @@ -0,0 +1,139 @@ +/* + * 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'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:url_launcher/link.dart'; + +class AddServerInfoScreen extends StatelessWidget { + final VoidCallback onNext; + + const AddServerInfoScreen({super.key, required this.onNext}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final loc = AppLocalizations.of(context); + + return IntrinsicWidth( + child: Container( + constraints: BoxConstraints( + minWidth: MediaQuery.sizeOf(context).width / 2.5, + ), + alignment: AlignmentDirectional.center, + child: Card( + color: theme.cardColor, + elevation: 4.0, + clipBehavior: Clip.antiAlias, + margin: const EdgeInsets.all(16) + MediaQuery.paddingOf(context), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsetsDirectional.all(16.0), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Image.asset( + 'assets/images/icon.png', + height: 124.0, + width: 124.0, + fit: BoxFit.contain, + ), + const SizedBox(height: 24.0), + Text( + loc.projectName, + style: theme.textTheme.displayLarge?.copyWith( + fontSize: 36.0, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4.0), + Text( + loc.projectDescription, + style: theme.textTheme.headlineSmall, + ), + const SizedBox(height: 16.0), + Row(mainAxisAlignment: MainAxisAlignment.end, children: [ + Link( + uri: Uri.https('www.bluecherrydvr.com', '/'), + builder: (context, open) { + return TextButton( + onPressed: open, + child: Text(loc.website), + ); + }, + ), + const SizedBox(width: 8.0), + Link( + uri: Uri.https( + 'www.bluecherrydvr.com', + '/product/v3license/', + ), + builder: (context, open) { + return TextButton( + onPressed: open, + child: Text(loc.purchase), + ); + }, + ), + ]), + const Divider(thickness: 1.0), + const SizedBox(height: 16.0), + Text( + loc.welcome, + style: theme.textTheme.displayLarge?.copyWith( + fontSize: 20.0, + fontWeight: FontWeight.w600, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8.0), + Text( + loc.welcomeDescription, + style: theme.textTheme.headlineSmall, + textAlign: TextAlign.center, + ), + ]), + ), + const SizedBox(height: 16.0), + Material( + child: InkWell( + onTap: onNext, + child: Container( + alignment: AlignmentDirectional.center, + width: double.infinity, + height: 56.0, + child: Text( + loc.letsGo.toUpperCase(), + style: TextStyle( + color: theme.colorScheme.onPrimaryContainer, + fontSize: 16.0, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/servers/additional_server_settings.dart b/lib/screens/servers/additional_server_settings.dart new file mode 100644 index 00000000..de72a2ec --- /dev/null +++ b/lib/screens/servers/additional_server_settings.dart @@ -0,0 +1,269 @@ +/* + * 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/models/server.dart'; +import 'package:bluecherry_client/providers/settings_provider.dart'; +import 'package:bluecherry_client/screens/layouts/desktop/stream_data.dart'; +import 'package:bluecherry_client/screens/servers/wizard.dart'; +import 'package:bluecherry_client/utils/methods.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 AdditionalServerSettings extends StatefulWidget { + final VoidCallback onBack; + final VoidCallback onNext; + final Server? server; + final Future Function(Server server) onServerChanged; + + /// Whether this isn't adding the server for the first time + final bool isEditing; + + const AdditionalServerSettings({ + super.key, + required this.onBack, + required this.onNext, + required this.server, + required this.onServerChanged, + this.isEditing = false, + }); + + @override + State createState() => + _AdditionalServerSettingsState(); +} + +class _AdditionalServerSettingsState extends State { + bool connectAutomaticallyAtStartup = true; + late StreamingType? streamingType = + widget.server?.additionalSettings.preferredStreamingType; + late RTSPProtocol? rtspProtocol = + widget.server?.additionalSettings.rtspProtocol; + late RenderingQuality? renderingQuality = + widget.server?.additionalSettings.renderingQuality; + late UnityVideoFit? videoFit = widget.server?.additionalSettings.videoFit; + + Future updateServer() async { + if (widget.server != null) { + await widget.onServerChanged(widget.server!.copyWith( + additionalSettings: AdditionalServerOptions( + connectAutomaticallyAtStartup: connectAutomaticallyAtStartup, + preferredStreamingType: streamingType, + rtspProtocol: rtspProtocol, + renderingQuality: renderingQuality, + videoFit: videoFit, + ), + )); + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final loc = AppLocalizations.of(context); + final settings = context.watch(); + + return PopScope( + canPop: widget.isEditing, + onPopInvoked: (didPop) => widget.onBack(), + child: IntrinsicWidth( + child: Container( + margin: const EdgeInsetsDirectional.all(16.0), + constraints: BoxConstraints( + minWidth: MediaQuery.sizeOf(context).width / 2.5, + ), + child: Card( + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsetsDirectional.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: isDesktop + ? MediaQuery.sizeOf(context).width / 2.5 + : null, + child: buildCardAppBar( + title: loc.serverSettings, + description: loc.serverSettingsDescription, + onBack: widget.server == null ? widget.onBack : null, + ), + ), + _buildSelectable( + title: loc.streamingType, + values: StreamingType.values, + value: streamingType, + defaultValue: settings.streamingType, + onChanged: (value) { + setState(() { + streamingType = value; + }); + }, + ), + _buildSelectable( + title: loc.rtspProtocol, + values: RTSPProtocol.values, + value: rtspProtocol, + defaultValue: settings.rtspProtocol, + onChanged: (value) { + setState(() { + rtspProtocol = value; + }); + }, + ), + _buildSelectable( + title: loc.cameraViewFit, + description: loc.cameraViewFitDescription, + values: UnityVideoFit.values, + value: videoFit, + defaultValue: settings.cameraViewFit, + onChanged: (value) { + setState(() { + videoFit = value; + }); + }, + ), + _buildSelectable( + title: loc.renderingQuality, + description: loc.renderingQualityDescription, + values: RenderingQuality.values, + value: renderingQuality, + defaultValue: settings.videoQuality, + onChanged: (value) { + setState(() { + renderingQuality = value; + }); + }, + ), + const Divider(), + CheckboxListTile.adaptive( + value: connectAutomaticallyAtStartup, + onChanged: (value) { + setState( + () => connectAutomaticallyAtStartup = value ?? true, + ); + }, + title: Text(loc.connectAutomaticallyAtStartup), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + secondary: Tooltip( + message: loc.connectAutomaticallyAtStartupDescription, + child: Icon( + Icons.info_outline, + color: theme.colorScheme.secondary, + size: 20.0, + ), + ), + ), + Padding( + padding: const EdgeInsetsDirectional.only(top: 12.0), + child: Row(children: [ + if (streamingType != null || + rtspProtocol != null || + renderingQuality != null || + videoFit != null) + TextButton( + onPressed: () { + setState(() { + streamingType = null; + rtspProtocol = null; + renderingQuality = null; + videoFit = null; + }); + }, + child: Text(loc.clear), + ), + const Spacer(), + FilledButton( + onPressed: () async { + await updateServer(); + widget.onNext(); + }, + child: Padding( + padding: const EdgeInsetsDirectional.all(8.0), + child: Text(loc.finish.toUpperCase()), + ), + ), + ]), + ), + ], + ), + ), + ), + ), + ), + ); + } + + Widget _buildSelectable({ + required String title, + String? description, + required Iterable values, + required T? value, + required T defaultValue, + required ValueChanged onChanged, + }) { + return Builder(builder: (context) { + return ListTile( + title: Row(children: [ + Flexible(child: Text(title)), + if (description != null) + Padding( + padding: const EdgeInsetsDirectional.only(start: 6.0), + child: Tooltip( + message: description, + child: Icon( + Icons.info_outline, + color: Theme.of(context).colorScheme.secondary, + size: 16.0, + ), + ), + ), + ]), + trailing: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 175.0), + child: DropdownButtonHideUnderline( + child: DropdownButton( + // isExpanded: true, + value: value, + onChanged: (v) { + onChanged(v); + }, + hint: Text(defaultValue.name.toUpperCase()), + items: values.map((v) { + return DropdownMenuItem( + value: v, + child: Row(children: [ + Text(v.name.toUpperCase()), + if (defaultValue == v) ...[ + const SizedBox(width: 10.0), + const DefaultValueIcon(), + ], + ]), + ); + }).toList(), + ), + ), + ), + ); + }); + } +} diff --git a/lib/screens/servers/configure_dvr_server.dart b/lib/screens/servers/configure_dvr_server.dart new file mode 100644 index 00000000..96d49e17 --- /dev/null +++ b/lib/screens/servers/configure_dvr_server.dart @@ -0,0 +1,491 @@ +/* + * 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/api/api.dart'; +import 'package:bluecherry_client/models/server.dart'; +import 'package:bluecherry_client/providers/server_provider.dart'; +import 'package:bluecherry_client/screens/servers/error.dart'; +import 'package:bluecherry_client/screens/servers/wizard.dart'; +import 'package:bluecherry_client/utils/constants.dart'; +import 'package:bluecherry_client/utils/methods.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +enum _ServerAddState { + none, + checkingServerCredentials, + gettingDevices; +} + +class ConfigureDVRServerScreen extends StatefulWidget { + final VoidCallback onBack; + final VoidCallback onNext; + final ValueChanged onServerChange; + final Server? server; + + const ConfigureDVRServerScreen({ + super.key, + required this.onBack, + required this.onNext, + required this.onServerChange, + required this.server, + }); + + @override + State createState() => + _ConfigureDVRServerScreenState(); +} + +class _ConfigureDVRServerScreenState extends State { + final hostnameController = TextEditingController(); + final portController = TextEditingController(text: '$kDefaultPort'); + final rtspPortController = TextEditingController(text: '$kDefaultRTSPPort'); + final nameController = TextEditingController(); + final usernameController = TextEditingController(); + final passwordController = TextEditingController(); + + bool _nameTextFieldEverFocused = false; + bool disableFinishButton = false; + bool showingPassword = false; + + final formKey = GlobalKey(); + final finishFocusNode = FocusNode(); + + String getServerHostname(String text) { + if (Uri.parse(text).scheme.isEmpty) text = 'https://$text'; + return Uri.parse(text).host; + } + + _ServerAddState state = _ServerAddState.none; + + @override + void dispose() { + hostnameController.dispose(); + portController.dispose(); + rtspPortController.dispose(); + nameController.dispose(); + usernameController.dispose(); + passwordController.dispose(); + finishFocusNode.dispose(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + hostnameController.addListener(() { + final hostname = getServerHostname(hostnameController.text); + if (!_nameTextFieldEverFocused && hostname.isNotEmpty) { + nameController.text = hostname.split('.').first; + } + }); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final loc = AppLocalizations.of(context); + + final hostnameField = TextFormField( + enabled: !disableFinishButton, + validator: (value) { + if (value == null || value.isEmpty) { + return loc.errorTextField( + loc.hostname, + ); + } + return null; + }, + controller: hostnameController, + autofocus: true, + autocorrect: false, + enableSuggestions: false, + keyboardType: TextInputType.url, + textInputAction: TextInputAction.next, + style: theme.textTheme.headlineMedium, + decoration: InputDecoration( + label: Text(loc.hostname), + hintText: loc.hostnameExample, + border: const OutlineInputBorder(), + ), + ); + + final portField = TextFormField( + enabled: !disableFinishButton, + validator: (value) { + if (value == null || value.isEmpty) { + return loc.errorTextField(loc.port); + } + return null; + }, + controller: portController, + autofocus: true, + keyboardType: TextInputType.number, + textInputAction: TextInputAction.next, + style: theme.textTheme.headlineMedium, + decoration: InputDecoration( + label: Text(loc.port), + hintText: '$kDefaultPort', + border: const OutlineInputBorder(), + ), + onChanged: (value) { + portController.text = value.replaceAll(RegExp(r'[^0-9]'), ''); + }, + ); + + final rtspPortField = TextFormField( + enabled: !disableFinishButton, + // https://github.com/bluecherrydvr/unity/issues/182 + // validator: (value) { + // if (value == null || value.isEmpty) { + // return loc.errorTextField(loc.rtspPort); + // } + // return null; + // }, + controller: rtspPortController, + autofocus: true, + keyboardType: TextInputType.number, + textInputAction: TextInputAction.next, + style: theme.textTheme.headlineMedium, + decoration: InputDecoration( + label: Text(loc.rtspPort), + hintText: '$kDefaultRTSPPort', + border: const OutlineInputBorder(), + ), + onChanged: (value) { + rtspPortController.text = value.replaceAll(RegExp(r'[^0-9]'), ''); + }, + ); + + final nameField = TextFormField( + enabled: !disableFinishButton, + validator: (value) { + if (value == null || value.isEmpty) { + return loc.errorTextField(loc.serverName); + } + return null; + }, + onTap: () => _nameTextFieldEverFocused = true, + controller: nameController, + textCapitalization: TextCapitalization.words, + keyboardType: TextInputType.name, + textInputAction: TextInputAction.next, + style: theme.textTheme.headlineMedium, + decoration: InputDecoration( + label: Text(loc.serverName), + border: const OutlineInputBorder(), + ), + ); + + final usernameField = TextFormField( + enabled: !disableFinishButton, + validator: (value) { + if (value == null || value.isEmpty) { + return loc.errorTextField( + loc.username, + ); + } + return null; + }, + controller: usernameController, + style: theme.textTheme.headlineMedium, + keyboardType: TextInputType.name, + textInputAction: TextInputAction.next, + decoration: InputDecoration( + label: Text(loc.username), + hintText: loc.usernameHint, + border: const OutlineInputBorder(), + ), + ); + + final passwordField = TextFormField( + enabled: !disableFinishButton, + validator: (value) { + if (value == null || value.isEmpty) { + return loc.errorTextField(loc.password); + } + return null; + }, + controller: passwordController, + obscureText: !showingPassword, + style: theme.textTheme.headlineMedium, + keyboardType: TextInputType.visiblePassword, + textInputAction: TextInputAction.done, + decoration: InputDecoration( + label: Text(loc.password), + border: const OutlineInputBorder(), + suffix: Tooltip( + message: showingPassword ? loc.hidePassword : loc.showPassword, + child: InkWell( + borderRadius: BorderRadius.circular(8.0), + child: Icon( + showingPassword ? Icons.visibility : Icons.visibility_off, + size: 22.0, + ), + onTap: () => setState( + () => showingPassword = !showingPassword, + ), + ), + ), + ), + onFieldSubmitted: (_) => finish(context), + ); + + return PopScope( + canPop: false, + onPopInvoked: (didPop) async { + if (widget.server == null) { + widget.onBack(); + } + }, + child: IntrinsicWidth( + child: Container( + constraints: BoxConstraints( + minWidth: MediaQuery.sizeOf(context).width / 2.5, + ), + margin: const EdgeInsetsDirectional.all(16.0), + child: Card( + elevation: 4.0, + margin: EdgeInsets.zero, + child: FocusTraversalGroup( + policy: OrderedTraversalPolicy(), + child: Form( + key: formKey, + child: SingleChildScrollView( + padding: const EdgeInsetsDirectional.symmetric( + horizontal: 16.0, + vertical: 24.0, + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + buildCardAppBar( + title: loc.configure, + description: loc.configureDescription, + onBack: widget.onBack, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 5, + child: FocusTraversalOrder( + order: const NumericFocusOrder(0), + child: hostnameField, + ), + ), + const SizedBox(width: 16.0), + Expanded( + flex: 2, + child: FocusTraversalOrder( + order: const NumericFocusOrder(1), + child: portField, + ), + ), + const SizedBox(width: 16.0), + Expanded( + flex: 2, + child: FocusTraversalOrder( + order: const NumericFocusOrder(2), + child: rtspPortField, + ), + ), + ], + ), + const SizedBox(height: 16.0), + FocusTraversalOrder( + order: const NumericFocusOrder(3), + child: nameField, + ), + const SizedBox(height: 16.0), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: FocusTraversalOrder( + order: const NumericFocusOrder(5), + child: usernameField, + ), + ), + const SizedBox(width: 8.0), + Padding( + padding: const EdgeInsetsDirectional.only(top: 8.0), + child: FocusTraversalOrder( + order: NumericFocusOrder(isMobilePlatform ? -1 : 4), + child: MaterialButton( + onPressed: disableFinishButton + ? null + : () { + usernameController.text = + kDefaultUsername; + passwordController.text = + kDefaultPassword; + finishFocusNode.requestFocus(); + }, + child: Text(loc.useDefault.toUpperCase()), + ), + ), + ), + ], + ), + const SizedBox(height: 16.0), + FocusTraversalOrder( + order: const NumericFocusOrder(6), + child: passwordField, + ), + const SizedBox(height: 16.0), + Row(mainAxisAlignment: MainAxisAlignment.end, children: [ + if (disableFinishButton) ...[ + const SizedBox( + height: 16.0, + width: 16.0, + child: CircularProgressIndicator.adaptive( + strokeWidth: 1.5, + ), + ), + Padding( + padding: const EdgeInsetsDirectional.symmetric( + horizontal: 8.0, + ), + child: Text(switch (state) { + _ServerAddState.checkingServerCredentials => + loc.checkingServerCredentials, + _ServerAddState.gettingDevices => + loc.gettingDevices, + _ServerAddState.none => '', + }), + ), + ], + FocusTraversalOrder( + order: const NumericFocusOrder(8), + child: MaterialButton( + onPressed: disableFinishButton + ? null + : () { + widget.onNext(); + }, + child: Padding( + padding: const EdgeInsetsDirectional.all(8.0), + child: Text(loc.skip.toUpperCase()), + ), + ), + ), + const SizedBox(width: 6.0), + FocusTraversalOrder( + order: const NumericFocusOrder(7), + child: FilledButton( + onPressed: disableFinishButton + ? null + : () => finish(context), + focusNode: finishFocusNode, + child: Padding( + padding: const EdgeInsetsDirectional.all(8.0), + child: Text(loc.next.toUpperCase()), + ), + ), + ), + ]), + ]), + ), + ), + ), + ), + ), + ), + ); + } + + Future finish(BuildContext context) async { + if (formKey.currentState?.validate() ?? false) { + final focusScope = FocusScope.of(context); + + final name = nameController.text.trim(); + final hostname = getServerHostname(hostnameController.text.trim()); + + if (ServersProvider.instance.servers.any((s) { + final serverHost = Uri.parse(s.login).host; + final newServerHost = Uri.parse(hostname).host; + return serverHost.isNotEmpty && + newServerHost.isNotEmpty && + serverHost == newServerHost; + })) { + showDialog( + context: context, + builder: (context) { + final loc = AppLocalizations.of(context); + + return ServerNotAddedErrorDialog( + name: name, + description: loc.serverAlreadyAdded(name), + ); + }, + ); + return; + } + + if (mounted) { + setState(() { + disableFinishButton = true; + state = _ServerAddState.checkingServerCredentials; + }); + } + final port = int.parse(portController.text.trim()); + final server = await API.instance.checkServerCredentials( + Server( + name: name, + ip: hostname, + port: port, + login: usernameController.text.trim(), + password: passwordController.text, + devices: [], + rtspPort: int.tryParse(rtspPortController.text.trim()) ?? port, + ), + ); + focusScope.unfocus(); + + if (server.serverUUID != null && server.hasCookies) { + widget.onServerChange(server); + state = _ServerAddState.gettingDevices; + await ServersProvider.instance.add(server); + widget.onNext(); + } else { + state = _ServerAddState.none; + if (context.mounted) { + showDialog( + context: context, + builder: (context) { + final loc = AppLocalizations.of(context); + return ServerNotAddedErrorDialog( + name: server.name, + description: loc.serverNotAddedErrorDescription( + server.port.toString(), + server.rtspPort.toString(), + ), + onRetry: () { + Navigator.of(context).maybePop(); + if (this.context.mounted) finish(this.context); + }, + ); + }, + ); + } + } + + if (mounted) setState(() => disableFinishButton = false); + } + } +} diff --git a/lib/widgets/servers/edit_server.dart b/lib/screens/servers/edit_server.dart similarity index 100% rename from lib/widgets/servers/edit_server.dart rename to lib/screens/servers/edit_server.dart diff --git a/lib/widgets/servers/edit_server_settings.dart b/lib/screens/servers/edit_server_settings.dart similarity index 85% rename from lib/widgets/servers/edit_server_settings.dart rename to lib/screens/servers/edit_server_settings.dart index 4fb77430..94a2b77f 100644 --- a/lib/widgets/servers/edit_server_settings.dart +++ b/lib/screens/servers/edit_server_settings.dart @@ -1,6 +1,6 @@ import 'package:bluecherry_client/models/server.dart'; -import 'package:bluecherry_client/widgets/servers/add_server.dart'; -import 'package:bluecherry_client/widgets/servers/edit_server.dart'; +import 'package:bluecherry_client/screens/servers/additional_server_settings.dart'; +import 'package:bluecherry_client/screens/servers/edit_server.dart'; import 'package:flutter/material.dart'; Future showEditServerSettings(BuildContext context, Server server) { diff --git a/lib/widgets/servers/error.dart b/lib/screens/servers/error.dart similarity index 86% rename from lib/widgets/servers/error.dart rename to lib/screens/servers/error.dart index 49fa3404..b1fad07b 100644 --- a/lib/widgets/servers/error.dart +++ b/lib/screens/servers/error.dart @@ -21,9 +21,12 @@ class ServerNotAddedErrorDialog extends StatelessWidget { return AlertDialog( title: Text(loc.serverNotAddedError(name)), - content: Text( - description, - style: theme.textTheme.headlineMedium, + content: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400.0), + child: Text( + description, + style: theme.textTheme.headlineMedium, + ), ), actions: [ if (onRetry != null) diff --git a/lib/screens/servers/finish.dart b/lib/screens/servers/finish.dart new file mode 100644 index 00000000..1bb09e41 --- /dev/null +++ b/lib/screens/servers/finish.dart @@ -0,0 +1,200 @@ +/* + * 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/models/server.dart'; +import 'package:bluecherry_client/screens/layouts/device_grid.dart'; +import 'package:bluecherry_client/utils/constants.dart'; +import 'package:bluecherry_client/utils/extensions.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class LetsGoScreen extends StatelessWidget { + final VoidCallback onBack; + final Server? server; + final VoidCallback onFinish; + + const LetsGoScreen({ + super.key, + required this.onBack, + required this.server, + required this.onFinish, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final loc = AppLocalizations.of(context); + + final addedCard = Card( + elevation: 4.0, + margin: const EdgeInsetsDirectional.only(bottom: 8.0), + color: Color.alphaBlend( + Colors.green.withOpacity(0.2), + theme.cardColor, + ), + child: Padding( + padding: const EdgeInsetsDirectional.all(16.0), + child: Row(children: [ + Icon( + Icons.check, + color: Colors.green.shade400, + ), + const SizedBox(width: 16.0), + Expanded( + child: Text(loc.serverAdded), + ), + ]), + ), + ); + + final tipsCard = Card( + elevation: 4.0, + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsetsDirectional.all(16.0), + child: SelectionArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + loc.letsGoDescription, + style: theme.textTheme.headlineMedium, + ), + ...[loc.tip0, loc.tip1, loc.tip2, loc.tip3].map((tip) { + return Padding( + padding: const EdgeInsetsDirectional.only( + top: 8.0, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text(' • '), + const SizedBox(width: 4.0), + Expanded( + child: Text(tip), + ), + ], + ), + ); + }), + ], + ), + ), + ), + ); + + final finishButton = Align( + alignment: AlignmentDirectional.centerEnd, + child: FloatingActionButton.extended( + onPressed: onFinish, + label: Text(loc.finish.toUpperCase()), + icon: const Icon(Icons.check), + ), + ); + + return LayoutBuilder(builder: (context, consts) { + if (consts.maxWidth < kMobileBreakpoint.width) { + return PopScope( + canPop: false, + child: ListView( + padding: const EdgeInsetsDirectional.all(24.0), + children: [ + SizedBox( + height: consts.maxHeight * 0.875, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (server != null) addedCard, + tipsCard, + const SizedBox(height: 8.0), + finishButton, + const SizedBox(height: 12.0), + ], + ), + ), + Card( + child: Padding( + padding: const EdgeInsetsDirectional.symmetric(vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + children: server!.devices.sorted().map((device) { + return DesktopDeviceSelectorTile( + device: device, + selected: false, + selectable: false, + ); + }).toList(), + ), + ), + ) + ], + ), + ); + } else { + return PopScope( + canPop: false, + child: IntrinsicWidth( + child: Container( + margin: const EdgeInsetsDirectional.all(16.0), + constraints: BoxConstraints( + minWidth: MediaQuery.sizeOf(context).width / 2.5, + ), + child: Row(children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (server != null) addedCard, + tipsCard, + const SizedBox(height: 8.0), + finishButton, + ], + ), + ), + if (server != null && server!.devices.isNotEmpty) ...[ + const SizedBox(width: 16.0), + SizedBox( + width: kSidebarConstraints.maxWidth, + child: Card( + child: SingleChildScrollView( + padding: + const EdgeInsetsDirectional.symmetric(vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + children: server!.devices.sorted().map((device) { + return DesktopDeviceSelectorTile( + device: device, + selected: false, + selectable: false, + ); + }).toList(), + ), + ), + ), + ), + ] + ]), + ), + ), + ); + } + }); + } +} diff --git a/lib/screens/servers/wizard.dart b/lib/screens/servers/wizard.dart new file mode 100644 index 00000000..779ead2e --- /dev/null +++ b/lib/screens/servers/wizard.dart @@ -0,0 +1,182 @@ +/* + * 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/models/server.dart'; +import 'package:bluecherry_client/providers/home_provider.dart'; +import 'package:bluecherry_client/screens/servers/add_server_info.dart'; +import 'package:bluecherry_client/screens/servers/additional_server_settings.dart'; +import 'package:bluecherry_client/screens/servers/configure_dvr_server.dart'; +import 'package:bluecherry_client/screens/servers/finish.dart'; +import 'package:bluecherry_client/widgets/drawer_button.dart'; +import 'package:bluecherry_client/widgets/squared_icon_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +Widget buildCardAppBar({ + required String title, + required String description, + VoidCallback? onBack, +}) { + return Builder(builder: (context) { + final theme = Theme.of(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(children: [ + if (onBack != null) + Padding( + padding: const EdgeInsetsDirectional.only(end: 8.0), + child: SquaredIconButton( + icon: const BackButtonIcon(), + tooltip: MaterialLocalizations.of(context).backButtonTooltip, + onPressed: () { + onBack(); + FocusScope.of(context).unfocus(); + }, + ), + ), + Text( + title, + style: theme.textTheme.displayMedium, + ), + ]), + const SizedBox(height: 12.0), + Text( + description, + style: theme.textTheme.headlineMedium, + softWrap: true, + ), + const SizedBox(height: 20.0), + ], + ); + }); +} + +class AddServerWizard extends StatefulWidget { + final VoidCallback onFinish; + + const AddServerWizard({super.key, required this.onFinish}); + + @override + State createState() => _AddServerWizardState(); +} + +class _AddServerWizardState extends State { + Server? server; + final controller = PageController( + initialPage: + HomeProvider.instance.automaticallyGoToAddServersScreen ? 1 : 0, + ); + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + void _onNext() { + controller.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + + void _onBack() { + controller.previousPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + + @override + Widget build(BuildContext context) { + return AnnotatedRegion( + value: const SystemUiOverlayStyle( + statusBarColor: Colors.white12, + statusBarIconBrightness: Brightness.light, + statusBarBrightness: Brightness.dark, + ), + child: DecoratedBox( + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage('assets/images/background.webp'), + fit: BoxFit.cover, + ), + ), + child: SafeArea( + child: Stack(children: [ + Padding( + padding: MediaQuery.viewInsetsOf(context), + child: PageView( + controller: controller, + physics: const NeverScrollableScrollPhysics(), + children: [ + Center(child: AddServerInfoScreen(onNext: _onNext)), + Center( + child: ConfigureDVRServerScreen( + onBack: _onBack, + onNext: _onNext, + server: server, + onServerChange: (server) => + setState(() => this.server = server), + ), + ), + Center( + child: AdditionalServerSettings( + onBack: _onBack, + onNext: _onNext, + server: server, + onServerChanged: (server) async { + if (this.server != null) { + setState(() => this.server = server); + } + }, + ), + ), + Center( + child: LetsGoScreen( + server: server, + onFinish: widget.onFinish, + onBack: _onBack, + ), + ), + ], + ), + ), + if (Scaffold.hasDrawer(context)) + PositionedDirectional( + top: MediaQuery.paddingOf(context).top, + start: 0, + child: const Material( + type: MaterialType.transparency, + color: Colors.amber, + child: SizedBox( + height: kToolbarHeight, + width: kToolbarHeight, + child: UnityDrawerButton(iconColor: Colors.white), + ), + ), + ), + ]), + ), + ), + ); + } +} diff --git a/lib/widgets/settings/desktop/general.dart b/lib/screens/settings/desktop/general.dart similarity index 93% rename from lib/widgets/settings/desktop/general.dart rename to lib/screens/settings/desktop/general.dart index 45f84814..6c2ea942 100644 --- a/lib/widgets/settings/desktop/general.dart +++ b/lib/screens/settings/desktop/general.dart @@ -17,9 +17,9 @@ * along with this program. If not, see . */ +import 'package:bluecherry_client/screens/settings/desktop/settings.dart'; +import 'package:bluecherry_client/screens/settings/shared/tiles.dart'; import 'package:bluecherry_client/widgets/misc.dart'; -import 'package:bluecherry_client/widgets/settings/desktop/settings.dart'; -import 'package:bluecherry_client/widgets/settings/shared/tiles.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; diff --git a/lib/widgets/settings/desktop/server.dart b/lib/screens/settings/desktop/server.dart similarity index 97% rename from lib/widgets/settings/desktop/server.dart rename to lib/screens/settings/desktop/server.dart index 7011a700..434bd43e 100644 --- a/lib/widgets/settings/desktop/server.dart +++ b/lib/screens/settings/desktop/server.dart @@ -18,10 +18,10 @@ */ import 'package:bluecherry_client/providers/settings_provider.dart'; +import 'package:bluecherry_client/screens/layouts/video_status_label.dart'; +import 'package:bluecherry_client/screens/settings/desktop/settings.dart'; +import 'package:bluecherry_client/screens/settings/mobile/settings.dart'; import 'package:bluecherry_client/utils/extensions.dart'; -import 'package:bluecherry_client/widgets/device_grid/video_status_label.dart'; -import 'package:bluecherry_client/widgets/settings/desktop/settings.dart'; -import 'package:bluecherry_client/widgets/settings/mobile/settings.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; diff --git a/lib/widgets/settings/desktop/settings.dart b/lib/screens/settings/desktop/settings.dart similarity index 93% rename from lib/widgets/settings/desktop/settings.dart rename to lib/screens/settings/desktop/settings.dart index a2c29131..96b805a4 100644 --- a/lib/widgets/settings/desktop/settings.dart +++ b/lib/screens/settings/desktop/settings.dart @@ -17,11 +17,11 @@ * along with this program. If not, see . */ +import 'package:bluecherry_client/screens/settings/desktop/general.dart'; +import 'package:bluecherry_client/screens/settings/desktop/server.dart'; +import 'package:bluecherry_client/screens/settings/desktop/updates.dart'; +import 'package:bluecherry_client/screens/settings/shared/date_language.dart'; import 'package:bluecherry_client/utils/constants.dart'; -import 'package:bluecherry_client/widgets/settings/desktop/general.dart'; -import 'package:bluecherry_client/widgets/settings/desktop/server.dart'; -import 'package:bluecherry_client/widgets/settings/desktop/updates.dart'; -import 'package:bluecherry_client/widgets/settings/shared/date_language.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; diff --git a/lib/widgets/settings/desktop/updates.dart b/lib/screens/settings/desktop/updates.dart similarity index 97% rename from lib/widgets/settings/desktop/updates.dart rename to lib/screens/settings/desktop/updates.dart index f055f2cf..ccdb3948 100644 --- a/lib/widgets/settings/desktop/updates.dart +++ b/lib/screens/settings/desktop/updates.dart @@ -21,10 +21,10 @@ import 'dart:io'; import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/providers/update_provider.dart'; +import 'package:bluecherry_client/screens/settings/desktop/settings.dart'; +import 'package:bluecherry_client/screens/settings/shared/update.dart'; import 'package:bluecherry_client/utils/logging.dart'; import 'package:bluecherry_client/utils/window.dart'; -import 'package:bluecherry_client/widgets/settings/desktop/settings.dart'; -import 'package:bluecherry_client/widgets/settings/shared/update.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; diff --git a/lib/widgets/settings/mobile/settings.dart b/lib/screens/settings/mobile/settings.dart similarity index 93% rename from lib/widgets/settings/mobile/settings.dart rename to lib/screens/settings/mobile/settings.dart index 0c9fc14d..584b4b51 100644 --- a/lib/widgets/settings/mobile/settings.dart +++ b/lib/screens/settings/mobile/settings.dart @@ -24,16 +24,17 @@ import 'package:bluecherry_client/providers/home_provider.dart'; import 'package:bluecherry_client/providers/server_provider.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/providers/update_provider.dart'; +import 'package:bluecherry_client/screens/layouts/device_grid.dart'; +import 'package:bluecherry_client/screens/servers/edit_server.dart'; +import 'package:bluecherry_client/screens/servers/edit_server_settings.dart'; +import 'package:bluecherry_client/screens/settings/shared/date_language.dart'; +import 'package:bluecherry_client/screens/settings/shared/tiles.dart'; +import 'package:bluecherry_client/screens/settings/shared/update.dart'; import 'package:bluecherry_client/utils/constants.dart'; import 'package:bluecherry_client/utils/extensions.dart'; -import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; -import 'package:bluecherry_client/widgets/device_grid/device_grid.dart'; +import 'package:bluecherry_client/widgets/drawer_button.dart'; import 'package:bluecherry_client/widgets/misc.dart'; -import 'package:bluecherry_client/widgets/servers/edit_server.dart'; -import 'package:bluecherry_client/widgets/servers/edit_server_settings.dart'; -import 'package:bluecherry_client/widgets/settings/shared/date_language.dart'; -import 'package:bluecherry_client/widgets/settings/shared/tiles.dart'; -import 'package:bluecherry_client/widgets/settings/shared/update.dart'; +import 'package:bluecherry_client/widgets/squared_icon_button.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; diff --git a/lib/widgets/settings/settings.dart b/lib/screens/settings/settings.dart similarity index 91% rename from lib/widgets/settings/settings.dart rename to lib/screens/settings/settings.dart index a784003d..3f91afd4 100644 --- a/lib/widgets/settings/settings.dart +++ b/lib/screens/settings/settings.dart @@ -17,9 +17,9 @@ * along with this program. If not, see . */ +import 'package:bluecherry_client/screens/settings/desktop/settings.dart'; +import 'package:bluecherry_client/screens/settings/mobile/settings.dart'; import 'package:bluecherry_client/utils/constants.dart'; -import 'package:bluecherry_client/widgets/settings/desktop/settings.dart'; -import 'package:bluecherry_client/widgets/settings/mobile/settings.dart'; import 'package:flutter/material.dart'; class Settings extends StatelessWidget { diff --git a/lib/widgets/settings/shared/date_language.dart b/lib/screens/settings/shared/date_language.dart similarity index 98% rename from lib/widgets/settings/shared/date_language.dart rename to lib/screens/settings/shared/date_language.dart index 778dc6a3..f971186e 100644 --- a/lib/widgets/settings/shared/date_language.dart +++ b/lib/screens/settings/shared/date_language.dart @@ -19,8 +19,8 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; +import 'package:bluecherry_client/screens/settings/desktop/settings.dart'; import 'package:bluecherry_client/utils/extensions.dart'; -import 'package:bluecherry_client/widgets/settings/desktop/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'; @@ -120,13 +120,13 @@ class LanguageSection extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - name.uppercaseFirst(), + name.uppercaseFirst, maxLines: 1, softWrap: false, style: theme.textTheme.bodyMedium, ), Text( - nativeName.uppercaseFirst(), + nativeName.uppercaseFirst, style: theme.textTheme.labelSmall, ), ], diff --git a/lib/widgets/settings/shared/server_tile.dart b/lib/screens/settings/shared/server_tile.dart similarity index 100% rename from lib/widgets/settings/shared/server_tile.dart rename to lib/screens/settings/shared/server_tile.dart diff --git a/lib/widgets/settings/shared/tiles.dart b/lib/screens/settings/shared/tiles.dart similarity index 99% rename from lib/widgets/settings/shared/tiles.dart rename to lib/screens/settings/shared/tiles.dart index 01b5a67a..2da87d94 100644 --- a/lib/widgets/settings/shared/tiles.dart +++ b/lib/screens/settings/shared/tiles.dart @@ -1,8 +1,8 @@ import 'dart:io'; import 'package:bluecherry_client/providers/settings_provider.dart'; +import 'package:bluecherry_client/screens/layouts/desktop/stream_data.dart'; import 'package:bluecherry_client/utils/extensions.dart'; -import 'package:bluecherry_client/widgets/device_grid/desktop/stream_data.dart'; import 'package:bluecherry_client/widgets/misc.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/foundation.dart'; diff --git a/lib/widgets/settings/shared/update.dart b/lib/screens/settings/shared/update.dart similarity index 99% rename from lib/widgets/settings/shared/update.dart rename to lib/screens/settings/shared/update.dart index ba94d254..00a7666d 100644 --- a/lib/widgets/settings/shared/update.dart +++ b/lib/screens/settings/shared/update.dart @@ -19,8 +19,8 @@ import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/providers/update_provider.dart'; +import 'package:bluecherry_client/screens/settings/desktop/settings.dart'; import 'package:bluecherry_client/utils/methods.dart'; -import 'package:bluecherry_client/widgets/settings/desktop/settings.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; diff --git a/lib/utils/app_links/app_links_real.dart b/lib/utils/app_links/app_links_real.dart index 7cb730c2..e6a390a5 100644 --- a/lib/utils/app_links/app_links_real.dart +++ b/lib/utils/app_links/app_links_real.dart @@ -21,10 +21,10 @@ import 'dart:io'; import 'package:app_links/app_links.dart'; import 'package:bluecherry_client/main.dart'; +import 'package:bluecherry_client/screens/layouts/desktop/external_stream.dart'; import 'package:bluecherry_client/utils/config.dart'; import 'package:bluecherry_client/utils/logging.dart'; import 'package:bluecherry_client/utils/methods.dart'; -import 'package:bluecherry_client/widgets/device_grid/desktop/external_stream.dart'; import 'package:flutter/widgets.dart'; import 'package:path/path.dart' as path; import 'package:win32_registry/win32_registry.dart'; diff --git a/lib/utils/config.dart b/lib/utils/config.dart index 8474def0..1764c232 100644 --- a/lib/utils/config.dart +++ b/lib/utils/config.dart @@ -20,8 +20,8 @@ import 'dart:io'; import 'package:bluecherry_client/main.dart'; +import 'package:bluecherry_client/screens/layouts/desktop/external_stream.dart'; import 'package:bluecherry_client/utils/video_player.dart'; -import 'package:bluecherry_client/widgets/device_grid/desktop/external_stream.dart'; import 'package:flutter/rendering.dart'; /// Represents a video overlay. diff --git a/lib/utils/extensions.dart b/lib/utils/extensions.dart index 414e24e5..49aaeb9b 100644 --- a/lib/utils/extensions.dart +++ b/lib/utils/extensions.dart @@ -110,7 +110,7 @@ extension UnityVideoQualityExtension on UnityVideoQuality { } extension StringExtension on String { - String uppercaseFirst() { + String get uppercaseFirst { if (isEmpty) return this; return substring(0, 1).toUpperCase() + substring(1); diff --git a/lib/widgets/collapsable_sidebar.dart b/lib/widgets/collapsable_sidebar.dart index d6ac38b4..99e54c44 100644 --- a/lib/widgets/collapsable_sidebar.dart +++ b/lib/widgets/collapsable_sidebar.dart @@ -17,7 +17,7 @@ * along with this program. If not, see . */ -import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; +import 'package:bluecherry_client/widgets/squared_icon_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; diff --git a/lib/widgets/desktop_buttons.dart b/lib/widgets/desktop_buttons.dart index b473c6ac..76159b32 100644 --- a/lib/widgets/desktop_buttons.dart +++ b/lib/widgets/desktop_buttons.dart @@ -25,11 +25,11 @@ import 'package:bluecherry_client/models/device.dart'; import 'package:bluecherry_client/models/event.dart'; import 'package:bluecherry_client/providers/home_provider.dart'; import 'package:bluecherry_client/providers/update_provider.dart'; +import 'package:bluecherry_client/screens/events_browser/events_screen.dart'; +import 'package:bluecherry_client/screens/events_timeline/events_playback.dart'; +import 'package:bluecherry_client/screens/home.dart'; import 'package:bluecherry_client/utils/methods.dart'; -import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; -import 'package:bluecherry_client/widgets/events/events_screen.dart'; -import 'package:bluecherry_client/widgets/events_timeline/events_playback.dart'; -import 'package:bluecherry_client/widgets/home.dart'; +import 'package:bluecherry_client/widgets/squared_icon_button.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; diff --git a/lib/widgets/device_selector_screen.dart b/lib/widgets/device_selector.dart similarity index 96% rename from lib/widgets/device_selector_screen.dart rename to lib/widgets/device_selector.dart index 3224e745..7caa552f 100644 --- a/lib/widgets/device_selector_screen.dart +++ b/lib/widgets/device_selector.dart @@ -21,9 +21,9 @@ import 'package:bluecherry_client/models/device.dart'; import 'package:bluecherry_client/providers/server_provider.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/utils/theme.dart'; -import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; import 'package:bluecherry_client/widgets/error_warning.dart'; import 'package:bluecherry_client/widgets/misc.dart'; +import 'package:bluecherry_client/widgets/squared_icon_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; @@ -31,7 +31,7 @@ import 'package:sliver_tools/sliver_tools.dart'; typedef EventsPerDevice = Map; -Future showDeviceSelectorScreen( +Future showDeviceSelector( BuildContext context, { List selected = const [], Iterable? available, @@ -49,7 +49,7 @@ Future showDeviceSelectorScreen( builder: (context, controller) { return PrimaryScrollController( controller: controller, - child: DeviceSelectorScreen( + child: DeviceSelector( selected: selected, available: available, eventsPerDevice: eventsPerDevice, @@ -61,7 +61,7 @@ Future showDeviceSelectorScreen( ); } -class DeviceSelectorScreen extends StatelessWidget { +class DeviceSelector extends StatelessWidget { /// The devices already selected final Iterable selected; @@ -70,7 +70,7 @@ class DeviceSelectorScreen extends StatelessWidget { /// The amount of events per device final EventsPerDevice eventsPerDevice; - const DeviceSelectorScreen({ + const DeviceSelector({ super.key, this.selected = const [], this.available, @@ -163,7 +163,7 @@ class DeviceSelectorScreen extends StatelessWidget { : theme.disabledColor, ), children: [ - TextSpan(text: device.name.uppercaseFirst()), + TextSpan(text: device.name.uppercaseFirst), if (eventsPerDevice[device] != null) TextSpan( text: diff --git a/lib/widgets/drawer_button.dart b/lib/widgets/drawer_button.dart new file mode 100644 index 00000000..49f48f98 --- /dev/null +++ b/lib/widgets/drawer_button.dart @@ -0,0 +1,86 @@ +/* + * 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'; + +/// A helper function that returns a [UnityDrawerButton] if the parent +/// [Scaffold] has a drawer. +/// +/// This is useful for when you want to display a drawer button in the appbar +/// but only if the parent [Scaffold] has a drawer. +// ignore: non_constant_identifier_names +Widget? MaybeUnityDrawerButton( + BuildContext context, { + EdgeInsetsGeometry padding = EdgeInsetsDirectional.zero, + VoidCallback? open, +}) { + if (Scaffold.hasDrawer(context)) { + return Padding( + padding: padding, + child: UnityDrawerButton( + enforce: true, + open: Scaffold.of(context).openDrawer, + ), + ); + } + + return null; +} + +/// A button that listen to updates to the parent [Scaffold] and display the +/// drawer button accordingly +class UnityDrawerButton extends StatelessWidget { + final Color? iconColor; + final double? iconSize; + final double splashRadius; + + final bool enforce; + final VoidCallback? open; + + const UnityDrawerButton({ + super.key, + this.iconColor, + this.iconSize = 22.0, + this.splashRadius = 20.0, + this.enforce = false, + this.open, + }); + + @override + Widget build(BuildContext context) { + if (Scaffold.hasDrawer(context) || enforce) { + return Tooltip( + message: MaterialLocalizations.of(context).openAppDrawerTooltip, + child: Center( + child: SizedBox( + height: 44.0, + width: 44.0, + child: InkWell( + onTap: open ?? () => Scaffold.of(context).openDrawer(), + radius: 10.0, + borderRadius: BorderRadius.circular(100.0), + child: Icon(Icons.menu, color: iconColor, size: iconSize), + ), + ), + ), + ); + } + return const SizedBox.shrink(); + } +} diff --git a/lib/widgets/misc.dart b/lib/widgets/misc.dart index 85cf6315..df90e138 100644 --- a/lib/widgets/misc.dart +++ b/lib/widgets/misc.dart @@ -19,9 +19,12 @@ import 'dart:async'; +import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/utils/methods.dart'; +import 'package:bluecherry_client/widgets/squared_icon_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:unity_video_player/unity_video_player.dart'; const double kDesktopAppBarHeight = 64.0; @@ -249,72 +252,6 @@ class SubHeader extends StatelessWidget { } } -/// A helper function that returns a [UnityDrawerButton] if the parent -/// [Scaffold] has a drawer. -/// -/// This is useful for when you want to display a drawer button in the appbar -/// but only if the parent [Scaffold] has a drawer. -// ignore: non_constant_identifier_names -Widget? MaybeUnityDrawerButton( - BuildContext context, { - EdgeInsetsGeometry padding = EdgeInsetsDirectional.zero, - VoidCallback? open, -}) { - if (Scaffold.hasDrawer(context)) { - return Padding( - padding: padding, - child: UnityDrawerButton( - enforce: true, - open: Scaffold.of(context).openDrawer, - ), - ); - } - - return null; -} - -/// A button that listen to updates to the parent [Scaffold] and display the -/// drawer button accordingly -class UnityDrawerButton extends StatelessWidget { - final Color? iconColor; - final double? iconSize; - final double splashRadius; - - final bool enforce; - final VoidCallback? open; - - const UnityDrawerButton({ - super.key, - this.iconColor, - this.iconSize = 22.0, - this.splashRadius = 20.0, - this.enforce = false, - this.open, - }); - - @override - Widget build(BuildContext context) { - if (Scaffold.hasDrawer(context) || enforce) { - return Tooltip( - message: MaterialLocalizations.of(context).openAppDrawerTooltip, - child: Center( - child: SizedBox( - height: 44.0, - width: 44.0, - child: InkWell( - onTap: open ?? () => Scaffold.of(context).openDrawer(), - radius: 10.0, - borderRadius: BorderRadius.circular(100.0), - child: Icon(Icons.menu, color: iconColor, size: iconSize), - ), - ), - ), - ); - } - return const SizedBox.shrink(); - } -} - /// Outlines any text with the given color and stroke width making use of /// [TextStyle.shadows] /// @@ -491,3 +428,28 @@ class _EnforceScrollbarScrollState extends State { ); } } + +class CameraViewFitButton extends StatelessWidget { + final UnityVideoFit fit; + final ValueChanged onChanged; + + const CameraViewFitButton({ + super.key, + required this.fit, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return SquaredIconButton( + tooltip: fit.locale(context), + onPressed: () => onChanged(fit.next), + icon: Icon( + fit.icon, + size: 18.0, + shadows: outlinedText(), + color: Colors.white, + ), + ); + } +} diff --git a/lib/widgets/player/widgets.dart b/lib/widgets/player/widgets.dart deleted file mode 100644 index 4548f54a..00000000 --- a/lib/widgets/player/widgets.dart +++ /dev/null @@ -1,49 +0,0 @@ -/* - * This file is a part of Bluecherry Client (https://github.com/bluecherrydvr/unity). - * - * Copyright 2022 Bluecherry, LLC - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation; either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -import 'package:bluecherry_client/utils/extensions.dart'; -import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; -import 'package:bluecherry_client/widgets/misc.dart'; -import 'package:flutter/material.dart'; -import 'package:unity_video_player/unity_video_player.dart'; - -class CameraViewFitButton extends StatelessWidget { - final UnityVideoFit fit; - final ValueChanged onChanged; - - const CameraViewFitButton({ - super.key, - required this.fit, - required this.onChanged, - }); - - @override - Widget build(BuildContext context) { - return SquaredIconButton( - tooltip: fit.locale(context), - onPressed: () => onChanged(fit.next), - icon: Icon( - fit.icon, - size: 18.0, - shadows: outlinedText(), - color: Colors.white, - ), - ); - } -} diff --git a/lib/widgets/ptz.dart b/lib/widgets/ptz.dart index d95d8ad7..4e419ac4 100644 --- a/lib/widgets/ptz.dart +++ b/lib/widgets/ptz.dart @@ -19,8 +19,8 @@ import 'package:bluecherry_client/api/api.dart'; import 'package:bluecherry_client/models/device.dart'; -import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; import 'package:bluecherry_client/widgets/hover_button.dart'; +import 'package:bluecherry_client/widgets/squared_icon_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; diff --git a/lib/widgets/search.dart b/lib/widgets/search.dart index db9d2e9e..f0f3cd37 100644 --- a/lib/widgets/search.dart +++ b/lib/widgets/search.dart @@ -1,4 +1,4 @@ -import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; +import 'package:bluecherry_client/widgets/squared_icon_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; diff --git a/lib/widgets/servers/add_server.dart b/lib/widgets/servers/add_server.dart deleted file mode 100644 index deb82f47..00000000 --- a/lib/widgets/servers/add_server.dart +++ /dev/null @@ -1,1184 +0,0 @@ -/* - * This file is a part of Bluecherry Client (https://github.com/bluecherrydvr/unity). - * - * Copyright 2022 Bluecherry, LLC - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation; either version 3 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -import 'package:bluecherry_client/api/api.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/utils/constants.dart'; -import 'package:bluecherry_client/utils/extensions.dart'; -import 'package:bluecherry_client/utils/methods.dart'; -import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; -import 'package:bluecherry_client/widgets/device_grid/desktop/stream_data.dart'; -import 'package:bluecherry_client/widgets/device_grid/device_grid.dart'; -import 'package:bluecherry_client/widgets/misc.dart'; -import 'package:bluecherry_client/widgets/servers/error.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:provider/provider.dart'; -import 'package:unity_video_player/unity_video_player.dart'; -import 'package:url_launcher/link.dart'; - -Widget _buildCardAppBar({ - required String title, - required String description, - VoidCallback? onBack, -}) { - return Builder(builder: (context) { - final theme = Theme.of(context); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row(children: [ - if (onBack != null) - Padding( - padding: const EdgeInsetsDirectional.only(end: 8.0), - child: SquaredIconButton( - icon: const BackButtonIcon(), - tooltip: MaterialLocalizations.of(context).backButtonTooltip, - onPressed: () { - onBack(); - FocusScope.of(context).unfocus(); - }, - ), - ), - Text( - title, - style: theme.textTheme.displayMedium, - ), - ]), - const SizedBox(height: 12.0), - Text( - description, - style: theme.textTheme.headlineMedium, - softWrap: true, - ), - const SizedBox(height: 20.0), - ], - ); - }); -} - -enum _ServerAddState { - none, - checkingServerCredentials, - gettingDevices; -} - -class AddServerWizard extends StatefulWidget { - final VoidCallback onFinish; - - const AddServerWizard({super.key, required this.onFinish}); - - @override - State createState() => _AddServerWizardState(); -} - -class _AddServerWizardState extends State { - Server? server; - final controller = PageController( - initialPage: - HomeProvider.instance.automaticallyGoToAddServersScreen ? 1 : 0, - ); - - @override - void dispose() { - controller.dispose(); - super.dispose(); - } - - void _onNext() { - controller.nextPage( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - } - - void _onBack() { - controller.previousPage( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - } - - @override - Widget build(BuildContext context) { - return AnnotatedRegion( - value: const SystemUiOverlayStyle( - statusBarColor: Colors.white12, - statusBarIconBrightness: Brightness.light, - statusBarBrightness: Brightness.dark, - ), - child: DecoratedBox( - decoration: const BoxDecoration( - image: DecorationImage( - image: AssetImage('assets/images/background.webp'), - fit: BoxFit.cover, - ), - ), - child: SafeArea( - child: Stack(children: [ - Padding( - padding: MediaQuery.viewInsetsOf(context), - child: PageView( - controller: controller, - physics: const NeverScrollableScrollPhysics(), - children: [ - Center(child: AddServerInfoScreen(onNext: _onNext)), - Center( - child: ConfigureDVRServerScreen( - onBack: _onBack, - onNext: _onNext, - server: server, - onServerChange: (server) => - setState(() => this.server = server), - ), - ), - Center( - child: AdditionalServerSettings( - onBack: _onBack, - onNext: _onNext, - server: server, - onServerChanged: (server) async { - if (this.server != null) { - setState(() => this.server = server); - } - }, - ), - ), - Center( - child: LetsGoScreen( - server: server, - onFinish: widget.onFinish, - onBack: _onBack, - ), - ), - ], - ), - ), - if (Scaffold.hasDrawer(context)) - PositionedDirectional( - top: MediaQuery.paddingOf(context).top, - start: 0, - child: const Material( - type: MaterialType.transparency, - color: Colors.amber, - child: SizedBox( - height: kToolbarHeight, - width: kToolbarHeight, - child: UnityDrawerButton(iconColor: Colors.white), - ), - ), - ), - ]), - ), - ), - ); - } -} - -class AddServerInfoScreen extends StatelessWidget { - final VoidCallback onNext; - - const AddServerInfoScreen({super.key, required this.onNext}); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final loc = AppLocalizations.of(context); - - return IntrinsicWidth( - child: Container( - constraints: BoxConstraints( - minWidth: MediaQuery.sizeOf(context).width / 2.5, - ), - alignment: AlignmentDirectional.center, - child: Card( - color: theme.cardColor, - elevation: 4.0, - clipBehavior: Clip.antiAlias, - margin: const EdgeInsets.all(16) + MediaQuery.paddingOf(context), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsetsDirectional.all(16.0), - child: Column(mainAxisSize: MainAxisSize.min, children: [ - Image.asset( - 'assets/images/icon.png', - height: 124.0, - width: 124.0, - fit: BoxFit.contain, - ), - const SizedBox(height: 24.0), - Text( - loc.projectName, - style: theme.textTheme.displayLarge?.copyWith( - fontSize: 36.0, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 4.0), - Text( - loc.projectDescription, - style: theme.textTheme.headlineSmall, - ), - const SizedBox(height: 16.0), - Row(mainAxisAlignment: MainAxisAlignment.end, children: [ - Link( - uri: Uri.https('www.bluecherrydvr.com', '/'), - builder: (context, open) { - return TextButton( - onPressed: open, - child: Text(loc.website), - ); - }, - ), - const SizedBox(width: 8.0), - Link( - uri: Uri.https( - 'www.bluecherrydvr.com', - '/product/v3license/', - ), - builder: (context, open) { - return TextButton( - onPressed: open, - child: Text(loc.purchase), - ); - }, - ), - ]), - const Divider(thickness: 1.0), - const SizedBox(height: 16.0), - Text( - loc.welcome, - style: theme.textTheme.displayLarge?.copyWith( - fontSize: 20.0, - fontWeight: FontWeight.w600, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 8.0), - Text( - loc.welcomeDescription, - style: theme.textTheme.headlineSmall, - textAlign: TextAlign.center, - ), - ]), - ), - const SizedBox(height: 16.0), - Material( - child: InkWell( - onTap: onNext, - child: Container( - alignment: AlignmentDirectional.center, - width: double.infinity, - height: 56.0, - child: Text( - loc.letsGo.toUpperCase(), - style: TextStyle( - color: theme.colorScheme.onPrimaryContainer, - fontSize: 16.0, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - ), - ], - ), - ), - ), - ); - } -} - -class ConfigureDVRServerScreen extends StatefulWidget { - final VoidCallback onBack; - final VoidCallback onNext; - final ValueChanged onServerChange; - final Server? server; - - const ConfigureDVRServerScreen({ - super.key, - required this.onBack, - required this.onNext, - required this.onServerChange, - required this.server, - }); - - @override - State createState() => - _ConfigureDVRServerScreenState(); -} - -class _ConfigureDVRServerScreenState extends State { - final hostnameController = TextEditingController(); - final portController = TextEditingController(text: '$kDefaultPort'); - final rtspPortController = TextEditingController(text: '$kDefaultRTSPPort'); - final nameController = TextEditingController(); - final usernameController = TextEditingController(); - final passwordController = TextEditingController(); - - bool _nameTextFieldEverFocused = false; - bool disableFinishButton = false; - bool showingPassword = false; - - final formKey = GlobalKey(); - final finishFocusNode = FocusNode(); - - String getServerHostname(String text) { - if (Uri.parse(text).scheme.isEmpty) text = 'https://$text'; - return Uri.parse(text).host; - } - - _ServerAddState state = _ServerAddState.none; - - @override - void dispose() { - hostnameController.dispose(); - portController.dispose(); - rtspPortController.dispose(); - nameController.dispose(); - usernameController.dispose(); - passwordController.dispose(); - finishFocusNode.dispose(); - super.dispose(); - } - - @override - void initState() { - super.initState(); - hostnameController.addListener(() { - final hostname = getServerHostname(hostnameController.text); - if (!_nameTextFieldEverFocused && hostname.isNotEmpty) { - nameController.text = hostname.split('.').first; - } - }); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final loc = AppLocalizations.of(context); - - final hostnameField = TextFormField( - enabled: !disableFinishButton, - validator: (value) { - if (value == null || value.isEmpty) { - return loc.errorTextField( - loc.hostname, - ); - } - return null; - }, - controller: hostnameController, - autofocus: true, - autocorrect: false, - enableSuggestions: false, - keyboardType: TextInputType.url, - textInputAction: TextInputAction.next, - style: theme.textTheme.headlineMedium, - decoration: InputDecoration( - label: Text(loc.hostname), - hintText: loc.hostnameExample, - border: const OutlineInputBorder(), - ), - ); - - final portField = TextFormField( - enabled: !disableFinishButton, - validator: (value) { - if (value == null || value.isEmpty) { - return loc.errorTextField(loc.port); - } - return null; - }, - controller: portController, - autofocus: true, - keyboardType: TextInputType.number, - textInputAction: TextInputAction.next, - style: theme.textTheme.headlineMedium, - decoration: InputDecoration( - label: Text(loc.port), - hintText: '$kDefaultPort', - border: const OutlineInputBorder(), - ), - onChanged: (value) { - portController.text = value.replaceAll(RegExp(r'[^0-9]'), ''); - }, - ); - - final rtspPortField = TextFormField( - enabled: !disableFinishButton, - // https://github.com/bluecherrydvr/unity/issues/182 - // validator: (value) { - // if (value == null || value.isEmpty) { - // return loc.errorTextField(loc.rtspPort); - // } - // return null; - // }, - controller: rtspPortController, - autofocus: true, - keyboardType: TextInputType.number, - textInputAction: TextInputAction.next, - style: theme.textTheme.headlineMedium, - decoration: InputDecoration( - label: Text(loc.rtspPort), - hintText: '$kDefaultRTSPPort', - border: const OutlineInputBorder(), - ), - onChanged: (value) { - rtspPortController.text = value.replaceAll(RegExp(r'[^0-9]'), ''); - }, - ); - - final nameField = TextFormField( - enabled: !disableFinishButton, - validator: (value) { - if (value == null || value.isEmpty) { - return loc.errorTextField(loc.serverName); - } - return null; - }, - onTap: () => _nameTextFieldEverFocused = true, - controller: nameController, - textCapitalization: TextCapitalization.words, - keyboardType: TextInputType.name, - textInputAction: TextInputAction.next, - style: theme.textTheme.headlineMedium, - decoration: InputDecoration( - label: Text(loc.serverName), - border: const OutlineInputBorder(), - ), - ); - - final usernameField = TextFormField( - enabled: !disableFinishButton, - validator: (value) { - if (value == null || value.isEmpty) { - return loc.errorTextField( - loc.username, - ); - } - return null; - }, - controller: usernameController, - style: theme.textTheme.headlineMedium, - keyboardType: TextInputType.name, - textInputAction: TextInputAction.next, - decoration: InputDecoration( - label: Text(loc.username), - hintText: loc.usernameHint, - border: const OutlineInputBorder(), - ), - ); - - final passwordField = TextFormField( - enabled: !disableFinishButton, - validator: (value) { - if (value == null || value.isEmpty) { - return loc.errorTextField(loc.password); - } - return null; - }, - controller: passwordController, - obscureText: !showingPassword, - style: theme.textTheme.headlineMedium, - keyboardType: TextInputType.visiblePassword, - textInputAction: TextInputAction.done, - decoration: InputDecoration( - label: Text(loc.password), - border: const OutlineInputBorder(), - suffix: Tooltip( - message: showingPassword ? loc.hidePassword : loc.showPassword, - child: InkWell( - borderRadius: BorderRadius.circular(8.0), - child: Icon( - showingPassword ? Icons.visibility : Icons.visibility_off, - size: 22.0, - ), - onTap: () => setState( - () => showingPassword = !showingPassword, - ), - ), - ), - ), - onFieldSubmitted: (_) => finish(context), - ); - - return PopScope( - canPop: false, - onPopInvoked: (didPop) async { - if (widget.server == null) { - widget.onBack(); - } - }, - child: IntrinsicWidth( - child: Container( - constraints: BoxConstraints( - minWidth: MediaQuery.sizeOf(context).width / 2.5, - ), - margin: const EdgeInsetsDirectional.all(16.0), - child: Card( - elevation: 4.0, - margin: EdgeInsets.zero, - child: FocusTraversalGroup( - policy: OrderedTraversalPolicy(), - child: Form( - key: formKey, - child: SingleChildScrollView( - padding: const EdgeInsetsDirectional.symmetric( - horizontal: 16.0, - vertical: 24.0, - ), - child: Column(mainAxisSize: MainAxisSize.min, children: [ - _buildCardAppBar( - title: loc.configure, - description: loc.configureDescription, - onBack: widget.onBack, - ), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - flex: 5, - child: FocusTraversalOrder( - order: const NumericFocusOrder(0), - child: hostnameField, - ), - ), - const SizedBox(width: 16.0), - Expanded( - flex: 2, - child: FocusTraversalOrder( - order: const NumericFocusOrder(1), - child: portField, - ), - ), - const SizedBox(width: 16.0), - Expanded( - flex: 2, - child: FocusTraversalOrder( - order: const NumericFocusOrder(2), - child: rtspPortField, - ), - ), - ], - ), - const SizedBox(height: 16.0), - FocusTraversalOrder( - order: const NumericFocusOrder(3), - child: nameField, - ), - const SizedBox(height: 16.0), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: FocusTraversalOrder( - order: const NumericFocusOrder(5), - child: usernameField, - ), - ), - const SizedBox(width: 8.0), - Padding( - padding: const EdgeInsetsDirectional.only(top: 8.0), - child: FocusTraversalOrder( - order: NumericFocusOrder(isMobilePlatform ? -1 : 4), - child: MaterialButton( - onPressed: disableFinishButton - ? null - : () { - usernameController.text = - kDefaultUsername; - passwordController.text = - kDefaultPassword; - finishFocusNode.requestFocus(); - }, - child: Text(loc.useDefault.toUpperCase()), - ), - ), - ), - ], - ), - const SizedBox(height: 16.0), - FocusTraversalOrder( - order: const NumericFocusOrder(6), - child: passwordField, - ), - const SizedBox(height: 16.0), - Row(mainAxisAlignment: MainAxisAlignment.end, children: [ - if (disableFinishButton) ...[ - const SizedBox( - height: 16.0, - width: 16.0, - child: CircularProgressIndicator.adaptive( - strokeWidth: 1.5, - ), - ), - Padding( - padding: const EdgeInsetsDirectional.symmetric( - horizontal: 8.0, - ), - child: Text(switch (state) { - _ServerAddState.checkingServerCredentials => - loc.checkingServerCredentials, - _ServerAddState.gettingDevices => - loc.gettingDevices, - _ServerAddState.none => '', - }), - ), - ], - FocusTraversalOrder( - order: const NumericFocusOrder(8), - child: MaterialButton( - onPressed: disableFinishButton - ? null - : () { - widget.onNext(); - }, - child: Padding( - padding: const EdgeInsetsDirectional.all(8.0), - child: Text(loc.skip.toUpperCase()), - ), - ), - ), - const SizedBox(width: 6.0), - FocusTraversalOrder( - order: const NumericFocusOrder(7), - child: FilledButton( - onPressed: disableFinishButton - ? null - : () => finish(context), - focusNode: finishFocusNode, - child: Padding( - padding: const EdgeInsetsDirectional.all(8.0), - child: Text(loc.next.toUpperCase()), - ), - ), - ), - ]), - ]), - ), - ), - ), - ), - ), - ), - ); - } - - Future finish(BuildContext context) async { - if (formKey.currentState?.validate() ?? false) { - final focusScope = FocusScope.of(context); - - final name = nameController.text.trim(); - final hostname = getServerHostname(hostnameController.text.trim()); - - if (ServersProvider.instance.servers.any((s) { - final serverHost = Uri.parse(s.login).host; - final newServerHost = Uri.parse(hostname).host; - return serverHost.isNotEmpty && - newServerHost.isNotEmpty && - serverHost == newServerHost; - })) { - showDialog( - context: context, - builder: (context) { - final loc = AppLocalizations.of(context); - - return ServerNotAddedErrorDialog( - name: name, - description: loc.serverAlreadyAdded(name), - ); - }, - ); - return; - } - - if (mounted) { - setState(() { - disableFinishButton = true; - state = _ServerAddState.checkingServerCredentials; - }); - } - final port = int.parse(portController.text.trim()); - final server = await API.instance.checkServerCredentials( - Server( - name: name, - ip: hostname, - port: port, - login: usernameController.text.trim(), - password: passwordController.text, - devices: [], - rtspPort: int.tryParse(rtspPortController.text.trim()) ?? port, - ), - ); - focusScope.unfocus(); - - if (server.serverUUID != null && server.hasCookies) { - widget.onServerChange(server); - state = _ServerAddState.gettingDevices; - await ServersProvider.instance.add(server); - widget.onNext(); - } else { - state = _ServerAddState.none; - if (context.mounted) { - showDialog( - context: context, - builder: (context) { - final loc = AppLocalizations.of(context); - return ServerNotAddedErrorDialog( - name: server.name, - description: loc.serverNotAddedErrorDescription, - onRetry: () { - Navigator.of(context).maybePop(); - if (this.context.mounted) finish(this.context); - }, - ); - }, - ); - } - } - - if (mounted) setState(() => disableFinishButton = false); - } - } -} - -class AdditionalServerSettings extends StatefulWidget { - final VoidCallback onBack; - final VoidCallback onNext; - final Server? server; - final Future Function(Server server) onServerChanged; - - /// Whether this isn't adding the server for the first time - final bool isEditing; - - const AdditionalServerSettings({ - super.key, - required this.onBack, - required this.onNext, - required this.server, - required this.onServerChanged, - this.isEditing = false, - }); - - @override - State createState() => - _AdditionalServerSettingsState(); -} - -class _AdditionalServerSettingsState extends State { - bool connectAutomaticallyAtStartup = true; - late StreamingType? streamingType = - widget.server?.additionalSettings.preferredStreamingType; - late RTSPProtocol? rtspProtocol = - widget.server?.additionalSettings.rtspProtocol; - late RenderingQuality? renderingQuality = - widget.server?.additionalSettings.renderingQuality; - late UnityVideoFit? videoFit = widget.server?.additionalSettings.videoFit; - - Future updateServer() async { - if (widget.server != null) { - await widget.onServerChanged(widget.server!.copyWith( - additionalSettings: AdditionalServerOptions( - connectAutomaticallyAtStartup: connectAutomaticallyAtStartup, - preferredStreamingType: streamingType, - rtspProtocol: rtspProtocol, - renderingQuality: renderingQuality, - videoFit: videoFit, - ), - )); - } - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final loc = AppLocalizations.of(context); - final settings = context.watch(); - - return PopScope( - canPop: widget.isEditing, - onPopInvoked: (didPop) => widget.onBack(), - child: IntrinsicWidth( - child: Container( - margin: const EdgeInsetsDirectional.all(16.0), - constraints: BoxConstraints( - minWidth: MediaQuery.sizeOf(context).width / 2.5, - ), - child: Card( - margin: EdgeInsets.zero, - child: Padding( - padding: const EdgeInsetsDirectional.all(16.0), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: isDesktop - ? MediaQuery.sizeOf(context).width / 2.5 - : null, - child: _buildCardAppBar( - title: loc.serverSettings, - description: loc.serverSettingsDescription, - onBack: widget.server == null ? widget.onBack : null, - ), - ), - _buildSelectable( - title: loc.streamingType, - values: StreamingType.values, - value: streamingType, - defaultValue: settings.streamingType, - onChanged: (value) { - setState(() { - streamingType = value; - }); - }, - ), - _buildSelectable( - title: loc.rtspProtocol, - values: RTSPProtocol.values, - value: rtspProtocol, - defaultValue: settings.rtspProtocol, - onChanged: (value) { - setState(() { - rtspProtocol = value; - }); - }, - ), - _buildSelectable( - title: loc.cameraViewFit, - description: loc.cameraViewFitDescription, - values: UnityVideoFit.values, - value: videoFit, - defaultValue: settings.cameraViewFit, - onChanged: (value) { - setState(() { - videoFit = value; - }); - }, - ), - _buildSelectable( - title: loc.renderingQuality, - description: loc.renderingQualityDescription, - values: RenderingQuality.values, - value: renderingQuality, - defaultValue: settings.videoQuality, - onChanged: (value) { - setState(() { - renderingQuality = value; - }); - }, - ), - const Divider(), - CheckboxListTile.adaptive( - value: connectAutomaticallyAtStartup, - onChanged: (value) { - setState( - () => connectAutomaticallyAtStartup = value ?? true, - ); - }, - title: Text(loc.connectAutomaticallyAtStartup), - dense: true, - controlAffinity: ListTileControlAffinity.leading, - secondary: Tooltip( - message: loc.connectAutomaticallyAtStartupDescription, - child: Icon( - Icons.info_outline, - color: theme.colorScheme.secondary, - size: 20.0, - ), - ), - ), - Padding( - padding: const EdgeInsetsDirectional.only(top: 12.0), - child: Row(children: [ - if (streamingType != null || - rtspProtocol != null || - renderingQuality != null || - videoFit != null) - TextButton( - onPressed: () { - setState(() { - streamingType = null; - rtspProtocol = null; - renderingQuality = null; - videoFit = null; - }); - }, - child: Text(loc.clear), - ), - const Spacer(), - FilledButton( - onPressed: () async { - await updateServer(); - widget.onNext(); - }, - child: Padding( - padding: const EdgeInsetsDirectional.all(8.0), - child: Text(loc.finish.toUpperCase()), - ), - ), - ]), - ), - ], - ), - ), - ), - ), - ), - ); - } - - Widget _buildSelectable({ - required String title, - String? description, - required Iterable values, - required T? value, - required T defaultValue, - required ValueChanged onChanged, - }) { - return Builder(builder: (context) { - return ListTile( - title: Row(children: [ - Flexible(child: Text(title)), - if (description != null) - Padding( - padding: const EdgeInsetsDirectional.only(start: 6.0), - child: Tooltip( - message: description, - child: Icon( - Icons.info_outline, - color: Theme.of(context).colorScheme.secondary, - size: 16.0, - ), - ), - ), - ]), - trailing: ConstrainedBox( - constraints: const BoxConstraints(minWidth: 175.0), - child: DropdownButtonHideUnderline( - child: DropdownButton( - // isExpanded: true, - value: value, - onChanged: (v) { - onChanged(v); - }, - hint: Text(defaultValue.name.toUpperCase()), - items: values.map((v) { - return DropdownMenuItem( - value: v, - child: Row(children: [ - Text(v.name.toUpperCase()), - if (defaultValue == v) ...[ - const SizedBox(width: 10.0), - const DefaultValueIcon(), - ], - ]), - ); - }).toList(), - ), - ), - ), - ); - }); - } -} - -class LetsGoScreen extends StatelessWidget { - final VoidCallback onBack; - final Server? server; - final VoidCallback onFinish; - - const LetsGoScreen({ - super.key, - required this.onBack, - required this.server, - required this.onFinish, - }); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final loc = AppLocalizations.of(context); - - final addedCard = Card( - elevation: 4.0, - margin: const EdgeInsetsDirectional.only(bottom: 8.0), - color: Color.alphaBlend( - Colors.green.withOpacity(0.2), - theme.cardColor, - ), - child: Padding( - padding: const EdgeInsetsDirectional.all(16.0), - child: Row(children: [ - Icon( - Icons.check, - color: Colors.green.shade400, - ), - const SizedBox(width: 16.0), - Expanded( - child: Text(loc.serverAdded), - ), - ]), - ), - ); - - final tipsCard = Card( - elevation: 4.0, - margin: EdgeInsets.zero, - child: Padding( - padding: const EdgeInsetsDirectional.all(16.0), - child: SelectionArea( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - loc.letsGoDescription, - style: theme.textTheme.headlineMedium, - ), - ...[loc.tip0, loc.tip1, loc.tip2, loc.tip3].map((tip) { - return Padding( - padding: const EdgeInsetsDirectional.only( - top: 8.0, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text(' • '), - const SizedBox(width: 4.0), - Expanded( - child: Text(tip), - ), - ], - ), - ); - }), - ], - ), - ), - ), - ); - - final finishButton = Align( - alignment: AlignmentDirectional.centerEnd, - child: FloatingActionButton.extended( - onPressed: onFinish, - label: Text(loc.finish.toUpperCase()), - icon: const Icon(Icons.check), - ), - ); - - return LayoutBuilder(builder: (context, consts) { - if (consts.maxWidth < kMobileBreakpoint.width) { - return PopScope( - canPop: false, - child: ListView( - padding: const EdgeInsetsDirectional.all(24.0), - children: [ - SizedBox( - height: consts.maxHeight * 0.875, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (server != null) addedCard, - tipsCard, - const SizedBox(height: 8.0), - finishButton, - const SizedBox(height: 12.0), - ], - ), - ), - Card( - child: Padding( - padding: const EdgeInsetsDirectional.symmetric(vertical: 8), - child: Column( - mainAxisSize: MainAxisSize.min, - children: server!.devices.sorted().map((device) { - return DesktopDeviceSelectorTile( - device: device, - selected: false, - selectable: false, - ); - }).toList(), - ), - ), - ) - ], - ), - ); - } else { - return PopScope( - canPop: false, - child: IntrinsicWidth( - child: Container( - margin: const EdgeInsetsDirectional.all(16.0), - constraints: BoxConstraints( - minWidth: MediaQuery.sizeOf(context).width / 2.5, - ), - child: Row(children: [ - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - if (server != null) addedCard, - tipsCard, - const SizedBox(height: 8.0), - finishButton, - ], - ), - ), - if (server != null && server!.devices.isNotEmpty) ...[ - const SizedBox(width: 16.0), - SizedBox( - width: kSidebarConstraints.maxWidth, - child: Card( - child: SingleChildScrollView( - padding: - const EdgeInsetsDirectional.symmetric(vertical: 8), - child: Column( - mainAxisSize: MainAxisSize.min, - children: server!.devices.sorted().map((device) { - return DesktopDeviceSelectorTile( - device: device, - selected: false, - selectable: false, - ); - }).toList(), - ), - ), - ), - ), - ] - ]), - ), - ), - ); - } - }); - } -} diff --git a/lib/utils/widgets/squared_icon_button.dart b/lib/widgets/squared_icon_button.dart similarity index 100% rename from lib/utils/widgets/squared_icon_button.dart rename to lib/widgets/squared_icon_button.dart diff --git a/lib/utils/widgets/tree_view.dart b/lib/widgets/tree_view.dart similarity index 100% rename from lib/utils/widgets/tree_view.dart rename to lib/widgets/tree_view.dart