diff --git a/lib/api/api_helpers.dart b/lib/api/api_helpers.dart index 0c6b32d4..3076af1c 100644 --- a/lib/api/api_helpers.dart +++ b/lib/api/api_helpers.dart @@ -22,6 +22,7 @@ import 'dart:io'; 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:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:path_provider/path_provider.dart'; @@ -167,7 +168,7 @@ class DevHttpOverrides extends HttpOverrides { } } - return true; + return SettingsProvider.instance.kAllowUntrustedCertificates.value; }; } } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 0dbe28ff..db139f0e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -425,6 +425,8 @@ "automaticDownloadUpdates": "Automatic download updates", "automaticDownloadUpdatesDescription": "Be among the first to get the latest updates, fixes and improvements as they roll out.", "updateHistory": "Update history", + "showReleaseNotes": "Show release notes", + "showReleaseNotesDescription": "Display release notes when a new version is installed", "newVersionAvailable": "New version available", "installVersion": "Install", "downloadVersion": "Download", @@ -456,8 +458,21 @@ "taskDownloadingEvent": "Downloading event", "@@@SETTINGS": {}, "defaultField": "Default", - "@@GENERAL": {}, "general": "General", + "generalSettingsSuggestion": "Notifications, Data Usage, Wakelock, etc", + "serverAndDevices": "Servers and Devices", + "serverAndDevicesSettingsSuggestion": "Connect to servers, manage devices, etc", + "eventsAndDownloads": "Events and Downloads", + "eventsAndDownloadsSettingsSuggestion": "Events history, downloads, etc", + "application": "Application", + "applicationSettingsSuggestion": "Appearance, theme, date and time, etc", + "privacyAndSecurity": "Privacy and Security", + "privacyAndSecuritySettingsSuggestion": "Data collection, error reporting, etc", + "updatesAndHelp": "Updates and Help", + "updatesAndHelpSettingsSuggestion": "Check for updates, update history, etc", + "advancedOptions": "Advanced Options", + "advancedOptionsSettingsSuggestion": "Funcionalidades em Beta, Opções de Desenvolvedor, etc", + "@@GENERAL": {}, "cycleTogglePeriod": "Layout cycle toggle period", "cycleTogglePeriodDescription": "The interval between layout changes when the cycle mode is enabled.", "notifications": "Notifications", @@ -465,6 +480,12 @@ "notificationClickBehavior": "Notification Click Behavior", "notificationClickBehaviorDescription": "Choose what happens when you click on a notification.", "showEventsScreen": "Show events history", + "@@@DATA_USAGE": {}, + "dataUsage": "Data Usage", + "streamsOnBackground": "Keep streams playing on background", + "streamsOnBackgroundDescription": "When to keep streams playing when the app is in background", + "automatic": "Automatic", + "wifiOnly": "Wifi Only", "@@EVENTS_AND_DOWNLOADS": {}, "chooseEveryDownloadsLocation": "Choose the location for every download", "chooseEveryDownloadsLocationDescription": "Whether to choose the location for each download or use the default location. When enabled, you will be prompted to choose the download directory for each download.", @@ -494,7 +515,6 @@ "convertToLocalTime": "Convert dates to the local timezone", "convertToLocalTimeDescription": "This will affect the date and time displayed in the app. This is useful when you are in a different timezone than the server. When disabled, the server timezone will be used.", "@@PRIVACY_AND_SECURITY": {}, - "privacyAndSecurity": "Privacy and Security", "allowDataCollection": "Allow Bluecherry to collect usage data", "allowDataCollectionDescription": "Allow Bluecherry to collect data to improve the app and provide better services. Data is collected anonymously and does not contain any personal information.", "automaticallyReportErrors": "Automatically report errors", @@ -537,6 +557,12 @@ } } }, + "@@SERVERS": {}, + "connectToServerAutomaticallyAtStartup": "Connect automatically at startup", + "connectToServerAutomaticallyAtStartupDescription": "If enabled, the server will be automatically connected when the app starts. This only applies to the new servers you add.", + "allowUntrustedCertificates": "Allow untrusted certificates", + "allowUntrustedCertificatesDescription": "Allow connecting to servers with untrusted certificates. This is useful when you are using self-signed certificates or certificates from unknown authorities.", + "certificateNotPassed": "Certificate not passed", "@@STREAMING": {}, "streamingSettings": "Streaming settings", "streamingType": "Streaming type", @@ -603,5 +629,8 @@ "rackNameExample": "Lab 1", "openServer": "Open server", "@SEARCH": {}, - "disableSearch": "Disable search" + "disableSearch": "Disable search", + "@@@Updates and Help": {}, + "help": "Help", + "licenses": "Licenses" } \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 034d2c14..0b230695 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -401,6 +401,8 @@ "automaticDownloadUpdates": "Téléchargement automatique des mises à jour", "automaticDownloadUpdatesDescription": "Faites parti des premiers à recevoir les dernières mises à jour, correctifs et améliorations quand elles sortent.", "updateHistory": "Historique de mises à jour", + "showReleaseNotes": "Show release notes", + "showReleaseNotesDescription": "Display release notes when a new version is installed", "newVersionAvailable": "Nouvelle version disponible", "installVersion": "Installer", "downloadVersion": "Télécharger", @@ -432,8 +434,21 @@ "taskDownloadingEvent": "Téléchargement de l'évènement", "@@@SETTINGS": {}, "defaultField": "Par défaut", - "@@GENERAL": {}, "general": "Général", + "generalSettingsSuggestion": "Notifications, Data Usage, Wakelock, etc", + "serverAndDevices": "Servers and Devices", + "serverAndDevicesSettingsSuggestion": "Connect to servers, manage devices, etc", + "eventsAndDownloads": "Events and Downloads", + "eventsAndDownloadsSettingsSuggestion": "Events history, downloads, etc", + "application": "Application", + "applicationSettingsSuggestion": "Appearance, theme, date and time, etc", + "privacyAndSecurity": "Sécurité et vie Privée", + "privacyAndSecuritySettingsSuggestion": "Data collection, error reporting, etc", + "updatesAndHelp": "Updates and Help", + "updatesAndHelpSettingsSuggestion": "Check for updates, update history, etc", + "advancedOptions": "Advanced Options", + "advancedOptionsSettingsSuggestion": "Funcionalidades em Beta, Opções de Desenvolvedor, etc", + "@@GENERAL": {}, "cycleTogglePeriod": "Durée du cycle de basculement", "cycleTogglePeriodDescription": "Intervalle de temps entre les changements de disposition quand le mode cycle est activé.", "notifications": "Notifications", @@ -441,6 +456,12 @@ "notificationClickBehavior": "Action de clic sur les notifications", "notificationClickBehaviorDescription": "Choisir ce qui se passe lorsque vous cliquez sur une notification.", "showEventsScreen": "Montrer le navigateur d'événements", + "@@@DATA_USAGE": {}, + "dataUsage": "Data Usage", + "streamsOnBackground": "Keep streams playing on background", + "streamsOnBackgroundDescription": "When to keep streams playing when the app is in background", + "automatic": "Automatic", + "wifiOnly": "Wifi Only", "@@EVENTS_AND_DOWNLOADS": {}, "chooseEveryDownloadsLocation": "Choisir l'emplacement pour chaque téléchargements", "chooseEveryDownloadsLocationDescription": "Choisir l'emplacement de chaque téléchargements ou utiliser l'emplacement par défaut. Lorsque activé vous devrez choisir l'emplacement pour chaque téléchargements.", @@ -470,7 +491,6 @@ "convertToLocalTime": "Convertir le temps à l'heure locale", "convertToLocalTimeDescription": "Convertir les temps affichés à l'heure locale. Cette option affecte l'heure et la date affichée dans l'application. Cette option est utile si le serveur est situé dans un autre fuseau horaire.", "@@PRIVACY_AND_SECURITY": {}, - "privacyAndSecurity": "Sécurité et vie Privée", "allowDataCollection": "Permettre à Bluecherry de collecter des données d'utilisation", "allowDataCollectionDescription": "Permettre à Bluecherry de collecter des données améliore l'application et fournit un meilleur service. Les données collectées ne contiennent aucune information personnelle.", "automaticallyReportErrors": "Signaler automatiquement les erreurs", @@ -511,6 +531,12 @@ "time": {} } }, + "@@SERVERS": {}, + "connectToServerAutomaticallyAtStartup": "Connect automatically at startup", + "connectToServerAutomaticallyAtStartupDescription": "If enabled, the server will be automatically connected when the app starts. This only applies to the new servers you add.", + "allowUntrustedCertificates": "Allow untrusted certificates", + "allowUntrustedCertificatesDescription": "Allow connecting to servers with untrusted certificates. This is useful when you are using self-signed certificates or certificates from unknown authorities.", + "certificateNotPassed": "Certificate not passed", "@@STREAMING": {}, "streamingSettings": "Paramètre de diffusion", "streamingType": "Type de diffusion", @@ -577,5 +603,8 @@ "rackNameExample": "Labo 1", "openServer": "Serveur libre", "@SEARCH": {}, - "disableSearch": "Désactiver la recherche" -} + "disableSearch": "Désactiver la recherche", + "@@@Updates and Help": {}, + "help": "Help", + "licenses": "Licenças" +} \ No newline at end of file diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 9bd4ee3b..787ba997 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -425,6 +425,8 @@ "automaticDownloadUpdates": "Pobieraj aktualizacje automatycznie", "automaticDownloadUpdatesDescription": "Bądź jedną z pierwszych osób, które otrzymają najnowsze aktualizacje, poprawki i ulepszenia w miarę ich wdrażania.", "updateHistory": "Historia aktualizacji", + "showReleaseNotes": "Show release notes", + "showReleaseNotesDescription": "Display release notes when a new version is installed", "newVersionAvailable": "Dostępna nowa wersja", "installVersion": "Instaluj", "downloadVersion": "Pobierz", @@ -456,8 +458,21 @@ "taskDownloadingEvent": "Pobieranie zdarzenia", "@@@SETTINGS": {}, "defaultField": "Default", - "@@GENERAL": {}, "general": "General", + "generalSettingsSuggestion": "Notifications, Data Usage, Wakelock, etc", + "serverAndDevices": "Servers and Devices", + "serverAndDevicesSettingsSuggestion": "Connect to servers, manage devices, etc", + "eventsAndDownloads": "Events and Downloads", + "eventsAndDownloadsSettingsSuggestion": "Events history, downloads, etc", + "application": "Application", + "applicationSettingsSuggestion": "Appearance, theme, date and time, etc", + "privacyAndSecurity": "Privacy and Security", + "privacyAndSecuritySettingsSuggestion": "Data collection, error reporting, etc", + "updatesAndHelp": "Updates and Help", + "updatesAndHelpSettingsSuggestion": "Check for updates, update history, etc", + "advancedOptions": "Advanced Options", + "advancedOptionsSettingsSuggestion": "Funcionalidades em Beta, Opções de Desenvolvedor, etc", + "@@GENERAL": {}, "cycleTogglePeriod": "Okres cyklicznego przełączania układu", "cycleTogglePeriodDescription": "The interval between layout changes when the cycle mode is enabled.", "notifications": "Notifications", @@ -465,6 +480,12 @@ "notificationClickBehavior": "Zachowanie po kliknięciu na powiadomienie", "notificationClickBehaviorDescription": "Choose what happens when you click on a notification.", "showEventsScreen": "Pokaż historię zdarzeń", + "@@@DATA_USAGE": {}, + "dataUsage": "Data Usage", + "streamsOnBackground": "Keep streams playing on background", + "streamsOnBackgroundDescription": "When to keep streams playing when the app is in background", + "automatic": "Automatic", + "wifiOnly": "Wifi Only", "@@EVENTS_AND_DOWNLOADS": {}, "chooseEveryDownloadsLocation": "Choose the location for every download", "chooseEveryDownloadsLocationDescription": "Whether to choose the location for each download or use the default location. When enabled, you will be prompted to choose the download directory for each download.", @@ -494,7 +515,6 @@ "convertToLocalTime": "Convert dates to the local timezone", "convertToLocalTimeDescription": "This will affect the date and time displayed in the app. This is useful when you are in a different timezone than the server. When disabled, the server timezone will be used.", "@@PRIVACY_AND_SECURITY": {}, - "privacyAndSecurity": "Privacy and Security", "allowDataCollection": "Allow Bluecherry to collect usage data", "allowDataCollectionDescription": "Allow Bluecherry to collect data to improve the app and provide better services. Data is collected anonymously and does not contain any personal information.", "automaticallyReportErrors": "Automatically report errors", @@ -537,6 +557,12 @@ } } }, + "@@SERVERS": {}, + "connectToServerAutomaticallyAtStartup": "Connect automatically at startup", + "connectToServerAutomaticallyAtStartupDescription": "If enabled, the server will be automatically connected when the app starts. This only applies to the new servers you add.", + "allowUntrustedCertificates": "Allow untrusted certificates", + "allowUntrustedCertificatesDescription": "Allow connecting to servers with untrusted certificates. This is useful when you are using self-signed certificates or certificates from unknown authorities.", + "certificateNotPassed": "Certificate not passed", "@@STREAMING": {}, "streamingSettings": "Streaming settings", "streamingType": "Streaming type", @@ -603,5 +629,8 @@ "rackNameExample": "Lab 1", "openServer": "Open server", "@SEARCH": {}, - "disableSearch": "Disable search" + "disableSearch": "Disable search", + "@@@Updates and Help": {}, + "help": "Help", + "licenses": "Licenças" } \ No newline at end of file diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 1fbf1cc9..45ab1b54 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -263,7 +263,7 @@ "downloaded": "Baixado", "downloading": "Baixando", "seeInDownloads": "Ver nos Downloads", - "downloadPath": "Diretório de Download", + "downloadPath": "Local de Download", "delete": "Deletar", "showInFiles": "Ver no Explorador de Arquivos", "noDownloads": "Você ainda não baixou nenhum evento :/", @@ -425,6 +425,8 @@ "automaticDownloadUpdates": "Baixar atualizações automaticamente", "automaticDownloadUpdatesDescription": "Seja um dos primeiros a receber as atualizações, correções e melhorias mais recentes assim que lançadas.", "updateHistory": "Histórico de atualizações", + "showReleaseNotes": "Mostrar notas de atualização", + "showReleaseNotesDescription": "Mostrar as notas de atualização quando uma versão for instalada.", "newVersionAvailable": "Nova versão disponível!", "installVersion": "Instalar", "downloadVersion": "Baixar", @@ -456,18 +458,37 @@ "taskDownloadingEvent": "Baixando evento", "@@@SETTINGS": {}, "defaultField": "Padrão", - "@@GENERAL": {}, "general": "Geral", + "generalSettingsSuggestion": "Notificações, Layouts, Wakelock, etc", + "serverAndDevices": "Servidores e Dispositivos", + "serverAndDevicesSettingsSuggestion": "Servidores, Dispositivos, Streaming, etc", + "eventsAndDownloads": "Eventos e Downloads", + "eventsAndDownloadsSettingsSuggestion": "Eventos, Histórico de Eventos, Downloads, etc", + "application": "Aplicação", + "applicationSettingsSuggestion": "Aparência, idioma, etc", + "privacyAndSecurity": "Privacidade e Segurança", + "privacyAndSecuritySettingsSuggestion": "Coleta de dados, relatórios de erros, etc", + "updatesAndHelp": "Atualizações e Ajuda", + "updatesAndHelpSettingsSuggestion": "Verificar atualizações, histórico de atualizações, etc", + "advancedOptions": "Opções Avançadas", + "advancedOptionsSettingsSuggestion": "Funcionalidades em Beta, Opções de Desenvolvedor, etc", + "@@GENERAL": {}, "cycleTogglePeriod": "Duração da alternância de layouts", "cycleTogglePeriodDescription": "O intervalo entre alterações de layout quando a alternância está ativada.", - "notifications": "Notifications", - "notificationsEnabled": "Notifications enabled", + "notifications": "Notificações", + "notificationsEnabled": "Notificações ativadas", "notificationClickBehavior": "Ação ao clicar na notificação", "notificationClickBehaviorDescription": "Escolha o que acontece quando você clica em uma notificação.", "showEventsScreen": "Mostar histórico de eventos", + "@@@DATA_USAGE": {}, + "dataUsage": "Uso de Dados", + "streamsOnBackground": "Manter transmissões em segundo plano", + "streamsOnBackgroundDescription": "Quando manter as transmissões em segundo plano quando o aplicativo estiver em segundo plano", + "automatic": "Automatico", + "wifiOnly": "Somente Wi-Fi", "@@EVENTS_AND_DOWNLOADS": {}, "chooseEveryDownloadsLocation": "Escolher a localização de cada download", - "chooseEveryDownloadsLocationDescription": "Se você deseja escolher a localização de cada download ou usar a localização padrão. Quando ativado, você será solicitado a escolher o diretório de download para cada download.", + "chooseEveryDownloadsLocationDescription": "Se você deseja escolher a localização de cada download ou usar a localização padrão. Quando ativado, você será solicitado a escolher a localização de cada download.", "allowCloseWhenDownloading": "Permitir fechar o aplicativo quando houver downloads em andamento", "events": "Eventos", "initialEventSpeed": "Velocidade inicial", @@ -480,7 +501,7 @@ "firstEventInitialPoint": "Primeiro evento", "hourAgoInitialPoint": "1 hora atrás", "@@APPLICATION": {}, - "appearance": "Appearance", + "appearance": "Visualização", "theme": "Aparência", "themeDescription": "Mude a aparência do aplicativo", "system": "Padrão do Sistema", @@ -491,10 +512,9 @@ "dateFormatDescription": "Qual formato usar para exibir datas", "timeFormat": "Formato de Hora", "timeFormatDescription": "Qual formato usar para exibir horas", - "convertToLocalTime": "Convert dates to the local timezone", - "convertToLocalTimeDescription": "This will affect the date and time displayed in the app. This is useful when you are in a different timezone than the server. When disabled, the server timezone will be used.", + "convertToLocalTime": "Converter datas para o fuso-horário local", + "convertToLocalTimeDescription": "Isso afetará a data e a hora exibidas no aplicativo. É útil quando você está em um fuso horário diferente do servidor. Quando desativado, o fuso horário do servidor será usado.", "@@PRIVACY_AND_SECURITY": {}, - "privacyAndSecurity": "Privacidade e Segurança", "allowDataCollection": "Permitir que Bluecherry colete dados de uso", "allowDataCollectionDescription": "Permitir que Bluecherry colete dados para melhorar o aplicativo e fornecer serviços melhores. Os dados são coletados anonimamente e não contêm informações pessoais.", "automaticallyReportErrors": "Relatar erros automaticamente", @@ -537,6 +557,12 @@ } } }, + "@@SERVERS": {}, + "connectToServerAutomaticallyAtStartup": "Conectar automaticamente ao iniciar", + "connectToServerAutomaticallyAtStartupDescription": "Se ativado, o servidor será conectado automaticamente quando o aplicativo for iniciado. Isso só se aplica aos novos servidores que você adicionar.", + "allowUntrustedCertificates": "Permitir certificados não confiáveis", + "allowUntrustedCertificatesDescription": "Permitir a conexão a servidores com certificados não confiáveis. Isso é útil quando você está usando certificados autoassinados ou certificados de autoridades desconhecidas.", + "certificateNotPassed": "Certificado não autorizado", "@@STREAMING": {}, "streamingSettings": "Configurações de streaming", "streamingType": "Tipo de streaming", @@ -603,5 +629,8 @@ "rackNameExample": "Lab 1", "openServer": "Abrir servidor", "@SEARCH": {}, - "disableSearch": "Desativar pesquisa" + "disableSearch": "Desativar pesquisa", + "@@@Updates and Help": {}, + "help": "Ajuda", + "licenses": "Licenças" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 4c07879d..f4b00d8d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -52,6 +52,7 @@ 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/splash_screen.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -220,8 +221,11 @@ class _UnityAppState extends State /// Whether the app is in background or not bool isInBackground = false; + Timer? backgroundTimer; + @override Future didChangeAppLifecycleState(AppLifecycleState state) async { + final settings = SettingsProvider.instance; switch (state) { case AppLifecycleState.resumed: debugPrint('in foreground'); @@ -233,6 +237,10 @@ class _UnityAppState extends State } isInBackground = false; + + backgroundTimer?.cancel(); + backgroundTimer = null; + UnityPlayers.playAll(); break; case AppLifecycleState.inactive: case AppLifecycleState.paused: @@ -240,6 +248,24 @@ class _UnityAppState extends State case AppLifecycleState.hidden: debugPrint('in background'); isInBackground = true; + + // After 30 seconds in background, pause all the streams + backgroundTimer = Timer(const Duration(seconds: 30), () async { + switch (settings.kStreamOnBackground.value) { + case NetworkUsage.auto: + case NetworkUsage.wifiOnly: + debugPrint('Pausing all streams'); + final connectionType = await Connectivity().checkConnectivity(); + if (connectionType == ConnectivityResult.mobile || + connectionType == ConnectivityResult.bluetooth) { + UnityPlayers.pauseAll(); + } + break; + case NetworkUsage.never: + break; + } + backgroundTimer = null; + }); break; } } @@ -310,8 +336,8 @@ class _UnityAppState extends State value: EventsProvider.instance, ), ], - child: Consumer( - builder: (context, settings, _) => MaterialApp( + child: Consumer(builder: (context, settings, _) { + return MaterialApp( debugShowCheckedModeBanner: false, navigatorKey: navigatorKey, navigatorObservers: [NObserver()], @@ -396,8 +422,8 @@ class _UnityAppState extends State return null; }, - ), - ), + ); + }), ); } } diff --git a/lib/models/layout.dart b/lib/models/layout.dart index 515350fb..e6964357 100644 --- a/lib/models/layout.dart +++ b/lib/models/layout.dart @@ -40,7 +40,7 @@ enum DesktopLayoutType { /// If selected, only 4 views will be show in the grid. Each view can have 4 /// cameras displayed, creating a soft and compact layout view - compactView, + compactView; } /// A layout is a view that can contain one or more [Device]s. diff --git a/lib/providers/mobile_view_provider.dart b/lib/providers/mobile_view_provider.dart index 115eed18..0b7f3ff4 100644 --- a/lib/providers/mobile_view_provider.dart +++ b/lib/providers/mobile_view_provider.dart @@ -37,7 +37,7 @@ class MobileViewProvider extends UnityProvider { return instance; } - /// Keeps camera [Device]s order/layout to show inside the [MobileDeviceGrid] in the order user last saved them. + /// Keeps camera [Device]s order/layout to show inside the [SmallDeviceGrid] in the order user last saved them. /// /// ```dart /// { diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 49ede215..9fe29d6b 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -31,7 +31,20 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:intl/intl.dart'; import 'package:unity_video_player/unity_video_player.dart'; -enum NetworkUsage { auto, wifiOnly, never } +enum NetworkUsage { + auto, + wifiOnly, + never; + + String locale(BuildContext context) { + final loc = AppLocalizations.of(context); + return switch (this) { + NetworkUsage.auto => loc.automatic, + NetworkUsage.wifiOnly => loc.wifiOnly, + NetworkUsage.never => loc.never, + }; + } +} enum EnabledPreference { on, ask, never } @@ -186,6 +199,16 @@ class SettingsProvider extends UnityProvider { saveAs: (value) => value.index.toString(), ); + // Server settings + final kConnectAutomaticallyAtStartup = _SettingsOption( + def: true, + key: 'server.connect_automatically_at_startup', + ); + final kAllowUntrustedCertificates = _SettingsOption( + def: true, + key: 'server.allow_untrusted_certificates', + ); + // Streaming settings final kStreamingType = _SettingsOption( def: kIsWeb ? StreamingType.hls : StreamingType.rtsp, @@ -317,7 +340,7 @@ class SettingsProvider extends UnityProvider { ); // Acessibility - final kAnimationsEnabled = _SettingsOption( + final kAnimationsEnabled = _SettingsOption( def: true, key: 'accessibility.animations_enabled', ); @@ -408,6 +431,8 @@ class SettingsProvider extends UnityProvider { kNotificationClickBehavior.loadData(data), kAutomaticStreaming.loadData(data), kStreamOnBackground.loadData(data), + kConnectAutomaticallyAtStartup.loadData(data), + kAllowUntrustedCertificates.loadData(data), kStreamingType.loadData(data), kRTSPProtocol.loadData(data), kRenderingQuality.loadData(data), @@ -469,6 +494,10 @@ class SettingsProvider extends UnityProvider { kAutomaticStreaming.saveAs(kAutomaticStreaming.value), kStreamOnBackground.key: kStreamOnBackground.saveAs(kStreamOnBackground.value), + kConnectAutomaticallyAtStartup.key: kConnectAutomaticallyAtStartup + .saveAs(kConnectAutomaticallyAtStartup.value), + kAllowUntrustedCertificates.key: kAllowUntrustedCertificates + .saveAs(kAllowUntrustedCertificates.value), kStreamingType.key: kStreamingType.saveAs(kStreamingType.value), kRTSPProtocol.key: kRTSPProtocol.saveAs(kRTSPProtocol.value), kRenderingQuality.key: diff --git a/lib/screens/direct_camera.dart b/lib/screens/direct_camera.dart index cc9fbd32..308e3e7d 100644 --- a/lib/screens/direct_camera.dart +++ b/lib/screens/direct_camera.dart @@ -114,8 +114,11 @@ class _DevicesForServer extends StatelessWidget { final serverIndicator = SubHeader( server.name, materialType: MaterialType.canvas, - subtext: - server.online ? loc.nDevices(server.devices.length) : loc.offline, + subtext: !server.passedCertificates + ? loc.certificateNotPassed + : server.online + ? loc.nDevices(server.devices.length) + : loc.offline, subtextStyle: TextStyle( color: !server.online ? theme.colorScheme.error : null, ), diff --git a/lib/screens/events_browser/events_screen_mobile.dart b/lib/screens/events_browser/events_screen_mobile.dart index f7b5b299..f58f21cd 100644 --- a/lib/screens/events_browser/events_screen_mobile.dart +++ b/lib/screens/events_browser/events_screen_mobile.dart @@ -137,9 +137,11 @@ class _EventsScreenMobileState extends State { style: const TextStyle(fontWeight: FontWeight.bold), ), subtitle: Text( - server.online - ? loc.nEvents(serverEvents.length) - : loc.offline, + !server.passedCertificates + ? loc.certificateNotPassed + : server.online + ? loc.nEvents(serverEvents.length) + : loc.offline, ), trailing: !server.online ? Icon( diff --git a/lib/screens/layouts/desktop/layout_manager.dart b/lib/screens/layouts/desktop/layout_manager.dart index f57e94ee..ee314e96 100644 --- a/lib/screens/layouts/desktop/layout_manager.dart +++ b/lib/screens/layouts/desktop/layout_manager.dart @@ -35,6 +35,34 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; +extension DesktopLayoutTypeExtension on DesktopLayoutType { + String text(BuildContext context) { + final loc = AppLocalizations.of(context); + + return switch (this) { + DesktopLayoutType.singleView => loc.singleView, + DesktopLayoutType.multipleView => loc.multipleView, + DesktopLayoutType.compactView => loc.compactView, + }; + } + + IconData get icon { + return switch (this) { + DesktopLayoutType.singleView => Icons.crop_square, + DesktopLayoutType.multipleView => Icons.view_compact_outlined, + DesktopLayoutType.compactView => Icons.view_comfy_outlined, + }; + } + + IconData get selectedIcon { + return switch (this) { + DesktopLayoutType.singleView => Icons.crop_square_rounded, + DesktopLayoutType.multipleView => Icons.view_compact_rounded, + DesktopLayoutType.compactView => Icons.view_comfy_rounded, + }; + } +} + class LayoutManager extends StatefulWidget { final Widget collapseButton; @@ -218,8 +246,8 @@ class _LayoutTileState extends State { enabled: !widget.selected, child: Icon( widget.selected - ? selectedIconForLayout(widget.layout.type) - : iconForLayout(widget.layout.type), + ? widget.layout.type.selectedIcon + : widget.layout.type.icon, key: ValueKey(widget.layout.type), size: 20.0, ), @@ -316,49 +344,11 @@ class _LayoutTileState extends State { } } -String textForLayout(BuildContext context, DesktopLayoutType type) { - final loc = AppLocalizations.of(context); - - switch (type) { - case DesktopLayoutType.singleView: - return loc.singleView; - case DesktopLayoutType.multipleView: - return loc.multipleView; - case DesktopLayoutType.compactView: - return loc.compactView; - } -} - -IconData iconForLayout(DesktopLayoutType type) { - switch (type) { - case DesktopLayoutType.singleView: - return Icons.crop_square; - case DesktopLayoutType.multipleView: - return Icons.view_compact_outlined; - case DesktopLayoutType.compactView: - return Icons.view_comfy_outlined; - } -} - -IconData selectedIconForLayout(DesktopLayoutType type) { - switch (type) { - case DesktopLayoutType.singleView: - return Icons.square_rounded; - case DesktopLayoutType.multipleView: - return Icons.view_compact; - case DesktopLayoutType.compactView: - return Icons.view_comfy; - } -} - class _LayoutTypeChooser extends StatelessWidget { final int selected; final ValueChanged onSelect; - const _LayoutTypeChooser({ - required this.selected, - required this.onSelect, - }); + const _LayoutTypeChooser({required this.selected, required this.onSelect}); @override Widget build(BuildContext context) { @@ -369,8 +359,7 @@ class _LayoutTypeChooser extends StatelessWidget { onPressed: onSelect, children: DesktopLayoutType.values.map((type) { final isSelected = type.index == selected; - final icon = - isSelected ? selectedIconForLayout(type) : iconForLayout(type); + final icon = isSelected ? type.selectedIcon : type.icon; return Row(children: [ const SizedBox(width: 12.0), @@ -379,7 +368,7 @@ class _LayoutTypeChooser extends StatelessWidget { child: Icon(icon, key: ValueKey(icon), size: 22.0), ), const SizedBox(width: 8.0), - Text(textForLayout(context, type)), + Text(type.text(context)), const SizedBox(width: 16.0), ]); }).toList(), @@ -395,12 +384,12 @@ class NewLayoutDialog extends StatefulWidget { } class _NewLayoutDialogState extends State { - final controller = TextEditingController(); - int selected = 1; + final _nameController = TextEditingController(); + int _typeIndex = 1; @override void dispose() { - controller.dispose(); + _nameController.dispose(); super.dispose(); } @@ -500,7 +489,7 @@ class _NewLayoutDialogState extends State { title: Text(loc.createNewLayout), content: Column(mainAxisSize: MainAxisSize.min, children: [ TextField( - controller: controller, + controller: _nameController, decoration: InputDecoration( hintText: loc.layoutNameHint, label: Text(loc.layoutName), @@ -509,9 +498,9 @@ class _NewLayoutDialogState extends State { ), SubHeader(loc.layoutTypeLabel, padding: EdgeInsetsDirectional.zero), _LayoutTypeChooser( - selected: selected, + selected: _typeIndex, onSelect: (index) { - if (mounted) setState(() => selected = index); + if (mounted) setState(() => _typeIndex = index); }, ), ]), @@ -538,9 +527,10 @@ class _NewLayoutDialogState extends State { FilledButton( onPressed: () { view.addLayout(Layout( - name: - controller.text.isNotEmpty ? controller.text : fallbackName, - type: DesktopLayoutType.values[selected], + name: _nameController.text.isNotEmpty + ? _nameController.text + : fallbackName, + type: DesktopLayoutType.values[_typeIndex], devices: [], )); Navigator.of(context).pop(); @@ -563,12 +553,14 @@ class EditLayoutDialog extends StatefulWidget { } class _EditLayoutDialogState extends State { - late final controller = TextEditingController(text: widget.layout.name); + late final layoutNameController = TextEditingController( + text: widget.layout.name, + ); late int selected = widget.layout.type.index; @override void dispose() { - controller.dispose(); + layoutNameController.dispose(); super.dispose(); } @@ -598,7 +590,7 @@ class _EditLayoutDialogState extends State { ]), content: Column(mainAxisSize: MainAxisSize.min, children: [ TextField( - controller: controller, + controller: layoutNameController, decoration: InputDecoration( hintText: loc.layoutNameHint, label: Text(loc.layoutName), @@ -623,7 +615,9 @@ class _EditLayoutDialogState extends State { view.updateLayout( widget.layout, widget.layout.copyWith( - name: controller.text.isEmpty ? null : controller.text, + name: layoutNameController.text.isEmpty + ? null + : layoutNameController.text, type: DesktopLayoutType.values[selected], ), ); diff --git a/lib/screens/layouts/desktop/desktop_device_grid.dart b/lib/screens/layouts/desktop/layout_view.dart similarity index 51% rename from lib/screens/layouts/desktop/desktop_device_grid.dart rename to lib/screens/layouts/desktop/layout_view.dart index 1e0e283e..f650ffbd 100644 --- a/lib/screens/layouts/desktop/desktop_device_grid.dart +++ b/lib/screens/layouts/desktop/layout_view.dart @@ -23,13 +23,13 @@ const _kReverseBreakpoint = 900.0; typedef FoldedDevices = List>; -class DesktopDeviceGrid extends StatefulWidget { - const DesktopDeviceGrid({super.key, required this.width}); +class LargeDeviceGrid extends StatefulWidget { + const LargeDeviceGrid({super.key, required this.width}); final double width; @override - State createState() => _DesktopDeviceGridState(); + State createState() => _LargeDeviceGridState(); } /// Calculates how many views there will be in the grid view @@ -47,7 +47,7 @@ int calculateCrossAxisCount(int deviceAmount) { return count; } -class _DesktopDeviceGridState extends State { +class _LargeDeviceGridState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -276,7 +276,7 @@ class LayoutView extends StatelessWidget { } final isReversed = - context.findAncestorWidgetOfExactType()!.width <= + context.findAncestorWidgetOfExactType()!.width <= _kReverseBreakpoint; return Material( @@ -294,53 +294,6 @@ class LayoutView extends StatelessWidget { } } -class DesktopDeviceTile extends StatefulWidget { - const DesktopDeviceTile({super.key, required this.device}); - - final Device device; - - @override - State createState() => _DesktopDeviceTileState(); -} - -class _DesktopDeviceTileState extends State { - late UnityVideoFit fit = widget.device.server.additionalSettings.videoFit ?? - SettingsProvider.instance.kVideoFit.value; - - @override - Widget build(BuildContext context) { - // watch for changes in the players list. usually happens when reloading - // or releasing a device - context.watch(); - final videoPlayer = UnityPlayers.players[widget.device.uuid]; - - if (videoPlayer == null) { - return Card( - clipBehavior: Clip.hardEdge, - child: DesktopTileViewport( - controller: null, - device: widget.device, - onFitChanged: (fit) => setState(() => this.fit = fit), - ), - ); - } - - return UnityVideoView( - key: ValueKey(widget.device.fullName), - heroTag: widget.device.streamURL, - player: videoPlayer, - fit: fit, - paneBuilder: (context, controller) { - return DesktopTileViewport( - controller: controller, - device: widget.device, - onFitChanged: (fit) => setState(() => this.fit = fit), - ); - }, - ); - } -} - class DesktopCompactTile extends StatelessWidget { const DesktopCompactTile({ super.key, @@ -384,301 +337,6 @@ class DesktopCompactTile extends StatelessWidget { } } -class DesktopTileViewport extends StatefulWidget { - final UnityVideoPlayer? controller; - final Device device; - final ValueChanged onFitChanged; - - const DesktopTileViewport({ - super.key, - required this.controller, - required this.device, - required this.onFitChanged, - }); - - @override - State createState() => _DesktopTileViewportState(); -} - -class _DesktopTileViewportState extends State { - bool ptzEnabled = false; - late double? volume = widget.controller?.volume; - - void updateVolume() { - if (widget.controller != null && mounted) { - setState(() => volume = widget.controller!.volume); - } - } - - @override - void initState() { - super.initState(); - if (widget.controller != null) { - updateVolume(); - } - } - - @override - void didUpdateWidget(covariant DesktopTileViewport oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.controller == null && widget.controller != null) { - updateVolume(); - } - } - - @override - Widget build(BuildContext context) { - final loc = AppLocalizations.of(context); - final theme = Theme.of(context); - final view = context.watch(); - final settings = context.watch(); - final closeButton = SquaredIconButton( - icon: Icon( - Icons.close_outlined, - color: theme.colorScheme.error, - size: 18.0, - ), - tooltip: loc.removeCamera, - onPressed: () { - view.remove(widget.device); - }, - ); - - final video = UnityVideoView.maybeOf(context); - final error = video?.error; - final isSubView = AlternativeWindow.maybeOf(context) != null; - - final reloadButton = SquaredIconButton( - icon: Icon( - Icons.replay_outlined, - shadows: outlinedIcon(), - color: Colors.white, - size: 16.0, - ), - tooltip: loc.reloadCamera, - onPressed: () async { - await UnityPlayers.reloadDevice(widget.device); - setState(() {}); - }, - ); - - Widget foreground = PTZController( - enabled: ptzEnabled, - device: widget.device, - builder: (context, commands, constraints) { - final states = HoverButton.of(context).states; - - final fit = - context.findAncestorWidgetOfExactType()?.fit ?? - widget.device.server.additionalSettings.videoFit ?? - settings.kVideoFit.value; - - return Stack(children: [ - Positioned.fill(child: MulticastViewport(device: widget.device)), - if (error != null) - Positioned.fill(child: ErrorWarning(message: error)), - IgnorePointer( - child: Padding( - padding: const EdgeInsetsDirectional.symmetric( - horizontal: 12.0, - vertical: 8.0, - ), - child: RichText( - text: TextSpan( - text: widget.device.name, - style: theme.textTheme.labelLarge?.copyWith( - color: Colors.white, - shadows: outlinedText(), - ), - children: [ - if (states.isHovering) - TextSpan( - text: '\n' - '${widget.device.externalData?.rackName ?? widget.device.server.name}', - style: theme.textTheme.labelSmall?.copyWith( - color: Colors.white, - shadows: outlinedText(), - ), - ), - if (states.isHovering && settings.kShowDebugInfo.value) - TextSpan( - text: '\nsource: ${video?.player.dataSource}' - '\nposition: ${video?.player.currentPos}' - '\nduration ${video?.player.duration}', - style: theme.textTheme.labelSmall?.copyWith( - color: Colors.white, - shadows: outlinedText(), - ), - ), - ], - ), - ), - ), - ), - PositionedDirectional( - end: 16.0, - top: 50.0, - child: PTZData(commands: commands), - ), - if (video != null) ...[ - PositionedDirectional( - end: 0, - start: 0, - bottom: 4.0, - child: Row(crossAxisAlignment: CrossAxisAlignment.end, children: [ - if (states.isHovering && error == null && !video.isLoading) ...[ - const SizedBox(width: 12.0), - if (widget.device.hasPTZ) - PTZToggleButton( - ptzEnabled: ptzEnabled, - onChanged: (enabled) => - setState(() => ptzEnabled = enabled), - ), - const Spacer(), - if (!video.isLoading) - () { - final isMuted = volume == 0.0; - return SquaredIconButton( - icon: Icon( - isMuted - ? Icons.volume_mute_rounded - : Icons.volume_up_rounded, - shadows: outlinedIcon(), - color: Colors.white, - size: 16.0, - ), - tooltip: isMuted ? loc.enableAudio : loc.disableAudio, - onPressed: () async { - if (isMuted) { - await widget.controller!.setVolume(1.0); - } else { - await widget.controller!.setVolume(0.0); - } - - updateVolume(); - }, - ); - }(), - if (isDesktopPlatform && !isSubView && !video.isLoading) - SquaredIconButton( - icon: Icon( - Icons.open_in_new_sharp, - shadows: outlinedIcon(), - color: Colors.white, - size: 16.0, - ), - tooltip: loc.openInANewWindow, - onPressed: () { - widget.device.openInANewWindow(); - }, - ), - if (!isSubView && !video.isLoading) - SquaredIconButton( - icon: Icon( - Icons.fullscreen_rounded, - shadows: outlinedIcon(), - color: Colors.white, - size: 16.0, - ), - tooltip: loc.showFullscreenCamera, - onPressed: () async { - UnityPlayers.openFullscreen( - context, - widget.device, - ptzEnabled: ptzEnabled, - ); - }, - ), - reloadButton, - // CameraViewFitButton( - // fit: fit, - // onChanged: widget.onFitChanged, - // ), - ] else ...[ - const Spacer(), - if (states.isHovering) reloadButton, - ], - Padding( - padding: const EdgeInsetsDirectional.only( - start: 6.0, - end: 6.0, - bottom: 6.0, - ), - child: VideoStatusLabel( - video: video, - device: widget.device, - ), - ), - ]), - ), - if (!isSubView && - view.currentLayout.devices.contains(widget.device)) - PositionedDirectional( - top: 4.0, - end: 4.0, - child: AnimatedOpacity( - opacity: !states.isHovering ? 0 : 1, - duration: const Duration(milliseconds: 200), - curve: Curves.easeInOut, - child: - Row(mainAxisAlignment: MainAxisAlignment.end, children: [ - SquaredIconButton( - icon: Icon( - moreIconData, - shadows: outlinedIcon(), - color: Colors.white, - ), - tooltip: loc.more, - onPressed: () async { - final device = await showStreamDataDialog( - context, - device: widget.device, - ptzEnabled: ptzEnabled, - onPTZEnabledChanged: (enabled) => setState(() { - ptzEnabled = enabled; - }), - fit: fit, - onFitChanged: widget.onFitChanged, - ); - if (device != null && mounted) { - view.updateDevice( - widget.device, - device, - reload: device.url != widget.device.url || - device.preferredStreamingType != - widget.device.preferredStreamingType, - ); - updateVolume(); - } - }, - ), - closeButton, - ]), - ), - ), - ], - ]); - }, - ); - - return TooltipTheme( - data: TooltipTheme.of(context).copyWith( - preferBelow: false, - verticalOffset: 20.0, - decoration: BoxDecoration( - color: theme.brightness == Brightness.light - ? Colors.black - : Colors.white, - borderRadius: isMobile - ? BorderRadius.circular(16.0) - : BorderRadius.circular(6.0), - ), - ), - child: foreground, - ); - } -} - class PresetsDialog extends StatelessWidget { final Device device; final bool hasSelected = false; diff --git a/lib/screens/layouts/desktop/multicast_view.dart b/lib/screens/layouts/desktop/multicast_view.dart index ff4ad3ca..071d422c 100644 --- a/lib/screens/layouts/desktop/multicast_view.dart +++ b/lib/screens/layouts/desktop/multicast_view.dart @@ -34,10 +34,7 @@ import 'package:unity_video_player/unity_video_player.dart'; class MulticastViewport extends StatefulWidget { final Device device; - const MulticastViewport({ - super.key, - required this.device, - }); + const MulticastViewport({super.key, required this.device}); @override State createState() => _MulticastViewportState(); diff --git a/lib/screens/layouts/desktop/desktop_sidebar.dart b/lib/screens/layouts/desktop/sidebar.dart similarity index 91% rename from lib/screens/layouts/desktop/desktop_sidebar.dart rename to lib/screens/layouts/desktop/sidebar.dart index a005ed74..bee2b45c 100644 --- a/lib/screens/layouts/desktop/desktop_sidebar.dart +++ b/lib/screens/layouts/desktop/sidebar.dart @@ -99,9 +99,11 @@ class _DesktopSidebarState extends State { child: SubHeader( server.name, materialType: MaterialType.canvas, - subtext: server.online - ? loc.nDevices(devices.length) - : loc.offline, + subtext: !server.passedCertificates + ? loc.certificateNotPassed + : server.online + ? loc.nDevices(devices.length) + : loc.offline, subtextStyle: TextStyle( color: !server.online ? theme.colorScheme.error @@ -168,7 +170,7 @@ class _DesktopSidebarState extends State { final selected = view.currentLayout.devices.contains(device); - final tile = DesktopDeviceSelectorTile( + final tile = DeviceSelectorTile( device: device, selected: selected, ); @@ -229,34 +231,31 @@ class NoServers extends StatelessWidget { final home = context.watch(); return Padding( padding: const EdgeInsetsDirectional.all(8.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.dns, - size: 48.0, - ), - const SizedBox(height: 6.0), - Text(loc.noServersAdded, textAlign: TextAlign.center), - Text.rich( - TextSpan( - text: loc.howToAddServer, - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - ), - recognizer: TapGestureRecognizer() - ..onTap = () => home.setTab(UnityTab.addServer, context), + child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + const Icon( + Icons.dns, + size: 48.0, + ), + const SizedBox(height: 6.0), + Text(loc.noServersAdded, textAlign: TextAlign.center), + Text.rich( + TextSpan( + text: loc.howToAddServer, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, ), - textAlign: TextAlign.center, + recognizer: TapGestureRecognizer() + ..onTap = () => home.setTab(UnityTab.addServer, context), ), - ], - ), + textAlign: TextAlign.center, + ), + ]), ); } } -class DesktopDeviceSelectorTile extends StatefulWidget { - const DesktopDeviceSelectorTile({ +class DeviceSelectorTile extends StatefulWidget { + const DeviceSelectorTile({ super.key, required this.device, required this.selected, @@ -268,13 +267,14 @@ class DesktopDeviceSelectorTile extends StatefulWidget { final bool selectable; @override - State createState() => - _DesktopDeviceSelectorTileState(); + State createState() => _DeviceSelectorTileState(); } -class _DesktopDeviceSelectorTileState extends State { - PointerDeviceKind? currentLongPressDeviceKind; +class _DeviceSelectorTileState extends State { + /// Used to check if the long press was caused by a touch input. + PointerDeviceKind? _currentLongPressDeviceKind; + /// Whether the user is hovering the tile. bool hovering = false; @override @@ -287,8 +287,9 @@ class _DesktopDeviceSelectorTileState extends State { onSecondaryTap: () => _displayOptions(context), // Only display options on long press if it's caused by a touch input + onLongPressDown: (details) => _currentLongPressDeviceKind = details.kind, onLongPressEnd: (details) { - switch (currentLongPressDeviceKind) { + switch (_currentLongPressDeviceKind) { case PointerDeviceKind.touch: _displayOptions(context); break; @@ -296,9 +297,8 @@ class _DesktopDeviceSelectorTileState extends State { break; } - currentLongPressDeviceKind = null; + _currentLongPressDeviceKind = null; }, - onLongPressDown: (details) => currentLongPressDeviceKind = details.kind, child: InkWell( onTap: !widget.device.status || !widget.selectable ? null diff --git a/lib/screens/layouts/desktop/viewport.dart b/lib/screens/layouts/desktop/viewport.dart new file mode 100644 index 00000000..f3e57110 --- /dev/null +++ b/lib/screens/layouts/desktop/viewport.dart @@ -0,0 +1,383 @@ +/* + * 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/device.dart'; +import 'package:bluecherry_client/providers/desktop_view_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/desktop/stream_data.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/video_player.dart'; +import 'package:bluecherry_client/utils/window.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/ptz.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'; +import 'package:unity_video_player/unity_video_player.dart'; + +class DesktopDeviceTile extends StatefulWidget { + const DesktopDeviceTile({super.key, required this.device}); + + final Device device; + + @override + State createState() => _DesktopDeviceTileState(); +} + +class _DesktopDeviceTileState extends State { + late UnityVideoFit fit = widget.device.server.additionalSettings.videoFit ?? + SettingsProvider.instance.kVideoFit.value; + + @override + Widget build(BuildContext context) { + // watch for changes in the players list. usually happens when reloading + // or releasing a device + context.watch(); + final videoPlayer = UnityPlayers.players[widget.device.uuid]; + + if (videoPlayer == null) { + return Card( + clipBehavior: Clip.hardEdge, + child: DesktopTileViewport( + controller: null, + device: widget.device, + onFitChanged: (fit) => setState(() => this.fit = fit), + ), + ); + } + + return UnityVideoView( + key: ValueKey(widget.device.fullName), + heroTag: widget.device.streamURL, + player: videoPlayer, + fit: fit, + paneBuilder: (context, controller) { + return DesktopTileViewport( + controller: controller, + device: widget.device, + onFitChanged: (fit) => setState(() => this.fit = fit), + ); + }, + ); + } +} + +class DesktopTileViewport extends StatefulWidget { + final UnityVideoPlayer? controller; + final Device device; + final ValueChanged onFitChanged; + + const DesktopTileViewport({ + super.key, + required this.controller, + required this.device, + required this.onFitChanged, + }); + + @override + State createState() => _DesktopTileViewportState(); +} + +class _DesktopTileViewportState extends State { + bool ptzEnabled = false; + late double? volume = widget.controller?.volume; + + void updateVolume() { + if (widget.controller != null && mounted) { + setState(() => volume = widget.controller!.volume); + } + } + + @override + void initState() { + super.initState(); + if (widget.controller != null) { + updateVolume(); + } + } + + @override + void didUpdateWidget(covariant DesktopTileViewport oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.controller == null && widget.controller != null) { + updateVolume(); + } + } + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + final theme = Theme.of(context); + final view = context.watch(); + final settings = context.watch(); + final video = UnityVideoView.maybeOf(context); + final isSubView = AlternativeWindow.maybeOf(context) != null; + final isMuted = volume == 0.0; + + Widget foreground = PTZController( + enabled: ptzEnabled, + device: widget.device, + builder: (context, commands, constraints) { + final states = HoverButton.of(context).states; + + final fit = + context.findAncestorWidgetOfExactType()?.fit ?? + widget.device.server.additionalSettings.videoFit ?? + settings.kVideoFit.value; + + final reloadButton = SquaredIconButton( + icon: Icon( + Icons.replay_outlined, + shadows: outlinedIcon(), + color: Colors.white, + size: 16.0, + ), + tooltip: loc.reloadCamera, + onPressed: () async { + await UnityPlayers.reloadDevice(widget.device); + setState(() {}); + }, + ); + + return Stack(children: [ + Positioned.fill(child: MulticastViewport(device: widget.device)), + if (video?.error != null) + Positioned.fill(child: ErrorWarning(message: video!.error!)), + IgnorePointer( + child: Padding( + padding: const EdgeInsetsDirectional.symmetric( + horizontal: 12.0, + vertical: 8.0, + ), + child: RichText( + text: TextSpan( + text: widget.device.name, + style: theme.textTheme.labelLarge?.copyWith( + color: Colors.white, + shadows: outlinedText(), + ), + children: [ + if (states.isHovering) + TextSpan( + text: '\n' + '${widget.device.externalData?.rackName ?? widget.device.server.name}', + style: theme.textTheme.labelSmall?.copyWith( + color: Colors.white, + shadows: outlinedText(), + ), + ), + if (states.isHovering && settings.kShowDebugInfo.value) + TextSpan( + text: '\nsource: ${video?.player.dataSource}' + '\nposition: ${video?.player.currentPos}' + '\nduration ${video?.player.duration}', + style: theme.textTheme.labelSmall?.copyWith( + color: Colors.white, + shadows: outlinedText(), + ), + ), + ], + ), + ), + ), + ), + PositionedDirectional( + end: 16.0, + top: 50.0, + child: PTZData(commands: commands), + ), + if (video != null) ...[ + PositionedDirectional( + end: 0, + start: 0, + bottom: 4.0, + child: Row(crossAxisAlignment: CrossAxisAlignment.end, children: [ + if (states.isHovering && + video.error == null && + !video.isLoading) ...[ + const SizedBox(width: 12.0), + if (widget.device.hasPTZ) + PTZToggleButton( + ptzEnabled: ptzEnabled, + onChanged: (enabled) => + setState(() => ptzEnabled = enabled), + ), + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + reverse: true, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (!video.isLoading) + SquaredIconButton( + icon: Icon( + isMuted + ? Icons.volume_mute_rounded + : Icons.volume_up_rounded, + shadows: outlinedIcon(), + color: Colors.white, + size: 16.0, + ), + tooltip: + isMuted ? loc.enableAudio : loc.disableAudio, + onPressed: () async { + if (isMuted) { + await widget.controller!.setVolume(1.0); + } else { + await widget.controller!.setVolume(0.0); + } + + updateVolume(); + }, + ), + if (isDesktopPlatform && + !isSubView && + !video.isLoading) + SquaredIconButton( + icon: Icon( + Icons.open_in_new_sharp, + shadows: outlinedIcon(), + color: Colors.white, + size: 16.0, + ), + tooltip: loc.openInANewWindow, + onPressed: widget.device.openInANewWindow, + ), + if (!isSubView && !video.isLoading) + SquaredIconButton( + icon: Icon( + Icons.fullscreen_rounded, + shadows: outlinedIcon(), + color: Colors.white, + size: 16.0, + ), + tooltip: loc.showFullscreenCamera, + onPressed: () async { + UnityPlayers.openFullscreen( + context, + widget.device, + ptzEnabled: ptzEnabled, + ); + }, + ), + reloadButton, + CameraViewFitButton( + fit: fit, + onChanged: widget.onFitChanged, + ), + ], + ), + ), + ), + ] else ...[ + const Spacer(), + if (states.isHovering) reloadButton, + ], + Padding( + padding: const EdgeInsetsDirectional.only( + start: 6.0, + end: 6.0, + bottom: 6.0, + ), + child: VideoStatusLabel(video: video, device: widget.device), + ), + ]), + ), + if (!isSubView && + view.currentLayout.devices.contains(widget.device)) + PositionedDirectional( + top: 4.0, + end: 4.0, + child: AnimatedOpacity( + opacity: !states.isHovering ? 0 : 1, + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + child: + Row(mainAxisAlignment: MainAxisAlignment.end, children: [ + SquaredIconButton( + icon: Icon( + moreIconData, + shadows: outlinedIcon(), + color: Colors.white, + ), + tooltip: loc.more, + onPressed: () async { + final device = await showStreamDataDialog( + context, + device: widget.device, + ptzEnabled: ptzEnabled, + onPTZEnabledChanged: (enabled) => setState(() { + ptzEnabled = enabled; + }), + fit: fit, + onFitChanged: widget.onFitChanged, + ); + if (device != null && mounted) { + view.updateDevice( + widget.device, + device, + reload: device.url != widget.device.url || + device.preferredStreamingType != + widget.device.preferredStreamingType, + ); + updateVolume(); + } + }, + ), + SquaredIconButton( + icon: Icon( + Icons.close_outlined, + color: theme.colorScheme.error, + size: 18.0, + ), + tooltip: loc.removeCamera, + onPressed: () => view.remove(widget.device), + ), + ]), + ), + ), + ], + ]); + }, + ); + + return TooltipTheme( + data: TooltipTheme.of(context).copyWith( + preferBelow: false, + verticalOffset: 20.0, + decoration: BoxDecoration( + color: theme.brightness == Brightness.light + ? Colors.black + : Colors.white, + borderRadius: isMobile + ? BorderRadius.circular(16.0) + : BorderRadius.circular(6.0), + ), + ), + child: foreground, + ); + } +} diff --git a/lib/screens/layouts/device_grid.dart b/lib/screens/layouts/device_grid.dart index e1ec73ee..622cf914 100644 --- a/lib/screens/layouts/device_grid.dart +++ b/lib/screens/layouts/device_grid.dart @@ -32,11 +32,8 @@ 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/desktop/viewport.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'; @@ -46,10 +43,7 @@ import 'package:bluecherry_client/utils/video_player.dart'; import 'package:bluecherry_client/utils/window.dart'; import 'package:bluecherry_client/widgets/collapsable_sidebar.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/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'; @@ -58,10 +52,9 @@ import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; import 'package:sliver_tools/sliver_tools.dart'; -import 'package:unity_video_player/unity_video_player.dart'; -part 'desktop/desktop_device_grid.dart'; -part 'desktop/desktop_sidebar.dart'; +part 'desktop/layout_view.dart'; +part 'desktop/sidebar.dart'; part 'mobile/mobile_device_grid.dart'; const double kMobileBottomBarHeight = 48.0; @@ -79,9 +72,9 @@ class DeviceGrid extends StatelessWidget { final width = consts.biggest.width; if (hasDrawer || width < kMobileBreakpoint.width) { - return const MobileDeviceGrid(); + return const SmallDeviceGrid(); } else { - return DesktopDeviceGrid(width: width); + return LargeDeviceGrid(width: width); } }), ); diff --git a/lib/screens/layouts/mobile/device_view.dart b/lib/screens/layouts/mobile/device_view.dart index 1baa2de8..ddb30194 100644 --- a/lib/screens/layouts/mobile/device_view.dart +++ b/lib/screens/layouts/mobile/device_view.dart @@ -37,7 +37,7 @@ import 'package:unity_video_player/unity_video_player.dart'; /// /// See also: /// -/// * [MobileDeviceGrid], used to render mobile views +/// * [SmallDeviceGrid], used to render mobile views /// * [DeviceTile], used to render the tile class MobileDeviceView extends StatefulWidget { /// Which tab is selected on the mobile device grid diff --git a/lib/screens/layouts/mobile/mobile_device_grid.dart b/lib/screens/layouts/mobile/mobile_device_grid.dart index 2923f5f2..6d5b6147 100644 --- a/lib/screens/layouts/mobile/mobile_device_grid.dart +++ b/lib/screens/layouts/mobile/mobile_device_grid.dart @@ -19,14 +19,14 @@ part of '../device_grid.dart'; -class MobileDeviceGrid extends StatefulWidget { - const MobileDeviceGrid({super.key}); +class SmallDeviceGrid extends StatefulWidget { + const SmallDeviceGrid({super.key}); @override - State createState() => _MobileDeviceGridState(); + State createState() => _SmallDeviceGridState(); } -class _MobileDeviceGridState extends State { +class _SmallDeviceGridState extends State { Timer? timer; @override @@ -79,7 +79,7 @@ class _MobileDeviceGridState extends State { Expanded( child: PageTransitionSwitcher( child: view.devices.keys - .map((key) => _MobileDeviceGridChild(tab: key)) + .map((key) => _SmallDeviceGridChild(tab: key)) .elementAt( view.devices.keys .toList() @@ -160,10 +160,10 @@ class _MobileDeviceGridState extends State { } } -class _MobileDeviceGridChild extends StatelessWidget { +class _SmallDeviceGridChild extends StatelessWidget { final int tab; - const _MobileDeviceGridChild({required this.tab}); + const _SmallDeviceGridChild({required this.tab}); @override Widget build(BuildContext context) { diff --git a/lib/screens/layouts/video_status_label.dart b/lib/screens/layouts/video_status_label.dart index ffc43fb3..bfa4a6e4 100644 --- a/lib/screens/layouts/video_status_label.dart +++ b/lib/screens/layouts/video_status_label.dart @@ -103,11 +103,11 @@ class _VideoStatusLabelState extends State { ); final minHeight = label.buildTextSpans(context).length * 15; - final willLeftOverflow = + final willRightOverflow = position.dx + _DeviceVideoInfo.minWidth > constraints.maxWidth; - final left = willLeftOverflow - ? (constraints.maxWidth - _DeviceVideoInfo.minWidth - 8.0) + final left = willRightOverflow + ? (position.dx - _DeviceVideoInfo.minWidth - 16.0) : position.dx; final top = position.dy > minHeight + 8.0 ? null diff --git a/lib/screens/multi_window/single_camera_window.dart b/lib/screens/multi_window/single_camera_window.dart index d0eaa362..b0489c62 100644 --- a/lib/screens/multi_window/single_camera_window.dart +++ b/lib/screens/multi_window/single_camera_window.dart @@ -21,7 +21,7 @@ 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/screens/layouts/desktop/viewport.dart'; import 'package:bluecherry_client/widgets/desktop_buttons.dart'; import 'package:flutter/material.dart'; import 'package:unity_video_player/unity_video_player.dart'; diff --git a/lib/screens/players/live_player.dart b/lib/screens/players/live_player.dart index 4561f125..43ffa870 100644 --- a/lib/screens/players/live_player.dart +++ b/lib/screens/players/live_player.dart @@ -361,7 +361,10 @@ class __DesktopLivePlayerState extends State<_DesktopLivePlayer> { Positioned.fill( child: Center( child: AspectRatio( - aspectRatio: player.aspectRatio, + aspectRatio: player.aspectRatio == 0 || + player.aspectRatio == double.infinity + ? 16 / 9 + : player.aspectRatio, child: MulticastViewport( device: widget.device, ), diff --git a/lib/screens/servers/additional_server_settings.dart b/lib/screens/servers/additional_server_settings.dart index d07c22f6..a08c34cd 100644 --- a/lib/screens/servers/additional_server_settings.dart +++ b/lib/screens/servers/additional_server_settings.dart @@ -51,7 +51,9 @@ class AdditionalServerSettings extends StatefulWidget { } class _AdditionalServerSettingsState extends State { - bool connectAutomaticallyAtStartup = true; + late bool connectAutomaticallyAtStartup = + widget.server?.additionalSettings.connectAutomaticallyAtStartup ?? + SettingsProvider.instance.kConnectAutomaticallyAtStartup.value; late StreamingType? streamingType = widget.server?.additionalSettings.preferredStreamingType; late RTSPProtocol? rtspProtocol = diff --git a/lib/screens/servers/finish.dart b/lib/screens/servers/finish.dart index 1bb09e41..e5bc58d2 100644 --- a/lib/screens/servers/finish.dart +++ b/lib/screens/servers/finish.dart @@ -134,7 +134,7 @@ class LetsGoScreen extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: server!.devices.sorted().map((device) { - return DesktopDeviceSelectorTile( + return DeviceSelectorTile( device: device, selected: false, selectable: false, @@ -179,7 +179,7 @@ class LetsGoScreen extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: server!.devices.sorted().map((device) { - return DesktopDeviceSelectorTile( + return DeviceSelectorTile( device: device, selected: false, selectable: false, diff --git a/lib/screens/settings/general.dart b/lib/screens/settings/general.dart index f9e919fc..11f64911 100644 --- a/lib/screens/settings/general.dart +++ b/lib/screens/settings/general.dart @@ -147,11 +147,27 @@ class GeneralSettings extends StatelessWidget { settings.kNotificationClickBehavior.value = v; }, ), + SubHeader( + loc.dataUsage, + padding: DesktopSettings.horizontalPadding, + ), + OptionsChooserTile( + icon: Icons.cloud_done, + title: loc.streamsOnBackground, + description: loc.streamsOnBackgroundDescription, + value: settings.kStreamOnBackground.value, + values: NetworkUsage.values.map((value) { + return Option( + value: value, + // icon: value.icon, + text: value.locale(context), + ); + }), + onChanged: (value) { + settings.kStreamOnBackground.value = value; + }, + ), if (settings.kShowDebugInfo.value) ...[ - const SubHeader( - 'Data Usage', - padding: DesktopSettings.horizontalPadding, - ), OptionsChooserTile( icon: Icons.data_usage, title: 'Automatic streaming', @@ -164,19 +180,6 @@ class GeneralSettings extends StatelessWidget { ], onChanged: (value) {}, ), - OptionsChooserTile( - icon: Icons.cloud_done, - title: 'Keep streams playing on background', - description: - 'When to keep streams playing when the app is in background', - value: '', - values: const [ - Option(value: '', icon: Icons.insights, text: 'Auto'), - Option(value: '', icon: Icons.wifi, text: 'Wifi only'), - Option(value: '', icon: Icons.not_interested, text: 'Never'), - ], - onChanged: (value) {}, - ), ListTile( leading: CircleAvatar( backgroundColor: Colors.transparent, diff --git a/lib/screens/settings/server_and_devices.dart b/lib/screens/settings/server_and_devices.dart index f4584ed0..0cfd1174 100644 --- a/lib/screens/settings/server_and_devices.dart +++ b/lib/screens/settings/server_and_devices.dart @@ -30,24 +30,74 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; import 'package:unity_video_player/unity_video_player.dart'; -class ServerSettings extends StatelessWidget { - const ServerSettings({super.key}); +class ServerAndDevicesSettings extends StatelessWidget { + const ServerAndDevicesSettings({super.key}); @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context); return ListView(children: [ - SubHeader(loc.servers), + SubHeader(loc.servers, padding: DesktopSettings.horizontalPadding), const ServersList(), const SizedBox(height: 8.0), - SubHeader(loc.streamingSettings), + SubHeader(loc.serverSettings, padding: DesktopSettings.horizontalPadding), + const ServerSettings(), const SizedBox(height: 8.0), + SubHeader( + loc.streamingSettings, + padding: DesktopSettings.horizontalPadding, + ), const StreamingSettings(), const SizedBox(height: 12.0), ]); } } +class ServerSettings extends StatelessWidget { + const ServerSettings({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final settings = context.watch(); + final loc = AppLocalizations.of(context); + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + CheckboxListTile.adaptive( + title: Text(loc.connectToServerAutomaticallyAtStartup), + subtitle: Text(loc.connectToServerAutomaticallyAtStartupDescription), + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.connect_without_contact), + ), + contentPadding: DesktopSettings.horizontalPadding, + value: settings.kConnectAutomaticallyAtStartup.value, + onChanged: (v) { + if (v != null) { + settings.kConnectAutomaticallyAtStartup.value = v; + } + }, + ), + CheckboxListTile.adaptive( + title: Text(loc.allowUntrustedCertificates), + subtitle: Text(loc.allowUntrustedCertificatesDescription), + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.approval), + ), + contentPadding: DesktopSettings.horizontalPadding, + value: settings.kAllowUntrustedCertificates.value, + onChanged: (v) { + if (v != null) { + settings.kAllowUntrustedCertificates.value = v; + } + }, + ), + ]); + } +} + class StreamingSettings extends StatelessWidget { const StreamingSettings({super.key}); @@ -60,7 +110,7 @@ class StreamingSettings extends StatelessWidget { return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ OptionsChooserTile( title: loc.streamingType, - icon: Icons.sensors, + icon: Icons.live_tv, value: settings.kStreamingType.value, values: StreamingType.values.map((value) { return Option( @@ -77,7 +127,7 @@ class StreamingSettings extends StatelessWidget { const SizedBox(height: 8.0), OptionsChooserTile( title: loc.rtspProtocol, - icon: Icons.sensors, + icon: Icons.settings_input_antenna, value: settings.kRTSPProtocol.value, values: RTSPProtocol.values.map((value) { return Option( diff --git a/lib/screens/settings/settings_desktop.dart b/lib/screens/settings/settings_desktop.dart index 0d8d2ed0..19af8efa 100644 --- a/lib/screens/settings/settings_desktop.dart +++ b/lib/screens/settings/settings_desktop.dart @@ -58,29 +58,29 @@ class _DesktopSettingsState extends State { icon: const Icon(Icons.dashboard), label: Text(loc.general), ), - const NavigationRailDestination( - icon: Icon(Icons.dns), - label: Text('Servers and Devices'), + NavigationRailDestination( + icon: const Icon(Icons.dns), + label: Text(loc.serverAndDevices), ), - const NavigationRailDestination( - icon: Icon(Icons.event), - label: Text('Events and Downloads'), + NavigationRailDestination( + icon: const Icon(Icons.event), + label: Text(loc.eventsAndDownloads), ), - const NavigationRailDestination( - icon: Icon(Icons.style), - label: Text('Application'), + NavigationRailDestination( + icon: const Icon(Icons.style), + label: Text(loc.application), ), - const NavigationRailDestination( - icon: Icon(Icons.security), - label: Text('Privacy and Security'), + NavigationRailDestination( + icon: const Icon(Icons.security), + label: Text(loc.privacyAndSecurity), ), - const NavigationRailDestination( - icon: Icon(Icons.update), - label: Text('Updates and Help'), + NavigationRailDestination( + icon: const Icon(Icons.update), + label: Text(loc.updatesAndHelp), ), - const NavigationRailDestination( - icon: Icon(Icons.code), - label: Text('Advanced Options'), + NavigationRailDestination( + icon: const Icon(Icons.code), + label: Text(loc.advancedOptions), ), ], selectedIndex: currentIndex, @@ -110,7 +110,7 @@ class _DesktopSettingsState extends State { duration: kThemeChangeDuration, child: switch (currentIndex) { 0 => const GeneralSettings(), - 1 => const ServerSettings(), + 1 => const ServerAndDevicesSettings(), 2 => const EventsAndDownloadsSettings(), 3 => const ApplicationSettings(), 4 => const PrivacySecuritySettings(), diff --git a/lib/screens/settings/settings_mobile.dart b/lib/screens/settings/settings_mobile.dart index 40e970e3..e87b447f 100644 --- a/lib/screens/settings/settings_mobile.dart +++ b/lib/screens/settings/settings_mobile.dart @@ -84,8 +84,7 @@ class _MobileSettingsState extends State { ListTile( leading: const Icon(Icons.dashboard), title: Text(loc.general), - subtitle: - const Text('Notifications, Data Usage, Wakelock, etc'), + subtitle: Text(loc.generalSettingsSuggestion), trailing: const Icon(Icons.chevron_right), onTap: () { showModalBottomSheet( @@ -100,8 +99,8 @@ class _MobileSettingsState extends State { ), ListTile( leading: const Icon(Icons.dns), - title: const Text('Servers and Devices'), - subtitle: const Text('Servers, Devices, Streaming, etc'), + title: Text(loc.serverAndDevices), + subtitle: Text(loc.serverAndDevicesSettingsSuggestion), trailing: const Icon(Icons.chevron_right), onTap: () { showModalBottomSheet( @@ -109,15 +108,15 @@ class _MobileSettingsState extends State { showDragHandle: true, scrollControlDisabledMaxHeightRatio: 0.9, builder: (context) { - return const ServerSettings(); + return const ServerAndDevicesSettings(); }, ); }, ), ListTile( leading: const Icon(Icons.event), - title: const Text('Events and Downloads'), - subtitle: const Text('Downloads, Events, Timeline, etc'), + title: Text(loc.eventsAndDownloads), + subtitle: Text(loc.eventsAndDownloadsSettingsSuggestion), trailing: const Icon(Icons.chevron_right), onTap: () { showModalBottomSheet( @@ -132,9 +131,8 @@ class _MobileSettingsState extends State { ), ListTile( leading: const Icon(Icons.style), - title: const Text('Application'), - subtitle: const Text( - 'Theme, Language, Date and Time, Window, etc'), + title: Text(loc.application), + subtitle: Text(loc.applicationSettingsSuggestion), trailing: const Icon(Icons.chevron_right), onTap: () { showModalBottomSheet( @@ -149,8 +147,8 @@ class _MobileSettingsState extends State { ), ListTile( leading: const Icon(Icons.security), - title: const Text('Privacy and Security'), - subtitle: const Text('Diagnostics, Privacy, Security, etc'), + title: Text(loc.privacyAndSecurity), + subtitle: Text(loc.privacyAndSecuritySettingsSuggestion), trailing: const Icon(Icons.chevron_right), onTap: () { showModalBottomSheet( @@ -165,8 +163,8 @@ class _MobileSettingsState extends State { ), ListTile( leading: const Icon(Icons.update), - title: const Text('Updates and Help'), - subtitle: const Text('Check for updates, Help, About, etc'), + title: Text(loc.updatesAndHelp), + subtitle: Text(loc.updatesAndHelpSettingsSuggestion), trailing: const Icon(Icons.chevron_right), onTap: () { showModalBottomSheet( @@ -181,9 +179,8 @@ class _MobileSettingsState extends State { ), ListTile( leading: const Icon(Icons.code), - title: const Text('Advanced Options'), - subtitle: - const Text('Beta Features, Developer Options, etc'), + title: Text(loc.advancedOptions), + subtitle: Text(loc.advancedOptionsSettingsSuggestion), trailing: const Icon(Icons.chevron_right), onTap: () { showModalBottomSheet( diff --git a/lib/screens/settings/shared/server_tile.dart b/lib/screens/settings/shared/server_tile.dart index d02c27ee..2474f6c2 100644 --- a/lib/screens/settings/shared/server_tile.dart +++ b/lib/screens/settings/shared/server_tile.dart @@ -203,7 +203,9 @@ class ServerTile extends StatelessWidget { !isLoading ? [ if (server.name != server.ip) server.ip, - if (server.online) + if (!server.passedCertificates) + loc.certificateNotPassed + else if (server.online) loc.nDevices(server.devices.length) else loc.offline, @@ -261,18 +263,20 @@ class ServerCard extends StatelessWidget { return GestureDetector( onSecondaryTap: showMenu, - child: SizedBox( - height: 180, - width: 180, + child: ConstrainedBox( + constraints: const BoxConstraints( + minHeight: 180.0, + maxHeight: 200.0, + minWidth: 180.0, + maxWidth: 200.0, + ), child: Card( - child: Stack(children: [ - Positioned.fill( - bottom: 8.0, - left: 8.0, - right: 8.0, - top: 8.0, + child: Stack(alignment: AlignmentDirectional.center, children: [ + Padding( + padding: const EdgeInsetsDirectional.all(8.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ CircleAvatar( backgroundColor: Colors.transparent, @@ -295,14 +299,19 @@ class ServerCard extends StatelessWidget { style: theme.textTheme.bodySmall, ), Text( - !server.online - ? loc.offline - : !isLoading - ? loc.nDevices(server.devices.length) - : '', + !server.passedCertificates + ? loc.certificateNotPassed + : !server.online + ? loc.offline + : !isLoading + ? loc.nDevices(server.devices.length) + : '', style: TextStyle( - color: !server.online ? theme.colorScheme.error : null, + color: !server.online || !server.passedCertificates + ? theme.colorScheme.error + : null, ), + textAlign: TextAlign.center, ), const SizedBox(height: 12.0), Transform.scale( @@ -445,7 +454,7 @@ class DevicesListDialog extends StatelessWidget { shrinkWrap: true, itemBuilder: (context, index) { final device = server.devices[index]; - return DesktopDeviceSelectorTile( + return DeviceSelectorTile( device: device, selected: false, selectable: false, diff --git a/lib/screens/settings/updates_and_help.dart b/lib/screens/settings/updates_and_help.dart index 82cf762a..dc99d80c 100644 --- a/lib/screens/settings/updates_and_help.dart +++ b/lib/screens/settings/updates_and_help.dart @@ -105,10 +105,8 @@ class UpdatesSettings extends StatelessWidget { foregroundColor: theme.iconTheme.color, child: const Icon(Icons.memory), ), - title: const Text('Show release notes'), - subtitle: const Text( - 'Display release notes when a new version is downloaded', - ), + title: Text(loc.showReleaseNotes), + subtitle: Text(loc.showReleaseNotesDescription), contentPadding: DesktopSettings.horizontalPadding, value: true, onChanged: (v) {}, @@ -459,7 +457,7 @@ class About extends StatelessWidget { return TextButton( onPressed: open, child: Text( - 'Help', + loc.help, semanticsLabel: 'www.bluecherrydvr.com/contact', style: TextStyle( color: theme.colorScheme.primary, @@ -482,7 +480,7 @@ class About extends StatelessWidget { ); }, child: Text( - 'Licenses', + loc.licenses, style: TextStyle( color: theme.colorScheme.primary, ), diff --git a/lib/utils/video_player.dart b/lib/utils/video_player.dart index 0b0593db..8404ebf0 100644 --- a/lib/utils/video_player.dart +++ b/lib/utils/video_player.dart @@ -194,6 +194,23 @@ class UnityPlayers with ChangeNotifier { if (isLocalController) await player.dispose(); } + static Future playAll() async { + for (final player in players.values) { + if (!player.isPlaying) { + player.seekTo(player.duration); + await player.start(); + } + } + return Future.value(); + } + + static Future pauseAll() { + for (final player in players.values) { + if (player.isPlaying) player.pause(); + } + return Future.value(); + } + @override void dispose() { _reloadTimer?.cancel(); diff --git a/lib/widgets/device_selector.dart b/lib/widgets/device_selector.dart index 7caa552f..558d1b61 100644 --- a/lib/widgets/device_selector.dart +++ b/lib/widgets/device_selector.dart @@ -109,11 +109,15 @@ class DeviceSelector extends StatelessWidget { child: SubHeader( server.name, materialType: MaterialType.canvas, - subtext: server.online - ? loc.nDevices(server.devices.length) - : loc.offline, + subtext: !server.passedCertificates + ? loc.certificateNotPassed + : server.online + ? loc.nDevices(server.devices.length) + : loc.offline, subtextStyle: TextStyle( - color: !server.online ? theme.colorScheme.error : null, + color: !server.online || !server.passedCertificates + ? theme.colorScheme.error + : null, ), trailing: servers.isServerLoading(server) ? const SizedBox(