diff --git a/.metadata b/.metadata index e27f5cc..656c923 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled. version: - revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + revision: f72efea43c3013323d1b95cff571f3c1caa37583 channel: stable project_type: app @@ -13,26 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - - platform: android - create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - - platform: ios - create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + create_revision: f72efea43c3013323d1b95cff571f3c1caa37583 + base_revision: f72efea43c3013323d1b95cff571f3c1caa37583 - platform: linux - create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - - platform: macos - create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - - platform: web - create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - - platform: windows - create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + create_revision: f72efea43c3013323d1b95cff571f3c1caa37583 + base_revision: f72efea43c3013323d1b95cff571f3c1caa37583 # User provided section diff --git a/assets/icon/icon-circle.ico b/assets/icon/icon-circle.ico new file mode 100644 index 0000000..e4da1bd Binary files /dev/null and b/assets/icon/icon-circle.ico differ diff --git a/assets/icon/icon-circle.png b/assets/icon/icon-circle.png new file mode 100644 index 0000000..5b39396 Binary files /dev/null and b/assets/icon/icon-circle.png differ diff --git a/assets/icon/icon-macos.png b/assets/icon/icon-macos.png new file mode 100755 index 0000000..c201d70 Binary files /dev/null and b/assets/icon/icon-macos.png differ diff --git a/debian/compile_deb.txt b/debian/compile_deb.txt new file mode 100644 index 0000000..4a18706 --- /dev/null +++ b/debian/compile_deb.txt @@ -0,0 +1 @@ +https://pub.dev/packages/flutter_to_debian \ No newline at end of file diff --git a/debian/debian.yaml b/debian/debian.yaml new file mode 100644 index 0000000..ddc864c --- /dev/null +++ b/debian/debian.yaml @@ -0,0 +1,14 @@ +flutter_app: + command: adguard_home_manager + arch: x64 + parent: /usr/local/lib + +control: + Package: AdGuardHomeManager + Version: 2.0.0 + Architecture: amd64 + Essential: no + Priority: optional + Depends: + Maintainer: JGeek00 + Description: AdGuard Home control app \ No newline at end of file diff --git a/debian/gui/adguard-home-manager.desktop b/debian/gui/adguard-home-manager.desktop new file mode 100644 index 0000000..a011591 --- /dev/null +++ b/debian/gui/adguard-home-manager.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Name=AdGuard Home Manager +Comment=Manage your AdGuard Home server +Exec=adguard-home-manager +Icon=${SNAP}/meta/gui/adguard-home-manager.png +Terminal=false +Type=Application +Categories=Utilities; diff --git a/debian/gui/adguard-home-manager.png b/debian/gui/adguard-home-manager.png new file mode 100644 index 0000000..5b39396 Binary files /dev/null and b/debian/gui/adguard-home-manager.png differ diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 1ffa568..7d212ec 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -365,7 +365,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.adguardHomeManager; + PRODUCT_BUNDLE_IDENTIFIER = com.jgeek00.adguard_home_manager; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -494,7 +494,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.adguardHomeManager; + PRODUCT_BUNDLE_IDENTIFIER = com.jgeek00.adguard_home_manager; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -517,7 +517,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.adguardHomeManager; + PRODUCT_BUNDLE_IDENTIFIER = com.jgeek00.adguard_home_manager; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/lib/base.dart b/lib/base.dart index 327b6ca..c33fce3 100644 --- a/lib/base.dart +++ b/lib/base.dart @@ -1,6 +1,7 @@ // ignore_for_file: use_build_context_synchronously, depend_on_referenced_packages import 'dart:async'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:animations/animations.dart'; @@ -10,7 +11,9 @@ import 'package:store_checker/store_checker.dart'; import 'package:flutter/services.dart'; import 'package:adguard_home_manager/widgets/bottom_nav_bar.dart'; +import 'package:adguard_home_manager/widgets/menu_bar.dart'; import 'package:adguard_home_manager/widgets/update_modal.dart'; +import 'package:adguard_home_manager/widgets/navigation_rail.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; import 'package:adguard_home_manager/models/github_release.dart'; @@ -53,11 +56,23 @@ class _BaseState extends State with WidgetsBindingObserver { } Future checkInstallationSource() async { - Source installationSource = await StoreChecker.getSource; - if (installationSource != Source.IS_INSTALLED_FROM_PLAY_STORE) { - final result = await checkAppUpdatesGitHub(); - if (result['result'] == 'success') { - if (updateExists(widget.appConfigProvider.getAppInfo!.version, result['body'].tagName)) { + final result = await checkAppUpdatesGitHub(); + if (result['result'] == 'success') { + final update = updateExists(widget.appConfigProvider.getAppInfo!.version, result['body'].tagName); + if (update == true) { + if (Platform.isAndroid) { + Source installationSource = await StoreChecker.getSource; + if (installationSource == Source.IS_INSTALLED_FROM_PLAY_STORE) { + return null; + } + else { + return result['body']; + } + } + else if (Platform.isIOS) { + return null; + } + else { return result['body']; } } @@ -107,38 +122,51 @@ class _BaseState extends State with WidgetsBindingObserver { final serversProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + List screens = serversProvider.selectedServer != null ? screensServerConnected : screensSelectServer; - return AnnotatedRegion( - value: SystemUiOverlayStyle( - statusBarColor: Colors.transparent, - statusBarBrightness: Theme.of(context).brightness == Brightness.light - ? Brightness.light - : Brightness.dark, - statusBarIconBrightness: Theme.of(context).brightness == Brightness.light - ? Brightness.dark - : Brightness.light, - systemNavigationBarColor: Theme.of(context).scaffoldBackgroundColor, - systemNavigationBarIconBrightness: Theme.of(context).brightness == Brightness.light - ? Brightness.dark - : Brightness.light, - ), - child: Scaffold( - body: PageTransitionSwitcher( - duration: const Duration(milliseconds: 200), - transitionBuilder: ( - (child, primaryAnimation, secondaryAnimation) => FadeThroughTransition( - animation: primaryAnimation, - secondaryAnimation: secondaryAnimation, - child: child, - ) - ), - child: screens[appConfigProvider.selectedScreen].body, + return CustomMenuBar( + child: AnnotatedRegion( + value: SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarBrightness: Theme.of(context).brightness == Brightness.light + ? Brightness.light + : Brightness.dark, + statusBarIconBrightness: Theme.of(context).brightness == Brightness.light + ? Brightness.dark + : Brightness.light, + systemNavigationBarColor: Theme.of(context).scaffoldBackgroundColor, + systemNavigationBarIconBrightness: Theme.of(context).brightness == Brightness.light + ? Brightness.dark + : Brightness.light, ), - bottomNavigationBar: const BottomNavBar(), - ) + child: Scaffold( + body: Row( + children: [ + if (width > 900) const SideNavigationRail(), + Expanded( + child: PageTransitionSwitcher( + duration: const Duration(milliseconds: 200), + transitionBuilder: ( + (child, primaryAnimation, secondaryAnimation) => FadeThroughTransition( + animation: primaryAnimation, + secondaryAnimation: secondaryAnimation, + child: child, + ) + ), + child: screens[appConfigProvider.selectedScreen].body, + ), + ), + ], + ), + bottomNavigationBar: width <= 900 + ? const BottomNavBar() + : null, + ) + ), ); } } \ No newline at end of file diff --git a/lib/functions/open_url.dart b/lib/functions/open_url.dart index b4d52d5..b435e47 100644 --- a/lib/functions/open_url.dart +++ b/lib/functions/open_url.dart @@ -1,17 +1,30 @@ -import 'package:flutter_web_browser/flutter_web_browser.dart'; +import 'dart:io'; -void openUrl(String url) { - FlutterWebBrowser.openWebPage( - url: url, - customTabsOptions: const CustomTabsOptions( - instantAppsEnabled: true, - showTitle: true, - urlBarHidingEnabled: false, - ), - safariVCOptions: const SafariViewControllerOptions( - barCollapsingEnabled: true, - dismissButtonStyle: SafariViewControllerDismissButtonStyle.close, - modalPresentationCapturesStatusBarAppearance: true, - ) - ); -} \ No newline at end of file +import 'package:flutter_web_browser/flutter_web_browser.dart'; +import 'package:url_launcher/url_launcher.dart'; + +void openUrl(String url) async { + if (Platform.isAndroid || Platform.isIOS) { + FlutterWebBrowser.openWebPage( + url: url, + customTabsOptions: const CustomTabsOptions( + instantAppsEnabled: true, + showTitle: true, + urlBarHidingEnabled: false, + ), + safariVCOptions: const SafariViewControllerOptions( + barCollapsingEnabled: true, + dismissButtonStyle: SafariViewControllerDismissButtonStyle.close, + modalPresentationCapturesStatusBarAppearance: true, + ) + ); + } + else { + final uri = Uri.parse(url); + if (await canLaunchUrl(uri)) { + await launchUrl(uri); + } else { + throw 'Could not launch $url'; + } + } +} \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 2b66b5e..3b087c6 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -45,10 +45,10 @@ "save": "Save", "serverStatus": "Server status", "connectionNotUpdated": "Connection not updated", - "ruleFilteringWidget": "Rule\nfiltering", - "safeBrowsingWidget": "Safe\nbrowsing", - "parentalFilteringWidget": "Parental\nfiltering", - "safeSearchWidget": "Safe\nsearch", + "ruleFilteringWidget": "Rule filtering", + "safeBrowsingWidget": "Safe browsing", + "parentalFilteringWidget": "Parental filtering", + "safeSearchWidget": "Safe search", "ruleFiltering": "Rule filtering", "safeBrowsing": "Safe browsing", "parentalFiltering": "Parental filtering", @@ -606,5 +606,11 @@ "remainingTime": "Remaining time", "safeSearchSettings": "Safe search settings", "loadingSafeSearchSettings": "Loading safe search settings...", - "safeSearchSettingsNotLoaded": "Error when loading safe search settings." + "safeSearchSettingsNotLoaded": "Error when loading safe search settings.", + "loadingLogsSettings": "Loading logs settings...", + "selectOptionLeftColumn": "Select an option of the left column", + "selectClientLeftColumn": "Select a client of the left column", + "disableList": "Disable list", + "enableList": "Enable list", + "screens": "Screens" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 76cff1b..90c396a 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -45,10 +45,10 @@ "save": "Guardar", "connectionNotUpdated": "Conexión no actualizada", "serverStatus": "Estado del servidor", - "ruleFilteringWidget": "Bloqueo por\nfiltros", - "safeBrowsingWidget": "Navegación\nsegura", - "parentalFilteringWidget": "Control\nparental", - "safeSearchWidget": "Búsqueda\nsegura", + "ruleFilteringWidget": "Bloqueo por filtros", + "safeBrowsingWidget": "Navegación segura", + "parentalFilteringWidget": "Control parental", + "safeSearchWidget": "Búsqueda segura", "ruleFiltering": "Bloqueo por filtros", "safeBrowsing": "Navegación segura", "parentalFiltering": "Control parental", @@ -606,5 +606,11 @@ "remainingTime": "Tiempo restante", "safeSearchSettings": "Configuración de búsqueda segura", "loadingSafeSearchSettings": "Cargando configuración de búsqueda segura...", - "safeSearchSettingsNotLoaded": "Error al cargar la configuración de búsqueda segura." + "safeSearchSettingsNotLoaded": "Error al cargar la configuración de búsqueda segura.", + "loadingLogsSettings": "Cargando configuración de registros...", + "selectOptionLeftColumn": "Selecciona una opción de la columna de la izquierda", + "selectClientLeftColumn": "Selecciona un cliente de la columna de la izquierda", + "disableList": "Deshabilitar lista", + "enableList": "Habilitar lista", + "screens": "Pantallas" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 5ef8610..6e8f4bf 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,25 +7,33 @@ import 'package:provider/provider.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; +import 'package:window_size/window_size.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/base.dart'; import 'package:adguard_home_manager/classes/http_override.dart'; -import 'package:adguard_home_manager/services/database.dart'; +import 'package:adguard_home_manager/services/db/database.dart'; import 'package:adguard_home_manager/constants/colors.dart'; import 'package:adguard_home_manager/providers/logs_provider.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; import 'package:adguard_home_manager/config/theme.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - SystemChrome.setPreferredOrientations( - [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown] - ); + + if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { + setWindowMinSize(const Size(500, 500)); + } + + if (Platform.isWindows || Platform.isLinux) { + sqfliteFfiInit(); + databaseFactory = databaseFactoryFfi; + } AppConfigProvider appConfigProvider = AppConfigProvider(); ServersProvider serversProvider = ServersProvider(); diff --git a/lib/providers/app_config_provider.dart b/lib/providers/app_config_provider.dart index 262a448..caa7dff 100644 --- a/lib/providers/app_config_provider.dart +++ b/lib/providers/app_config_provider.dart @@ -4,6 +4,7 @@ import 'package:flutter/scheduler.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:sqflite/sqlite_api.dart'; +import 'package:adguard_home_manager/services/db/queries.dart'; import 'package:adguard_home_manager/functions/conversions.dart'; import 'package:adguard_home_manager/models/app_log.dart'; @@ -16,6 +17,8 @@ class AppConfigProvider with ChangeNotifier { int _selectedScreen = 0; + int? _selectedSettingsScreen; + bool _showingSnackbar = false; int _selectedTheme = 0; @@ -118,6 +121,10 @@ class AppConfigProvider with ChangeNotifier { return _doNotRememberVersion; } + int? get selectedSettingsScreen { + return _selectedSettingsScreen; + } + void setDbInstance(Database db) { _dbInstance = db; } @@ -159,8 +166,19 @@ class AppConfigProvider with ChangeNotifier { notifyListeners(); } + void setSelectedSettingsScreen({required int? screen, bool? notify}) { + _selectedSettingsScreen = screen; + if (notify == true) { + notifyListeners(); + } + } + Future setOverrideSslCheck(bool status) async { - final updated = await _updateOverrideSslCheck(status == true ? 1 : 0); + final updated = await updateConfigQuery( + db: _dbInstance!, + column: 'overrideSslCheck', + value: status == true ? 1 : 0 + ); if (updated == true) { _overrideSslCheck = status == true ? 1 : 0; notifyListeners(); @@ -172,7 +190,11 @@ class AppConfigProvider with ChangeNotifier { } Future setHideZeroValues(bool status) async { - final updated = await _updateSetHideZeroValues(status == true ? 1 : 0); + final updated = await updateConfigQuery( + db: _dbInstance!, + column: 'overrideSslCheck', + value: status == true ? 1 : 0 + ); if (updated == true) { _hideZeroValues = status == true ? 1 : 0; notifyListeners(); @@ -184,7 +206,11 @@ class AppConfigProvider with ChangeNotifier { } Future setShowNameTimeLogs(bool status) async { - final updated = await _updateShowNameTimeLogsDb(status == true ? 1 : 0); + final updated = await updateConfigQuery( + db: _dbInstance!, + column: 'showNameTimeLogs', + value: status == true ? 1 : 0 + ); if (updated == true) { _showNameTimeLogs = status == true ? 1 : 0; notifyListeners(); @@ -196,7 +222,11 @@ class AppConfigProvider with ChangeNotifier { } Future setSelectedTheme(int value) async { - final updated = await _updateThemeDb(value); + final updated = await updateConfigQuery( + db: _dbInstance!, + column: 'theme', + value: value + ); if (updated == true) { _selectedTheme = value; notifyListeners(); @@ -208,7 +238,11 @@ class AppConfigProvider with ChangeNotifier { } Future setUseDynamicColor(bool value) async { - final updated = await _updateDynamicColorDb(value == true ? 1 : 0); + final updated = await updateConfigQuery( + db: _dbInstance!, + column: 'useDynamicColor', + value: value == true ? 1 : 0 + ); if (updated == true) { _useDynamicColor = value; notifyListeners(); @@ -220,7 +254,11 @@ class AppConfigProvider with ChangeNotifier { } Future setUseThemeColorForStatus(bool value) async { - final updated = await _updateUseThemeColorForStatusDb(value == true ? 1 : 0); + final updated = await updateConfigQuery( + db: _dbInstance!, + column: 'useThemeColorForStatus', + value: value == true ? 1 : 0 + ); if (updated == true) { _useThemeColorForStatus = value; notifyListeners(); @@ -232,7 +270,11 @@ class AppConfigProvider with ChangeNotifier { } Future setStaticColor(int value) async { - final updated = await _updateStaticColorDb(value); + final updated = await updateConfigQuery( + db: _dbInstance!, + column: 'staticColor', + value: value + ); if (updated == true) { _staticColor = value; notifyListeners(); @@ -244,109 +286,12 @@ class AppConfigProvider with ChangeNotifier { } Future setDoNotRememberVersion(String value) async { - try { - return await _dbInstance!.transaction((txn) async { - await txn.rawUpdate( - 'UPDATE appConfig SET doNotRememberVersion = "$value"', - ); - _doNotRememberVersion = value; - notifyListeners(); - return true; - }); - } catch (e) { - return false; - } - } - - Future _updateThemeDb(int value) async { - try { - return await _dbInstance!.transaction((txn) async { - await txn.rawUpdate( - 'UPDATE appConfig SET theme = $value', - ); - return true; - }); - } catch (e) { - return false; - } - } - - Future _updateDynamicColorDb(int value) async { - try { - return await _dbInstance!.transaction((txn) async { - await txn.rawUpdate( - 'UPDATE appConfig SET useDynamicColor = $value', - ); - return true; - }); - } catch (e) { - return false; - } - } - - Future _updateUseThemeColorForStatusDb(int value) async { - try { - return await _dbInstance!.transaction((txn) async { - await txn.rawUpdate( - 'UPDATE appConfig SET useThemeColorForStatus = $value', - ); - return true; - }); - } catch (e) { - return false; - } - } - - Future _updateStaticColorDb(int value) async { - try { - return await _dbInstance!.transaction((txn) async { - await txn.rawUpdate( - 'UPDATE appConfig SET staticColor = $value', - ); - return true; - }); - } catch (e) { - return false; - } - } - - Future _updateOverrideSslCheck(int value) async { - try { - return await _dbInstance!.transaction((txn) async { - await txn.rawUpdate( - 'UPDATE appConfig SET overrideSslCheck = $value', - ); - return true; - }); - } catch (e) { - return false; - } - } - - Future _updateSetHideZeroValues(int value) async { - try { - return await _dbInstance!.transaction((txn) async { - await txn.rawUpdate( - 'UPDATE appConfig SET hideZeroValues = $value', - ); - return true; - }); - } catch (e) { - return false; - } - } - - Future _updateShowNameTimeLogsDb(int value) async { - try { - return await _dbInstance!.transaction((txn) async { - await txn.rawUpdate( - 'UPDATE appConfig SET showNameTimeLogs = $value', - ); - return true; - }); - } catch (e) { - return false; - } + final updated = await updateConfigQuery( + db: _dbInstance!, + column: 'doNotRememberVersion', + value: value + ); + return updated; } void saveFromDb(Database dbInstance, Map dbData) { diff --git a/lib/providers/servers_provider.dart b/lib/providers/servers_provider.dart index c5cde45..e59d8ae 100644 --- a/lib/providers/servers_provider.dart +++ b/lib/providers/servers_provider.dart @@ -7,14 +7,15 @@ import 'package:adguard_home_manager/models/dns_info.dart'; import 'package:adguard_home_manager/models/rewrite_rules.dart'; import 'package:adguard_home_manager/models/filtering_status.dart'; import 'package:adguard_home_manager/models/clients_allowed_blocked.dart'; -import 'package:adguard_home_manager/models/update_available.dart'; import 'package:adguard_home_manager/models/blocked_services.dart'; import 'package:adguard_home_manager/models/clients.dart'; import 'package:adguard_home_manager/models/server_status.dart'; import 'package:adguard_home_manager/models/server.dart'; +import 'package:adguard_home_manager/models/update_available.dart'; import 'package:adguard_home_manager/services/http_requests.dart'; import 'package:adguard_home_manager/functions/time_server_disabled.dart'; import 'package:adguard_home_manager/functions/conversions.dart'; +import 'package:adguard_home_manager/services/db/queries.dart'; import 'package:adguard_home_manager/functions/compare_versions.dart'; import 'package:adguard_home_manager/constants/enums.dart'; @@ -33,6 +34,9 @@ class ServersProvider with ChangeNotifier { loadStatus: LoadStatus.loading, data: null ); + String? _searchTermClients; + List _filteredActiveClients = []; + List _filteredAddedClients = []; final Filtering _filtering = Filtering( loadStatus: LoadStatus.loading, @@ -86,6 +90,18 @@ class ServersProvider with ChangeNotifier { return _clients; } + String? get searchTermClients { + return _searchTermClients; + } + + List get filteredActiveClients { + return _filteredActiveClients; + } + + List get filteredAddedClients { + return _filteredAddedClients; + } + FilteringStatus? get filteringStatus { return _filteringStatus; } @@ -147,6 +163,43 @@ class ServersProvider with ChangeNotifier { void setClientsData(ClientsData data) { _clients.data = data; + if (_searchTermClients != null && _searchTermClients != '') { + _filteredActiveClients = _clients.data!.autoClientsData.where( + (client) => client.ip.contains(_searchTermClients!.toLowerCase()) || (client.name != null ? client.name!.contains(_searchTermClients!.toLowerCase()) : false) + ).toList(); + _filteredAddedClients = _clients.data!.clients.where( + (client) { + isContained(String value) => value.contains(value.toLowerCase()); + return client.ids.any(isContained); + } + ).toList(); + } + else { + _filteredActiveClients = data.autoClientsData; + _filteredAddedClients = data.clients; + } + notifyListeners(); + } + + void setSearchTermClients(String? value) { + _searchTermClients = value; + if (value != null && value != '') { + if (_clients.data != null) { + _filteredActiveClients = _clients.data!.autoClientsData.where( + (client) => client.ip.contains(value.toLowerCase()) || (client.name != null ? client.name!.contains(value.toLowerCase()) : false) + ).toList(); + _filteredAddedClients = _clients.data!.clients.where( + (client) { + isContained(String value) => value.contains(value.toLowerCase()); + return client.ids.any(isContained); + } + ).toList(); + } + } + else { + if (_clients.data != null) _filteredActiveClients = _clients.data!.autoClientsData; + if (_clients.data != null) _filteredAddedClients = _clients.data!.clients; + } notifyListeners(); } @@ -249,7 +302,7 @@ class ServersProvider with ChangeNotifier { } Future createServer(Server server) async { - final saved = await saveServerIntoDb(server); + final saved = await saveServerQuery(_dbInstance!, server); if (saved == null) { if (server.defaultServer == true) { final defaultServer = await setDefaultServer(server); @@ -274,7 +327,7 @@ class ServersProvider with ChangeNotifier { } Future setDefaultServer(Server server) async { - final updated = await setDefaultServerDb(server.id); + final updated = await setDefaultServerQuery(_dbInstance!, server.id); if (updated == null) { List newServers = _serversList.map((s) { if (s.id == server.id) { @@ -296,7 +349,7 @@ class ServersProvider with ChangeNotifier { } Future editServer(Server server) async { - final result = await editServerDb(server); + final result = await editServerQuery(_dbInstance!, server); if (result == null) { List newServers = _serversList.map((s) { if (s.id == server.id) { @@ -316,7 +369,7 @@ class ServersProvider with ChangeNotifier { } Future removeServer(Server server) async { - final result = await removeFromDb(server.id); + final result = await removeServerQuery(_dbInstance!, server.id); if (result == true) { _selectedServer = null; List newServers = _serversList.where((s) => s.id != server.id).toList(); @@ -472,63 +525,6 @@ class ServersProvider with ChangeNotifier { } } - Future saveServerIntoDb(Server server) async { - try { - return await _dbInstance!.transaction((txn) async { - await txn.rawInsert( - 'INSERT INTO servers (id, name, connectionMethod, domain, path, port, user, password, defaultServer, authToken, runningOnHa) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', - [server.id, server.name, server.connectionMethod, server.domain, server.path, server.port, server.user, server.password, server.defaultServer, server.authToken, convertFromBoolToInt(server.runningOnHa)] - ); - return null; - }); - } catch (e) { - return e; - } - } - - Future editServerDb(Server server) async { - try { - return await _dbInstance!.transaction((txn) async { - await txn.rawUpdate( - 'UPDATE servers SET name = ?, connectionMethod = ?, domain = ?, path = ?, port = ?, user = ?, password = ?, authToken = ?, runningOnHa = ? WHERE id = "${server.id}"', - [server.name, server.connectionMethod, server.domain, server.path, server.port, server.user, server.password, server.authToken, server.runningOnHa] - ); - return null; - }); - } catch (e) { - return e; - } - } - - Future removeFromDb(String id) async { - try { - return await _dbInstance!.transaction((txn) async { - await txn.rawDelete( - 'DELETE FROM servers WHERE id = "$id"', - ); - return true; - }); - } catch (e) { - return false; - } - } - - Future setDefaultServerDb(String id) async { - try { - return await _dbInstance!.transaction((txn) async { - await txn.rawUpdate( - 'UPDATE servers SET defaultServer = 0 WHERE defaultServer = 1', - ); - await txn.rawUpdate( - 'UPDATE servers SET defaultServer = 1 WHERE id = "$id"', - ); - return null; - }); - } catch (e) { - return e; - } - } - void checkServerUpdatesAvailable(Server server) async { setUpdateAvailableLoadStatus(LoadStatus.loading, true); final result = await checkServerUpdates(server: server); diff --git a/lib/screens/clients/active_client_tile.dart b/lib/screens/clients/active_client_tile.dart new file mode 100644 index 0000000..cabbb6c --- /dev/null +++ b/lib/screens/clients/active_client_tile.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; + +import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; + +import 'package:adguard_home_manager/models/clients.dart'; + +class ActiveClientTile extends StatelessWidget { + final AutoClient client; + final void Function(AutoClient) onTap; + final bool splitView; + final AutoClient? selectedClient; + + const ActiveClientTile({ + Key? key, + required this.client, + required this.onTap, + required this.splitView, + this.selectedClient + }) : super(key: key); + + @override + Widget build(BuildContext context) { + if (splitView == true) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(28), + child: InkWell( + borderRadius: BorderRadius.circular(28), + onTap: () => onTap(client), + child: Container( + width: double.maxFinite, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(28), + color: client == selectedClient + ? Theme.of(context).colorScheme.primaryContainer + : null + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + client.name != '' + ? client.name! + : client.ip, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + if (client.name != '') Text(client.ip) + ], + ), + ) + ], + ), + ), + Text( + client.source, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface + ), + ), + ], + ) + ), + ), + ), + ); + } + else { + return CustomListTile( + title: client.name != '' + ? client.name! + : client.ip, + subtitle: client.name != '' + ? client.ip + : null, + trailing: Text( + client.source, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface + ), + ), + onTap: () => onTap(client), + ); + } + } +} \ No newline at end of file diff --git a/lib/screens/clients/added_client_tile.dart b/lib/screens/clients/added_client_tile.dart new file mode 100644 index 0000000..d8a4b8f --- /dev/null +++ b/lib/screens/clients/added_client_tile.dart @@ -0,0 +1,250 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; + +import 'package:adguard_home_manager/functions/compare_versions.dart'; +import 'package:adguard_home_manager/models/clients.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; + +class AddedClientTile extends StatefulWidget { + final Client client; + final void Function(Client) onTap; + final void Function(Client) onLongPress; + final void Function(Client) onEdit; + final Client? selectedClient; + final bool? splitView; + + const AddedClientTile({ + Key? key, + required this.client, + required this.onTap, + required this.onLongPress, + required this.onEdit, + this.selectedClient, + required this.splitView + }) : super(key: key); + + @override + State createState() => _AddedClientTileState(); +} + +class _AddedClientTileState extends State { + bool hover = false; + + @override + Widget build(BuildContext context) { + final serversProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + + if (widget.splitView == true) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(28), + child: InkWell( + borderRadius: BorderRadius.circular(28), + onTap: () => widget.onTap(widget.client), + onHover: (v) => setState(() => hover = v), + child: Container( + width: double.maxFinite, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(28), + color: widget.client == widget.selectedClient + ? Theme.of(context).colorScheme.primaryContainer + : null + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.client.ids.toString().replaceAll(RegExp(r'^\[|\]$'), ''), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Icon( + Icons.filter_list_rounded, + size: 19, + color: widget.client.filteringEnabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red, + ), + const SizedBox(width: 10), + Icon( + Icons.vpn_lock_rounded, + size: 18, + color: widget.client.safebrowsingEnabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red, + ), + const SizedBox(width: 10), + Icon( + Icons.block, + size: 18, + color: widget.client.parentalEnabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red, + ), + const SizedBox(width: 10), + Icon( + Icons.search_rounded, + size: 19, + color: serverVersionIsAhead( + currentVersion: serversProvider.serverStatus.data!.serverVersion, + referenceVersion: 'v0.107.28', + referenceVersionBeta: 'v0.108.0-b.33' + ) == true + ? widget.client.safeSearch != null && widget.client.safeSearch!.enabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red + : widget.client.safesearchEnabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red, + ) + ], + ) + ], + ), + ) + ], + ), + ), + if (hover == true) IconButton( + onPressed: () => widget.onEdit(widget.client), + icon: const Icon(Icons.edit_rounded) + ) + ], + ) + ), + ), + ), + ); + } + else { + return CustomListTile( + onLongPress: () => widget.onLongPress(widget.client), + onTap: () => widget.onTap(widget.client), + onHover: (v) => setState(() => hover = v), + title: widget.client.name, + subtitleWidget: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.client.ids.toString().replaceAll(RegExp(r'^\[|\]$'), ''), + style: TextStyle( + color: Theme.of(context).listTileTheme.textColor + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Icon( + Icons.filter_list_rounded, + size: 19, + color: widget.client.filteringEnabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red, + ), + const SizedBox(width: 10), + Icon( + Icons.vpn_lock_rounded, + size: 18, + color: widget.client.safebrowsingEnabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red, + ), + const SizedBox(width: 10), + Icon( + Icons.block, + size: 18, + color: widget.client.parentalEnabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red, + ), + const SizedBox(width: 10), + Icon( + Icons.search_rounded, + size: 19, + color: serverVersionIsAhead( + currentVersion: serversProvider.serverStatus.data!.serverVersion, + referenceVersion: 'v0.107.28', + referenceVersionBeta: 'v0.108.0-b.33' + ) == true + ? widget.client.safeSearch != null && widget.client.safeSearch!.enabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red + : widget.client.safesearchEnabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red, + ) + ], + ) + ], + ), + trailing: hover == true + ? IconButton( + onPressed: () => widget.onEdit(widget.client), + icon: const Icon(Icons.edit_rounded) + ) + : null, + ); + } + } +} \ No newline at end of file diff --git a/lib/screens/clients/added_list.dart b/lib/screens/clients/added_list.dart index 922d2de..87cae35 100644 --- a/lib/screens/clients/added_list.dart +++ b/lib/screens/clients/added_list.dart @@ -1,12 +1,16 @@ // ignore_for_file: use_build_context_synchronously +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:animations/animations.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter_split_view/flutter_split_view.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/screens/clients/client_screen.dart'; +import 'package:adguard_home_manager/screens/clients/added_client_tile.dart'; import 'package:adguard_home_manager/screens/clients/remove_client_modal.dart'; import 'package:adguard_home_manager/screens/clients/fab.dart'; import 'package:adguard_home_manager/screens/clients/options_modal.dart'; @@ -27,13 +31,19 @@ class AddedList extends StatefulWidget { final LoadStatus loadStatus; final List data; final Future Function() fetchClients; + final void Function(Client) onClientSelected; + final Client? selectedClient; + final bool splitView; const AddedList({ Key? key, required this.scrollController, required this.loadStatus, required this.data, - required this.fetchClients + required this.fetchClients, + required this.onClientSelected, + this.selectedClient, + required this.splitView }) : super(key: key); @override @@ -69,6 +79,8 @@ class _AddedListState extends State { final serversProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + void confirmEditClient(Client client) async { ProcessModal processModal = ProcessModal(context: context); processModal.open(AppLocalizations.of(context)!.addingClient); @@ -130,6 +142,10 @@ class _AddedListState extends State { clientsData.clients = clientsData.clients.where((c) => c.name != client.name).toList(); serversProvider.setClientsData(clientsData); + if (widget.splitView == true) { + SplitView.of(context).popUntil(0); + } + showSnacbkar( context: context, appConfigProvider: appConfigProvider, @@ -150,15 +166,31 @@ class _AddedListState extends State { } void openClientModal(Client client) { - Navigator.push(context, MaterialPageRoute( - fullscreenDialog: true, - builder: (BuildContext context) => ClientScreen( - onConfirm: confirmEditClient, - serverVersion: serversProvider.serverStatus.data!.serverVersion, - onDelete: deleteClient, - client: client, - ) - )); + if (width > 900 || !(Platform.isAndroid | Platform.isIOS)) { + showDialog( + barrierDismissible: false, + context: context, + builder: (BuildContext context) => ClientScreen( + onConfirm: confirmEditClient, + serverVersion: serversProvider.serverStatus.data!.serverVersion, + onDelete: deleteClient, + client: client, + dialog: true, + ) + ); + } + else { + Navigator.push(context, MaterialPageRoute( + fullscreenDialog: true, + builder: (BuildContext context) => ClientScreen( + onConfirm: confirmEditClient, + serverVersion: serversProvider.serverStatus.data!.serverVersion, + onDelete: deleteClient, + client: client, + dialog: false, + ) + )); + } } void openDeleteModal(Client client) { @@ -181,9 +213,12 @@ class _AddedListState extends State { } return CustomTabContentList( + noSliver: !(Platform.isAndroid || Platform.isIOS), + listPadding: widget.splitView == true + ? const EdgeInsets.only(top: 8) + : null, loadingGenerator: () => SizedBox( width: double.maxFinite, - height: MediaQuery.of(context).size.height-171, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, @@ -202,109 +237,28 @@ class _AddedListState extends State { ), ), itemsCount: widget.data.length, - contentWidget: (index) => ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - isThreeLine: true, - onLongPress: () => openOptionsModal(widget.data[index]), - onTap: () => openClientModal(widget.data[index]), - title: Padding( - padding: const EdgeInsets.only(bottom: 5), - child: Text( - widget.data[index].name, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.normal, - color: Theme.of(context).colorScheme.onSurface - ), - ), - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.data[index].ids.toString().replaceAll(RegExp(r'^\[|\]$'), ''), - style: TextStyle( - color: Theme.of(context).listTileTheme.textColor - ), - ), - const SizedBox(height: 7), - Row( - children: [ - Icon( - Icons.filter_list_rounded, - size: 19, - color: widget.data[index].filteringEnabled == true - ? appConfigProvider.useThemeColorForStatus == true - ? Theme.of(context).colorScheme.primary - : Colors.green - : appConfigProvider.useThemeColorForStatus == true - ? Colors.grey - : Colors.red, - ), - const SizedBox(width: 10), - Icon( - Icons.vpn_lock_rounded, - size: 18, - color: widget.data[index].safebrowsingEnabled == true - ? appConfigProvider.useThemeColorForStatus == true - ? Theme.of(context).colorScheme.primary - : Colors.green - : appConfigProvider.useThemeColorForStatus == true - ? Colors.grey - : Colors.red, - ), - const SizedBox(width: 10), - Icon( - Icons.block, - size: 18, - color: widget.data[index].parentalEnabled == true - ? appConfigProvider.useThemeColorForStatus == true - ? Theme.of(context).colorScheme.primary - : Colors.green - : appConfigProvider.useThemeColorForStatus == true - ? Colors.grey - : Colors.red, - ), - const SizedBox(width: 10), - Icon( - Icons.search_rounded, - size: 19, - color: serverVersionIsAhead( - currentVersion: serversProvider.serverStatus.data!.serverVersion, - referenceVersion: 'v0.107.28', - referenceVersionBeta: 'v0.108.0-b.33' - ) == true - ? widget.data[index].safeSearch != null && widget.data[index].safeSearch!.enabled == true - ? appConfigProvider.useThemeColorForStatus == true - ? Theme.of(context).colorScheme.primary - : Colors.green - : appConfigProvider.useThemeColorForStatus == true - ? Colors.grey - : Colors.red - : widget.data[index].safesearchEnabled == true - ? appConfigProvider.useThemeColorForStatus == true - ? Theme.of(context).colorScheme.primary - : Colors.green - : appConfigProvider.useThemeColorForStatus == true - ? Colors.grey - : Colors.red, - ) - ], - ) - ], - ), - ), + contentWidget: (index) => AddedClientTile( + selectedClient: widget.selectedClient, + client: widget.data[index], + onTap: widget.onClientSelected, + onLongPress: openOptionsModal, + onEdit: openClientModal, + splitView: widget.splitView, + ), noData: SizedBox( width: double.maxFinite, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - AppLocalizations.of(context)!.noClientsList, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSurfaceVariant, + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + AppLocalizations.of(context)!.noClientsList, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), ), const SizedBox(height: 30), @@ -318,7 +272,6 @@ class _AddedListState extends State { ), errorGenerator: () => SizedBox( width: double.maxFinite, - height: MediaQuery.of(context).size.height-171, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, diff --git a/lib/screens/clients/client_screen.dart b/lib/screens/clients/client_screen.dart index 461ba42..2e282f5 100644 --- a/lib/screens/clients/client_screen.dart +++ b/lib/screens/clients/client_screen.dart @@ -19,6 +19,7 @@ class ClientScreen extends StatefulWidget { final String serverVersion; final void Function(Client) onConfirm; final void Function(Client)? onDelete; + final bool dialog; const ClientScreen({ Key? key, @@ -26,6 +27,7 @@ class ClientScreen extends StatefulWidget { required this.serverVersion, required this.onConfirm, this.onDelete, + required this.dialog }) : super(key: key); @override @@ -300,51 +302,13 @@ class _ClientScreenState extends State { ), ); } - - return Scaffold( - appBar: AppBar( - leading: IconButton( - onPressed: () => Navigator.pop(context), - icon: const Icon(Icons.close) - ), - title: Text( - widget.client != null - ? AppLocalizations.of(context)!.client - : AppLocalizations.of(context)!.addClient - ), - actions: [ - if (widget.client == null || (widget.client != null && editMode == true)) IconButton( - onPressed: validValues == true - ? () { - createClient(); - Navigator.pop(context); - } - : null, - icon: Icon( - widget.client != null && editMode == true - ? Icons.save_rounded - : Icons.check_rounded - ), - tooltip: widget.client != null && editMode == true - ? AppLocalizations.of(context)!.save - : AppLocalizations.of(context)!.confirm, - ), - if (widget.client != null && editMode == false) IconButton( - onPressed: () => setState(() => editMode = true), - icon: const Icon(Icons.edit_rounded), - tooltip: AppLocalizations.of(context)!.edit, - ), - if (widget.client != null) IconButton( - onPressed: openDeleteClientScreen, - icon: const Icon(Icons.delete_rounded), - tooltip: AppLocalizations.of(context)!.delete, - ), - const SizedBox(width: 10), - ], - ), - body: ListView( + + Widget content(bool withPaddingTop) { + return ListView( + padding: const EdgeInsets.only(top: 0), children: [ - const SizedBox(height: 24), + if (withPaddingTop == true) const SizedBox(height: 24), + if (withPaddingTop == false) const SizedBox(height: 6), Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: TextFormField( @@ -693,10 +657,7 @@ class _ClientScreenState extends State { child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - SizedBox( - width: editMode == true - ? MediaQuery.of(context).size.width - 108 - : MediaQuery.of(context).size.width - 40, + Expanded( child: TextFormField( enabled: editMode, controller: controller['controller'], @@ -751,7 +712,125 @@ class _ClientScreenState extends State { ), const SizedBox(height: 20) ], - ), - ); + ); + } + + if (widget.dialog == true) { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500 + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.clear_rounded) + ), + const SizedBox(width: 8), + Text( + widget.client != null + ? AppLocalizations.of(context)!.client + : AppLocalizations.of(context)!.addClient, + style: const TextStyle( + fontSize: 22 + ), + ), + ], + ), + Row( + children: [ + if (widget.client == null || (widget.client != null && editMode == true)) IconButton( + onPressed: validValues == true + ? () { + createClient(); + Navigator.pop(context); + } + : null, + icon: Icon( + widget.client != null && editMode == true + ? Icons.save_rounded + : Icons.check_rounded + ), + tooltip: widget.client != null && editMode == true + ? AppLocalizations.of(context)!.save + : AppLocalizations.of(context)!.confirm, + ), + if (widget.client != null && editMode == false) IconButton( + onPressed: () => setState(() => editMode = true), + icon: const Icon(Icons.edit_rounded), + tooltip: AppLocalizations.of(context)!.edit, + ), + if (widget.client != null) IconButton( + onPressed: openDeleteClientScreen, + icon: const Icon(Icons.delete_rounded), + tooltip: AppLocalizations.of(context)!.delete, + ), + const SizedBox(width: 10), + ], + ) + ], + ), + ), + Flexible( + child: content(false) + ) + ], + ), + ), + ); + } + else { + return Scaffold( + appBar: AppBar( + leading: IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.close) + ), + title: Text( + widget.client != null + ? AppLocalizations.of(context)!.client + : AppLocalizations.of(context)!.addClient + ), + actions: [ + if (widget.client == null || (widget.client != null && editMode == true)) IconButton( + onPressed: validValues == true + ? () { + createClient(); + Navigator.pop(context); + } + : null, + icon: Icon( + widget.client != null && editMode == true + ? Icons.save_rounded + : Icons.check_rounded + ), + tooltip: widget.client != null && editMode == true + ? AppLocalizations.of(context)!.save + : AppLocalizations.of(context)!.confirm, + ), + if (widget.client != null && editMode == false) IconButton( + onPressed: () => setState(() => editMode = true), + icon: const Icon(Icons.edit_rounded), + tooltip: AppLocalizations.of(context)!.edit, + ), + if (widget.client != null) IconButton( + onPressed: openDeleteClientScreen, + icon: const Icon(Icons.delete_rounded), + tooltip: AppLocalizations.of(context)!.delete, + ), + const SizedBox(width: 10), + ], + ), + body: content(true) + ); + } } } \ No newline at end of file diff --git a/lib/screens/clients/clients.dart b/lib/screens/clients/clients.dart index 549b242..9e13c77 100644 --- a/lib/screens/clients/clients.dart +++ b/lib/screens/clients/clients.dart @@ -1,9 +1,14 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; +import 'package:flutter_split_view/flutter_split_view.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/screens/clients/clients_list.dart'; import 'package:adguard_home_manager/screens/clients/search_clients.dart'; +import 'package:adguard_home_manager/screens/clients/logs_list_client.dart'; +import 'package:adguard_home_manager/screens/clients/clients_desktop_view.dart'; import 'package:adguard_home_manager/screens/clients/added_list.dart'; import 'package:adguard_home_manager/models/app_log.dart'; @@ -56,6 +61,9 @@ class _ClientsWidgetState extends State with TickerProviderStateM late TabController tabController; final ScrollController scrollController = ScrollController(); + bool searchMode = false; + final TextEditingController searchController = TextEditingController(); + Future fetchClients() async { widget.setLoadingStatus(LoadStatus.loading, false); final result = await getClients(widget.server); @@ -90,83 +98,207 @@ class _ClientsWidgetState extends State with TickerProviderStateM @override Widget build(BuildContext context) { final serversProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + + final width = MediaQuery.of(context).size.width; - return DefaultTabController( - length: 2, - child: NestedScrollView( - controller: scrollController, - headerSliverBuilder: ((context, innerBoxIsScrolled) { - return [ - SliverOverlapAbsorber( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - sliver: SliverAppBar( - title: Text(AppLocalizations.of(context)!.clients), - pinned: true, - floating: true, - centerTitle: false, - forceElevated: innerBoxIsScrolled, - actions: [ - if (serversProvider.clients.loadStatus == LoadStatus.loaded) ...[ - IconButton( - onPressed: () => { - Navigator.push(context, MaterialPageRoute( - builder: (context) => const SearchClients() - )) - }, - icon: const Icon(Icons.search), - tooltip: AppLocalizations.of(context)!.searchClients, - ), - const SizedBox(width: 10), - ] - ], - bottom: TabBar( - controller: tabController, - unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant, - tabs: [ - Tab( - icon: const Icon(Icons.devices), - text: AppLocalizations.of(context)!.activeClients, - ), - Tab( - icon: const Icon(Icons.add_rounded), - text: AppLocalizations.of(context)!.added, - ), - ] - ) - ), - ) - ]; - }), - body: Container( - decoration: BoxDecoration( - color: Theme.of(context).scaffoldBackgroundColor, - border: Border( - top: BorderSide( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.1) - ) - ) + PreferredSizeWidget tabBar() { + return TabBar( + controller: tabController, + unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant, + tabs: [ + Tab( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.devices), + const SizedBox(width: 8), + Text(AppLocalizations.of(context)!.activeClients) + ], + ), ), - child: TabBarView( - controller: tabController, - children: [ - ClientsList( - scrollController: scrollController, - loadStatus: serversProvider.clients.loadStatus, - data: serversProvider.clients.loadStatus == LoadStatus.loaded - ? serversProvider.clients.data!.autoClientsData : [], - fetchClients: fetchClients, + Tab( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.add_rounded), + const SizedBox(width: 8), + Text(AppLocalizations.of(context)!.added) + ], + ), + ), + ] + ); + } + + Widget tabBarView(bool sliver) { + return TabBarView( + controller: tabController, + children: [ + ClientsList( + scrollController: scrollController, + loadStatus: serversProvider.clients.loadStatus, + data: serversProvider.clients.loadStatus == LoadStatus.loaded + ? serversProvider.filteredActiveClients : [], + fetchClients: fetchClients, + onClientSelected: (client) => Navigator.push(context, MaterialPageRoute( + builder: (context) => LogsListClient( + ip: client.ip, + serversProvider: serversProvider, + appConfigProvider: appConfigProvider + ) + )), + splitView: false, + sliver: sliver, + ), + AddedList( + scrollController: scrollController, + loadStatus: serversProvider.clients.loadStatus, + data: serversProvider.clients.loadStatus == LoadStatus.loaded + ? serversProvider.filteredAddedClients : [], + fetchClients: fetchClients, + onClientSelected: (client) => Navigator.push(context, MaterialPageRoute( + builder: (context) => LogsListClient( + ip: client.ids[0], + serversProvider: serversProvider, + appConfigProvider: appConfigProvider + ) + )), + splitView: false, + ), + ] + ); + } + + if (width > 900) { + return SplitView.material( + hideDivider: true, + flexWidth: const FlexWidth(mainViewFlexWidth: 1, secondaryViewFlexWidth: 2), + placeholder: Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Text( + AppLocalizations.of(context)!.selectClientLeftColumn, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + color: Theme.of(context).colorScheme.onSurfaceVariant ), - AddedList( - scrollController: scrollController, - loadStatus: serversProvider.clients.loadStatus, - data: serversProvider.clients.loadStatus == LoadStatus.loaded - ? serversProvider.clients.data!.clients : [], - fetchClients: fetchClients, - ), - ] - ) + ), + ), ), - ) - ); + child: ClientsDesktopView( + serversProvider: serversProvider, + appConfigProvider: appConfigProvider, + fetchClients: fetchClients, + ) + ); + } + else { + if (!(Platform.isAndroid || Platform.isIOS)) { + return DefaultTabController( + length: 2, + child: Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context)!.clients), + centerTitle: false, + actions: [ + if (serversProvider.clients.loadStatus == LoadStatus.loaded) ...[ + IconButton( + onPressed: () => { + Navigator.push(context, MaterialPageRoute( + builder: (context) => const SearchClients() + )) + }, + icon: const Icon(Icons.search), + tooltip: AppLocalizations.of(context)!.searchClients, + ), + const SizedBox(width: 10), + ] + ], + bottom: tabBar() + ), + body: tabBarView(false), + ), + ); + } + else { + return DefaultTabController( + length: 2, + child: NestedScrollView( + controller: scrollController, + headerSliverBuilder: ((context, innerBoxIsScrolled) { + return [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverAppBar( + title: searchMode == true + ? Row( + children: [ + IconButton( + onPressed: () { + setState(() { + searchMode = false; + searchController.text = ""; + serversProvider.setSearchTermClients(null); + }); + }, + icon: const Icon(Icons.arrow_back_rounded) + ), + const SizedBox(width: 16), + Expanded( + child: TextField( + controller: searchController, + onChanged: (value) => serversProvider.setSearchTermClients(value), + decoration: InputDecoration( + suffixIcon: IconButton( + onPressed: () { + setState(() { + searchController.text = ""; + serversProvider.setSearchTermClients(null); + }); + }, + icon: const Icon(Icons.clear_rounded) + ), + hintText: AppLocalizations.of(context)!.search, + hintStyle: const TextStyle( + fontWeight: FontWeight.normal, + fontSize: 18 + ), + border: InputBorder.none, + ), + style: const TextStyle( + fontWeight: FontWeight.normal, + fontSize: 18 + ), + ), + ) + ], + ) + : Text(AppLocalizations.of(context)!.clients), + pinned: true, + floating: true, + centerTitle: false, + forceElevated: innerBoxIsScrolled, + actions: [ + if (serversProvider.clients.loadStatus == LoadStatus.loaded && searchMode == false) ...[ + IconButton( + onPressed: () => setState(() => searchMode = true), + icon: const Icon(Icons.search), + tooltip: AppLocalizations.of(context)!.searchClients, + ), + const SizedBox(width: 10), + ] + ], + bottom: tabBar() + ), + ) + ]; + }), + body: tabBarView(true) + ) + ); + } + } } } \ No newline at end of file diff --git a/lib/screens/clients/clients_desktop_view.dart b/lib/screens/clients/clients_desktop_view.dart new file mode 100644 index 0000000..0a4b783 --- /dev/null +++ b/lib/screens/clients/clients_desktop_view.dart @@ -0,0 +1,248 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_split_view/flutter_split_view.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/screens/clients/logs_list_client.dart'; +import 'package:adguard_home_manager/screens/clients/added_list.dart'; +import 'package:adguard_home_manager/screens/clients/clients_list.dart'; + +import 'package:adguard_home_manager/providers/app_config_provider.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/models/clients.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; + + +class ClientsDesktopView extends StatefulWidget { + final ServersProvider serversProvider; + final AppConfigProvider appConfigProvider; + final Future Function() fetchClients; + + const ClientsDesktopView({ + Key? key, + required this.serversProvider, + required this.appConfigProvider, + required this.fetchClients + }) : super(key: key); + + @override + State createState() => _ClientsDesktopViewState(); +} + +class _ClientsDesktopViewState extends State with TickerProviderStateMixin { + late TabController tabController; + final ScrollController scrollController = ScrollController(); + + AutoClient? selectedActiveClient; + Client? selectedAddedClient; + + bool searchMode = false; + final TextEditingController searchController = TextEditingController(); + + @override + void initState() { + super.initState(); + tabController = TabController( + initialIndex: 0, + length: 2, + vsync: this, + ); + tabController.addListener(() => widget.appConfigProvider.setSelectedClientsTab(tabController.index)); + } + + @override + Widget build(BuildContext context) { + final serversProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + + PreferredSizeWidget tabBar() { + return TabBar( + controller: tabController, + unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant, + tabs: [ + Tab( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.devices), + const SizedBox(width: 8), + Text(AppLocalizations.of(context)!.activeClients) + ], + ), + ), + Tab( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.add_rounded), + const SizedBox(width: 8), + Text(AppLocalizations.of(context)!.added) + ], + ), + ), + ] + ); + } + + Widget tabBarView(bool sliver) { + return TabBarView( + controller: tabController, + children: [ + ClientsList( + scrollController: scrollController, + loadStatus: serversProvider.clients.loadStatus, + data: serversProvider.clients.loadStatus == LoadStatus.loaded + ? serversProvider.filteredActiveClients : [], + fetchClients: widget.fetchClients, + onClientSelected: (client) => setState(() { + selectedAddedClient = null; + selectedActiveClient = client; + SplitView.of(context).setSecondary( + LogsListClient( + ip: client.ip, + name: client.name, + serversProvider: serversProvider, + appConfigProvider: appConfigProvider, + ) + ); + }), + selectedClient: selectedActiveClient, + splitView: true, + sliver: sliver, + ), + AddedList( + scrollController: scrollController, + loadStatus: serversProvider.clients.loadStatus, + data: serversProvider.clients.loadStatus == LoadStatus.loaded + ? serversProvider.filteredAddedClients : [], + fetchClients: widget.fetchClients, + onClientSelected: (client) => setState(() { + selectedActiveClient = null; + selectedAddedClient = client; + SplitView.of(context).setSecondary( + LogsListClient( + ip: client.ids[0], + name: client.name, + serversProvider: serversProvider, + appConfigProvider: appConfigProvider, + ) + ); + }), + selectedClient: selectedAddedClient, + splitView: true, + ), + ] + ); + } + + Widget title() { + if (searchMode == true) { + return Row( + children: [ + IconButton( + onPressed: () { + setState(() { + searchMode = false; + searchController.text = ""; + serversProvider.setSearchTermClients(null); + }); + }, + icon: const Icon(Icons.arrow_back_rounded) + ), + const SizedBox(width: 16), + Expanded( + child: TextField( + controller: searchController, + onChanged: (value) => serversProvider.setSearchTermClients(value), + decoration: InputDecoration( + suffixIcon: IconButton( + onPressed: () { + setState(() { + searchController.text = ""; + serversProvider.setSearchTermClients(null); + }); + }, + icon: const Icon(Icons.clear_rounded) + ), + hintText: AppLocalizations.of(context)!.search, + hintStyle: const TextStyle( + fontWeight: FontWeight.normal, + fontSize: 18 + ), + border: InputBorder.none, + ), + style: const TextStyle( + fontWeight: FontWeight.normal, + fontSize: 18 + ), + ), + ) + ], + ); + } + else { + return Text(AppLocalizations.of(context)!.clients); + } + } + + if (!(Platform.isAndroid || Platform.isIOS)) { + return DefaultTabController( + length: 2, + child: Scaffold( + appBar: AppBar( + title: title(), + centerTitle: false, + actions: [ + if (serversProvider.clients.loadStatus == LoadStatus.loaded && searchMode == false) ...[ + IconButton( + onPressed: () => setState(() => searchMode = true), + icon: const Icon(Icons.search), + tooltip: AppLocalizations.of(context)!.searchClients, + ), + const SizedBox(width: 10), + ] + ], + bottom: tabBar() + ), + body: tabBarView(false), + ), + ); + } + else { + return DefaultTabController( + length: 2, + child: NestedScrollView( + controller: scrollController, + headerSliverBuilder: ((context, innerBoxIsScrolled) { + return [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverAppBar( + title: title(), + pinned: true, + floating: true, + centerTitle: false, + forceElevated: innerBoxIsScrolled, + actions: [ + if (serversProvider.clients.loadStatus == LoadStatus.loaded && searchMode == false) ...[ + IconButton( + onPressed: () => setState(() => searchMode = true), + icon: const Icon(Icons.search), + tooltip: AppLocalizations.of(context)!.searchClients, + ), + const SizedBox(width: 10), + ] + ], + bottom: tabBar() + ), + ) + ]; + }), + body: tabBarView(true) + ) + ); + } + } +} \ No newline at end of file diff --git a/lib/screens/clients/clients_list.dart b/lib/screens/clients/clients_list.dart index af1ee86..9d2f11b 100644 --- a/lib/screens/clients/clients_list.dart +++ b/lib/screens/clients/clients_list.dart @@ -1,36 +1,44 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; +import 'package:adguard_home_manager/screens/clients/active_client_tile.dart'; + import 'package:adguard_home_manager/widgets/tab_content_list.dart'; import 'package:adguard_home_manager/models/clients.dart'; -import 'package:adguard_home_manager/models/applied_filters.dart'; -import 'package:adguard_home_manager/providers/app_config_provider.dart'; import 'package:adguard_home_manager/constants/enums.dart'; -import 'package:adguard_home_manager/providers/logs_provider.dart'; class ClientsList extends StatelessWidget { final ScrollController scrollController; final LoadStatus loadStatus; final List data; final Future Function() fetchClients; + final void Function(AutoClient) onClientSelected; + final AutoClient? selectedClient; + final bool splitView; + final bool sliver; const ClientsList({ Key? key, required this.scrollController, required this.loadStatus, required this.data, - required this.fetchClients + required this.fetchClients, + required this.onClientSelected, + this.selectedClient, + required this.splitView, + required this.sliver }) : super(key: key); @override Widget build(BuildContext context) { - final appConfigProvider = Provider.of(context); - final logsProvider = Provider.of(context); - return CustomTabContentList( + listPadding: splitView == true + ? const EdgeInsets.only(top: 8) + : null, + noSliver: !sliver, loadingGenerator: () => SizedBox( width: double.maxFinite, height: MediaQuery.of(context).size.height-171, @@ -52,32 +60,12 @@ class ClientsList extends StatelessWidget { ), ), itemsCount: data.length, - contentWidget: (index) => CustomListTile( - title: data[index].name != '' - ? data[index].name! - : data[index].ip, - subtitle: data[index].name != '' - ? data[index].ip - : null, - trailing: Text( - data[index].source, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface - ), - ), - onTap: () { - logsProvider.setSearchText(null); - logsProvider.setSelectedClients([data[index].ip]); - logsProvider.setAppliedFilters( - AppliedFiters( - selectedResultStatus: 'all', - searchText: null, - clients: [data[index].ip] - ) - ); - appConfigProvider.setSelectedScreen(2); - }, - ), + contentWidget: (index) => ActiveClientTile( + client: data[index], + onTap: onClientSelected, + splitView: splitView, + selectedClient: selectedClient, + ), noData: SizedBox( width: double.maxFinite, child: Column( diff --git a/lib/screens/clients/fab.dart b/lib/screens/clients/fab.dart index 4eff04e..83cf864 100644 --- a/lib/screens/clients/fab.dart +++ b/lib/screens/clients/fab.dart @@ -1,5 +1,7 @@ // ignore_for_file: use_build_context_synchronously +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -23,6 +25,8 @@ class ClientsFab extends StatelessWidget { final serversProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + void confirmAddClient(Client client) async { ProcessModal processModal = ProcessModal(context: context); processModal.open(AppLocalizations.of(context)!.addingClient); @@ -65,13 +69,27 @@ class ClientsFab extends StatelessWidget { } void openAddClient() { - Navigator.push(context, MaterialPageRoute( - fullscreenDialog: true, - builder: (BuildContext context) => ClientScreen( - onConfirm: confirmAddClient, - serverVersion: serversProvider.serverStatus.data!.serverVersion, - ) - )); + if (width > 900 || !(Platform.isAndroid | Platform.isIOS)) { + showDialog( + barrierDismissible: false, + context: context, + builder: (BuildContext context) => ClientScreen( + onConfirm: confirmAddClient, + serverVersion: serversProvider.serverStatus.data!.serverVersion, + dialog: true, + ) + ); + } + else { + Navigator.push(context, MaterialPageRoute( + fullscreenDialog: true, + builder: (BuildContext context) => ClientScreen( + onConfirm: confirmAddClient, + serverVersion: serversProvider.serverStatus.data!.serverVersion, + dialog: false, + ) + )); + } } return FloatingActionButton( diff --git a/lib/screens/clients/logs_list_client.dart b/lib/screens/clients/logs_list_client.dart new file mode 100644 index 0000000..91b8424 --- /dev/null +++ b/lib/screens/clients/logs_list_client.dart @@ -0,0 +1,229 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/screens/logs/log_tile.dart'; +import 'package:adguard_home_manager/screens/logs/log_details_screen.dart'; + +import 'package:adguard_home_manager/models/logs.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; +import 'package:adguard_home_manager/services/http_requests.dart'; + +class LogsListClient extends StatefulWidget { + final String ip; + final String? name; + final ServersProvider serversProvider; + final AppConfigProvider appConfigProvider; + + const LogsListClient({ + Key? key, + required this.ip, + this.name, + required this.serversProvider, + required this.appConfigProvider + }) : super(key: key); + + @override + State createState() => _LogsListClientState(); +} + +class _LogsListClientState extends State { + late ScrollController scrollController; + + bool isLoadingMore = false; + + int logsQuantity = 100; + int offset = 0; + + int loadStatus = 0; + LogsData? logsData; + + String previousIp = ""; + + bool showDivider = true; + + Future fetchLogs({ + int? inOffset, + bool? loadingMore, + String? responseStatus, + String? searchText, + }) async { + int offst = inOffset ?? offset; + + if (loadingMore != null && loadingMore == true) { + setState(() => isLoadingMore = true); + } + + final result = await getLogs( + server: widget.serversProvider.selectedServer!, + count: logsQuantity, + offset: offst, + search: '"${widget.ip}"' + ); + + if (loadingMore != null && loadingMore == true) { + setState(() => isLoadingMore = false); + } + + if (mounted) { + if (result['result'] == 'success') { + setState(() => offset = inOffset != null ? inOffset+logsQuantity : offset+logsQuantity); + if (loadingMore != null && loadingMore == true && logsData != null) { + LogsData newLogsData = result['data']; + newLogsData.data = [...logsData!.data, ...result['data'].data]; + setState(() => logsData = newLogsData); + } + else { + LogsData newLogsData = result['data']; + setState(() => logsData = newLogsData); + } + setState(() => loadStatus = 1); + } + else { + setState(() => loadStatus = 2); + widget.appConfigProvider.addLog(result['log']); + } + } + } + + + void scrollListener() { + if (scrollController.position.extentAfter < 500 && isLoadingMore == false) { + fetchLogs(loadingMore: true); + } + if (scrollController.position.pixels > 0) { + setState(() => showDivider = false); + } + else { + setState(() => showDivider = true); + } + } + + @override + void initState() { + scrollController = ScrollController()..addListener(scrollListener); + fetchLogs(inOffset: 0); + setState(() => previousIp = widget.ip); + super.initState(); + } + + @override + Widget build(BuildContext context) { + if (widget.ip != previousIp) { + setState(() => loadStatus = 0); + if (scrollController.hasClients) scrollController.animateTo(0, duration: const Duration(milliseconds: 1), curve: Curves.ease); + fetchLogs(inOffset: 0); + setState(() => previousIp = widget.ip); + } + + Widget status() { + switch (loadStatus) { + case 0: + return SizedBox( + width: double.maxFinite, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.loadingLogs, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ); + + case 1: + return RefreshIndicator( + onRefresh: fetchLogs, + child: ListView.builder( + controller: scrollController, + padding: const EdgeInsets.only(top: 0), + itemCount: isLoadingMore == true + ? logsData!.data.length+1 + : logsData!.data.length, + itemBuilder: (context, index) { + if (isLoadingMore == true && index == logsData!.data.length) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 20), + child: Center( + child: CircularProgressIndicator(), + ), + ); + } + else { + return LogTile( + log: logsData!.data[index], + index: index, + length: logsData!.data.length, + useAlwaysNormalTile: true, + onLogTap: (log) => showDialog( + context: context, + builder: (context) => LogDetailsScreen( + log: log, + dialog: true + ) + ) + ); + } + } + ), + ); + + case 2: + return SizedBox( + width: double.maxFinite, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon( + Icons.error, + color: Colors.red, + size: 50, + ), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.logsNotLoaded, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ); + + default: + return const SizedBox(); + } + } + + return Scaffold( + appBar: AppBar( + title: Text(widget.name != null && widget.name != '' ? widget.name! : widget.ip), + centerTitle: true, + actions: [ + if (!(Platform.isAndroid || Platform.isIOS)) ...[ + IconButton( + onPressed: fetchLogs, + icon: const Icon(Icons.refresh_rounded), + tooltip: AppLocalizations.of(context)!.refresh, + ), + const SizedBox(width: 8) + ] + ], + ), + body: status(), + ); + } +} \ No newline at end of file diff --git a/lib/screens/clients/safe_search_modal.dart b/lib/screens/clients/safe_search_modal.dart index 9d51f01..1be694c 100644 --- a/lib/screens/clients/safe_search_modal.dart +++ b/lib/screens/clients/safe_search_modal.dart @@ -64,109 +64,114 @@ class _SafeSearchModalState extends State { ) ], ), - content: Wrap( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Material( - color: Theme.of(context).colorScheme.primary.withOpacity(0.1), - borderRadius: BorderRadius.circular(28), - child: InkWell( - onTap: widget.disabled == true - ? null - : () => setState(() => generalEnabled = !generalEnabled), + content: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 400 + ), + child: Wrap( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Material( + color: Theme.of(context).colorScheme.primary.withOpacity(0.1), borderRadius: BorderRadius.circular(28), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 5 - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - AppLocalizations.of(context)!.enable, - style: TextStyle( - fontSize: 16, - color: widget.disabled == true - ? Colors.grey - : Theme.of(context).colorScheme.onSurface + child: InkWell( + onTap: widget.disabled == true + ? null + : () => setState(() => generalEnabled = !generalEnabled), + borderRadius: BorderRadius.circular(28), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 5 + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + AppLocalizations.of(context)!.enable, + style: TextStyle( + fontSize: 16, + color: widget.disabled == true + ? Colors.grey + : Theme.of(context).colorScheme.onSurface + ), ), - ), - Switch( - value: generalEnabled, - onChanged: widget.disabled == true - ? null - : (value) => setState(() => generalEnabled = value), - ) - ], + Switch( + value: generalEnabled, + onChanged: widget.disabled == true + ? null + : (value) => setState(() => generalEnabled = value), + ) + ], + ), ), ), ), ), - ), - const SizedBox(height: 4, width: double.maxFinite), - CustomCheckboxListTile( - value: bingEnabled, - onChanged: (value) => setState(() => bingEnabled = value), - title: "Bing", - disabled: widget.disabled || !generalEnabled, - padding: const EdgeInsets.symmetric( - horizontal: 36, - vertical: 4 + const SizedBox(height: 4, width: double.maxFinite), + CustomCheckboxListTile( + value: bingEnabled, + onChanged: (value) => setState(() => bingEnabled = value), + title: "Bing", + disabled: widget.disabled || !generalEnabled, + padding: const EdgeInsets.symmetric( + horizontal: 36, + vertical: 4 + ), ), - ), - CustomCheckboxListTile( - value: duckduckgoEnabled, - onChanged: (value) => setState(() => duckduckgoEnabled = value), - title: "DuckDuckGo", - disabled: widget.disabled || !generalEnabled, - padding: const EdgeInsets.symmetric( - horizontal: 36, - vertical: 4 + CustomCheckboxListTile( + value: duckduckgoEnabled, + onChanged: (value) => setState(() => duckduckgoEnabled = value), + title: "DuckDuckGo", + disabled: widget.disabled || !generalEnabled, + padding: const EdgeInsets.symmetric( + horizontal: 36, + vertical: 4 + ), ), - ), - CustomCheckboxListTile( - value: googleEnabled, - onChanged: (value) => setState(() => googleEnabled = value), - title: "Google", - disabled: widget.disabled || !generalEnabled, - padding: const EdgeInsets.symmetric( - horizontal: 36, - vertical: 4 + CustomCheckboxListTile( + value: googleEnabled, + onChanged: (value) => setState(() => googleEnabled = value), + title: "Google", + disabled: widget.disabled || !generalEnabled, + padding: const EdgeInsets.symmetric( + horizontal: 36, + vertical: 4 + ), ), - ), - CustomCheckboxListTile( - value: pixabayEnabled, - onChanged: (value) => setState(() => pixabayEnabled = value), - title: "Pixabay", - disabled: widget.disabled || !generalEnabled, - padding: const EdgeInsets.symmetric( - horizontal: 36, - vertical: 4 + CustomCheckboxListTile( + value: pixabayEnabled, + onChanged: (value) => setState(() => pixabayEnabled = value), + title: "Pixabay", + disabled: widget.disabled || !generalEnabled, + padding: const EdgeInsets.symmetric( + horizontal: 36, + vertical: 4 + ), ), - ), - CustomCheckboxListTile( - value: yandexEnabled, - onChanged: (value) => setState(() => yandexEnabled = value), - title: "Yandex", - disabled: widget.disabled || !generalEnabled, - padding: const EdgeInsets.symmetric( - horizontal: 36, - vertical: 4 + CustomCheckboxListTile( + value: yandexEnabled, + onChanged: (value) => setState(() => yandexEnabled = value), + title: "Yandex", + disabled: widget.disabled || !generalEnabled, + padding: const EdgeInsets.symmetric( + horizontal: 36, + vertical: 4 + ), ), - ), - CustomCheckboxListTile( - value: youtubeEnabled, - onChanged: (value) => setState(() => youtubeEnabled = value), - title: "YouTube", - disabled: widget.disabled || !generalEnabled, - padding: const EdgeInsets.symmetric( - horizontal: 36, - vertical: 4 + CustomCheckboxListTile( + value: youtubeEnabled, + onChanged: (value) => setState(() => youtubeEnabled = value), + title: "YouTube", + disabled: widget.disabled || !generalEnabled, + padding: const EdgeInsets.symmetric( + horizontal: 36, + vertical: 4 + ), ), - ), - ], + ], + ), ), actions: [ TextButton( diff --git a/lib/screens/clients/search_clients.dart b/lib/screens/clients/search_clients.dart index ce5d2bf..e22e238 100644 --- a/lib/screens/clients/search_clients.dart +++ b/lib/screens/clients/search_clients.dart @@ -1,5 +1,7 @@ // ignore_for_file: use_build_context_synchronously +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:animations/animations.dart'; import 'package:provider/provider.dart'; @@ -99,6 +101,8 @@ class _SearchClientsWidgetState extends State { final serversProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + void deleteClient(Client client) async { ProcessModal processModal = ProcessModal(context: context); processModal.open(AppLocalizations.of(context)!.removingClient); @@ -183,15 +187,31 @@ class _SearchClientsWidgetState extends State { } void openClientModal(Client client) { - Navigator.push(context, MaterialPageRoute( - fullscreenDialog: true, - builder: (BuildContext context) => ClientScreen( - onConfirm: confirmEditClient, - onDelete: deleteClient, - client: client, - serverVersion: serversProvider.serverStatus.data!.serverVersion, - ) - )); + if (width > 900 || !(Platform.isAndroid | Platform.isIOS)) { + showDialog( + barrierDismissible: false, + context: context, + builder: (BuildContext context) => ClientScreen( + onConfirm: confirmEditClient, + serverVersion: serversProvider.serverStatus.data!.serverVersion, + onDelete: deleteClient, + client: client, + dialog: true, + ) + ); + } + else { + Navigator.push(context, MaterialPageRoute( + fullscreenDialog: true, + builder: (BuildContext context) => ClientScreen( + onConfirm: confirmEditClient, + serverVersion: serversProvider.serverStatus.data!.serverVersion, + onDelete: deleteClient, + client: client, + dialog: false, + ) + )); + } } void openDeleteModal(Client client) { diff --git a/lib/screens/connect/connect.dart b/lib/screens/connect/connect.dart index ff55daf..0e19480 100644 --- a/lib/screens/connect/connect.dart +++ b/lib/screens/connect/connect.dart @@ -66,6 +66,7 @@ class _ConnectState extends State { controllers: expandableControllerList, onChange: expandOrContract, scrollController: scrollController, + breakingWidth: 700, ), AnimatedPositioned( duration: const Duration(milliseconds: 100), diff --git a/lib/screens/connect/fab.dart b/lib/screens/connect/fab.dart index 0c3204d..c20cafc 100644 --- a/lib/screens/connect/fab.dart +++ b/lib/screens/connect/fab.dart @@ -6,12 +6,27 @@ class FabConnect extends StatelessWidget { @override Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width; + void openAddServerModal() async { await Future.delayed(const Duration(seconds: 0), (() => { - Navigator.push(context, MaterialPageRoute( - fullscreenDialog: true, - builder: (BuildContext context) => const AddServerModal() - )) + if (width > 700) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => const AddServerModal( + window: true, + ), + ) + } + else { + Navigator.push(context, MaterialPageRoute( + fullscreenDialog: true, + builder: (BuildContext context) => const AddServerModal( + window: false, + ) + )) + } })); } diff --git a/lib/screens/filters/fab.dart b/lib/screens/filters/add_button.dart similarity index 78% rename from lib/screens/filters/fab.dart rename to lib/screens/filters/add_button.dart index 2a5d036..f2ba43e 100644 --- a/lib/screens/filters/fab.dart +++ b/lib/screens/filters/add_button.dart @@ -1,5 +1,7 @@ // ignore_for_file: use_build_context_synchronously +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -15,12 +17,14 @@ import 'package:adguard_home_manager/constants/enums.dart'; import 'package:adguard_home_manager/models/filtering.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart'; -class FiltersFab extends StatelessWidget { +class AddFiltersButton extends StatelessWidget { final String type; + final Widget Function(void Function()) widget; - const FiltersFab({ + const AddFiltersButton({ Key? key, required this.type, + required this.widget }) : super(key: key); @override @@ -28,6 +32,8 @@ class FiltersFab extends StatelessWidget { final serversProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + void confirmAddRule(String rule) async { ProcessModal processModal = ProcessModal(context: context); processModal.open(AppLocalizations.of(context)!.addingRule); @@ -64,14 +70,27 @@ class FiltersFab extends StatelessWidget { } void openAddCustomRule() { - Navigator.of(context).push( - MaterialPageRoute( - fullscreenDialog: true, + if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + context: context, builder: (context) => AddCustomRule( - onConfirm: confirmAddRule + onConfirm: confirmAddRule, + dialog: true, ), - ) - ); + barrierDismissible: false + ); + } + else { + Navigator.of(context).push( + MaterialPageRoute( + fullscreenDialog: true, + builder: (context) => AddCustomRule( + onConfirm: confirmAddRule, + dialog: false, + ), + ) + ); + } } void confirmAddList({required String name, required String url, required String type}) async { @@ -154,22 +173,34 @@ class FiltersFab extends StatelessWidget { } void openAddWhitelistBlacklist() { - showModalBottomSheet( - context: context, - builder: (ctx) => AddListModal( - type: type, - onConfirm: confirmAddList, - ), - isScrollControlled: true, - backgroundColor: Colors.transparent - ); + if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + context: context, + builder: (ctx) => AddListModal( + type: type, + onConfirm: confirmAddList, + dialog: true, + ), + ); + } + else { + showModalBottomSheet( + context: context, + builder: (ctx) => AddListModal( + type: type, + onConfirm: confirmAddList, + dialog: false, + ), + isScrollControlled: true, + backgroundColor: Colors.transparent + ); + } } - return FloatingActionButton( - onPressed: type == 'blacklist' || type == 'whitelist' + return widget( + type == 'blacklist' || type == 'whitelist' ? () => openAddWhitelistBlacklist() : () => openAddCustomRule(), - child: const Icon(Icons.add), ); } } \ No newline at end of file diff --git a/lib/screens/filters/add_custom_rule.dart b/lib/screens/filters/add_custom_rule.dart index 3f1e1f2..dfb5753 100644 --- a/lib/screens/filters/add_custom_rule.dart +++ b/lib/screens/filters/add_custom_rule.dart @@ -1,17 +1,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:provider/provider.dart'; -import 'package:flutter_web_browser/flutter_web_browser.dart'; +import 'package:adguard_home_manager/functions/open_url.dart'; import 'package:adguard_home_manager/constants/urls.dart'; -import 'package:adguard_home_manager/providers/app_config_provider.dart'; class AddCustomRule extends StatefulWidget { final void Function(String) onConfirm; + final bool dialog; const AddCustomRule({ Key? key, - required this.onConfirm + required this.onConfirm, + required this.dialog }) : super(key: key); @override @@ -72,293 +72,338 @@ class _AddCustomRuleState extends State { return rule; } - - void openDocsPage() { - FlutterWebBrowser.openWebPage( - url: Urls.customRuleDocs, - customTabsOptions: const CustomTabsOptions( - instantAppsEnabled: true, - showTitle: true, - urlBarHidingEnabled: false, - ), - safariVCOptions: const SafariViewControllerOptions( - barCollapsingEnabled: true, - dismissButtonStyle: SafariViewControllerDismissButtonStyle.close, - modalPresentationCapturesStatusBarAppearance: true, - ) - ); - } @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(AppLocalizations.of(context)!.addCustomRule), - actions: [ - IconButton( - onPressed: checkValidValues() == true - ? () { - Navigator.pop(context); - widget.onConfirm(buildRule()); - } - : null, - icon: const Icon(Icons.check) - ), - const SizedBox(width: 10) - ], - ), - body: ListView( - children: [ - const SizedBox(height: 24), - Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 5 - ), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary.withOpacity(0.1), - borderRadius: BorderRadius.circular(30), - border: Border.all( - color: Theme.of(context).colorScheme.primary - ) - ), - child: Text( - buildRule(), - textAlign: TextAlign.center, - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.w500 - ), + + List content() { + return [ + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 5 + ), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(30), + border: Border.all( + color: Theme.of(context).colorScheme.primary ) ), - ], - ), - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: TextFormField( - controller: domainController, - onChanged: (value) => setState(() => {}), - decoration: InputDecoration( - prefixIcon: const Icon(Icons.link_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) + child: Text( + buildRule(), + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.w500 ), - errorText: domainError, - labelText: AppLocalizations.of(context)!.domain, + ) + ), + ], + ), + Container(height: 30), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: TextFormField( + controller: domainController, + onChanged: (value) => setState(() => {}), + decoration: InputDecoration( + prefixIcon: const Icon(Icons.link_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) ), + errorText: domainError, + labelText: AppLocalizations.of(context)!.domain, ), ), - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: SegmentedButton( - segments: [ - ButtonSegment( - value: BlockingPresets.block, - label: Text(AppLocalizations.of(context)!.block) - ), - ButtonSegment( - value: BlockingPresets.unblock, - label: Text(AppLocalizations.of(context)!.unblock) - ), - ButtonSegment( - value: BlockingPresets.custom, - label: Text(AppLocalizations.of(context)!.custom) - ), - ], - selected: {preset}, - onSelectionChanged: (value) => setState(() => preset = value.first), - ), + ), + Container(height: 30), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: SegmentedButton( + segments: [ + ButtonSegment( + value: BlockingPresets.block, + label: Text(AppLocalizations.of(context)!.block) + ), + ButtonSegment( + value: BlockingPresets.unblock, + label: Text(AppLocalizations.of(context)!.unblock) + ), + ButtonSegment( + value: BlockingPresets.custom, + label: Text(AppLocalizations.of(context)!.custom) + ), + ], + selected: {preset}, + onSelectionChanged: (value) => setState(() => preset = value.first), ), - const SizedBox(height: 20), - Material( - color: Colors.transparent, - child: InkWell( - onTap: () => setState(() => addImportant = !addImportant), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 28), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only(left: 10), - child: Text( - AppLocalizations.of(context)!.addImportant, - style: TextStyle( - fontSize: 16, - color: Theme.of(context).colorScheme.onSurface - ), + ), + Container(height: 20), + Material( + color: Colors.transparent, + child: InkWell( + onTap: () => setState(() => addImportant = !addImportant), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 28), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 10), + child: Text( + AppLocalizations.of(context)!.addImportant, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface ), ), - Switch( - value: addImportant, - onChanged: (value) => setState(() => addImportant = value), - ) - ], - ), + ), + Switch( + value: addImportant, + onChanged: (value) => setState(() => addImportant = value), + ) + ], ), ), ), - const SizedBox(height: 20), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Card( - child: Padding( - padding: const EdgeInsets.all(20), - child: Column( + ), + Container(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Card( + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + Row( + children: [ + Icon( + Icons.info, + color: Theme.of(context).colorScheme.onSurface + ), + const SizedBox(width: 20), + Text( + AppLocalizations.of(context)!.examples, + style: TextStyle( + fontSize: 18, + color: Theme.of(context).colorScheme.onSurface + ), + ) + ], + ), + const SizedBox(height: 20), + SizedBox( + width: double.maxFinite, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "||example.org^", + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.primary + ), + ), + const SizedBox(height: 5), + Text( + AppLocalizations.of(context)!.example1, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.primary + ), + ), + const SizedBox(height: 20), + Text( + "@@||example.org^", + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.primary + ), + ), + const SizedBox(height: 5), + Text( + AppLocalizations.of(context)!.example2, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.primary + ), + ), + const SizedBox(height: 20), + Text( + "! Here goes a comment", + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.primary + ), + ), + Text( + "# Also a comment", + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.primary + ), + ), + const SizedBox(height: 5), + Text( + AppLocalizations.of(context)!.example3, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.primary + ), + ), + const SizedBox(height: 20), + Text( + "/REGEX/", + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.primary + ), + ), + const SizedBox(height: 5), + Text( + AppLocalizations.of(context)!.example4, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.primary + ), + ), + ], + ), + ) + ], + ), + ), + ), + ), + Container(height: 20), + Material( + color: Colors.transparent, + child: InkWell( + onTap: () => openUrl(Urls.customRuleDocs), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 10), + child: Text( + AppLocalizations.of(context)!.moreInformation, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface + ), + ), + ), + Padding( + padding: const EdgeInsets.only(right: 15), + child: Icon( + Icons.open_in_new, + color: Theme.of(context).colorScheme.onSurface + ), + ) + ], + ), + ), + ), + ), + Container(height: 20) + ]; + } + + if (widget.dialog == true) { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500 + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ - Icon( - Icons.info, - color: Theme.of(context).colorScheme.onSurface + IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.clear_rounded), + tooltip: AppLocalizations.of(context)!.close, ), - const SizedBox(width: 20), + const SizedBox(width: 8), Text( - AppLocalizations.of(context)!.examples, - style: TextStyle( - fontSize: 18, - color: Theme.of(context).colorScheme.onSurface + AppLocalizations.of(context)!.addCustomRule, + style: const TextStyle( + fontSize: 22 ), - ) + ), ], ), - const SizedBox(height: 20), - SizedBox( - width: double.maxFinite, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "||example.org^", - textAlign: TextAlign.left, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.primary - ), - ), - const SizedBox(height: 5), - Text( - AppLocalizations.of(context)!.example1, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.primary - ), - ), - const SizedBox(height: 20), - Text( - "@@||example.org^", - textAlign: TextAlign.left, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.primary - ), - ), - const SizedBox(height: 5), - Text( - AppLocalizations.of(context)!.example2, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.primary - ), - ), - const SizedBox(height: 20), - Text( - "! Here goes a comment", - textAlign: TextAlign.left, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.primary - ), - ), - Text( - "# Also a comment", - textAlign: TextAlign.left, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.primary - ), - ), - const SizedBox(height: 5), - Text( - AppLocalizations.of(context)!.example3, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.primary - ), - ), - const SizedBox(height: 20), - Text( - "/REGEX/", - textAlign: TextAlign.left, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.primary - ), - ), - const SizedBox(height: 5), - Text( - AppLocalizations.of(context)!.example4, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.primary - ), - ), - ], - ), + IconButton( + onPressed: checkValidValues() == true + ? () { + Navigator.pop(context); + widget.onConfirm(buildRule()); + } + : null, + icon: const Icon(Icons.check) ) ], ), ), - ), - ), - const SizedBox(height: 20), - Material( - color: Colors.transparent, - child: InkWell( - onTap: openDocsPage, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only(left: 10), - child: Text( - AppLocalizations.of(context)!.moreInformation, - style: TextStyle( - fontSize: 16, - color: Theme.of(context).colorScheme.onSurface - ), - ), - ), - Padding( - padding: const EdgeInsets.only(right: 15), - child: Icon( - Icons.open_in_new, - color: Theme.of(context).colorScheme.onSurface - ), - ) - ], + Flexible( + child: SingleChildScrollView( + child: Wrap( + alignment: WrapAlignment.center, + children: content(), + ), ), - ), - ), + ) + ], ), - const SizedBox(height: 20) - ], - ), - ); + ), + ); + } + else { + return Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context)!.addCustomRule), + actions: [ + IconButton( + onPressed: checkValidValues() == true + ? () { + Navigator.pop(context); + widget.onConfirm(buildRule()); + } + : null, + icon: const Icon(Icons.check) + ), + const SizedBox(width: 10) + ], + ), + body: ListView( + children: content(), + ) + ); + } } } \ No newline at end of file diff --git a/lib/screens/filters/add_list_modal.dart b/lib/screens/filters/add_list_modal.dart index 196cda1..fceebf9 100644 --- a/lib/screens/filters/add_list_modal.dart +++ b/lib/screens/filters/add_list_modal.dart @@ -10,6 +10,7 @@ class AddListModal extends StatefulWidget { final Filter? list; final void Function({required String name, required String url, required String type})? onConfirm; final void Function({required Filter list, required String type})? onEdit; + final bool dialog; const AddListModal({ Key? key, @@ -17,6 +18,7 @@ class AddListModal extends StatefulWidget { this.list, this.onConfirm, this.onEdit, + required this.dialog }) : super(key: key); @override @@ -68,51 +70,49 @@ class _AddListModalState extends State { @override Widget build(BuildContext context) { - return Padding( - padding: MediaQuery.of(context).viewInsets, - child: Container( - height: Platform.isIOS ? 386 : 370, - decoration: BoxDecoration( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28) - ), - color: Theme.of(context).dialogBackgroundColor - ), - child: Column( - children: [ - Expanded( - child: ListView( - physics: (Platform.isIOS ? 426 : 410) < MediaQuery.of(context).size.height - ? const NeverScrollableScrollPhysics() - : null, + Widget content() { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: SingleChildScrollView( + child: Wrap( children: [ - Padding( - padding: const EdgeInsets.only(top: 24), - child: Icon( - widget.type == 'whitelist' - ? Icons.verified_user_rounded - : Icons.gpp_bad_rounded, - size: 24, - color: Theme.of(context).listTileTheme.iconColor - ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 24), + child: Icon( + widget.type == 'whitelist' + ? Icons.verified_user_rounded + : Icons.gpp_bad_rounded, + size: 24, + color: Theme.of(context).listTileTheme.iconColor + ), + ), + const SizedBox(height: 16), + Text( + widget.list != null + ? widget.type == 'whitelist' + ? AppLocalizations.of(context)!.editWhitelist + : AppLocalizations.of(context)!.editBlacklist + : widget.type == 'whitelist' + ? AppLocalizations.of(context)!.addWhitelist + : AppLocalizations.of(context)!.addBlacklist, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + color: Theme.of(context).colorScheme.onSurface + ), + ), + const SizedBox(height: 16), + ], + ), + ], ), - const SizedBox(height: 16), - Text( - widget.list != null - ? widget.type == 'whitelist' - ? AppLocalizations.of(context)!.editWhitelist - : AppLocalizations.of(context)!.editBlacklist - : widget.type == 'whitelist' - ? AppLocalizations.of(context)!.addWhitelist - : AppLocalizations.of(context)!.addBlacklist, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSurface - ), - ), - const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: TextFormField( @@ -129,7 +129,7 @@ class _AddListModalState extends State { ), ), ), - const SizedBox(height: 30), + Container(height: 30), Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: TextFormField( @@ -151,54 +151,80 @@ class _AddListModalState extends State { ], ), ), - Padding( - padding: const EdgeInsets.all(24), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(AppLocalizations.of(context)!.cancel) - ), - const SizedBox(width: 20), - TextButton( - onPressed: () { - Navigator.pop(context); - if (widget.list != null) { - final Filter newList = Filter( - url: urlController.text, - name: nameController.text, - lastUpdated: widget.list!.lastUpdated, - id: widget.list!.id, - rulesCount: widget.list!.rulesCount, - enabled: widget.list!.enabled - ); - widget.onEdit!( - list: newList, - type: widget.type - ); - } - else { - widget.onConfirm!( - name: nameController.text, - url: urlController.text, - type: widget.type - ); - } - }, - child: Text( - widget.list != null - ? AppLocalizations.of(context)!.save - : AppLocalizations.of(context)!.confirm - ) - ), - ], - ), + ), + Padding( + padding: const EdgeInsets.all(24), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.cancel) + ), + const SizedBox(width: 20), + TextButton( + onPressed: () { + Navigator.pop(context); + if (widget.list != null) { + final Filter newList = Filter( + url: urlController.text, + name: nameController.text, + lastUpdated: widget.list!.lastUpdated, + id: widget.list!.id, + rulesCount: widget.list!.rulesCount, + enabled: widget.list!.enabled + ); + widget.onEdit!( + list: newList, + type: widget.type + ); + } + else { + widget.onConfirm!( + name: nameController.text, + url: urlController.text, + type: widget.type + ); + } + }, + child: Text( + widget.list != null + ? AppLocalizations.of(context)!.save + : AppLocalizations.of(context)!.confirm + ) + ), + ], ), - if (Platform.isIOS) const SizedBox(height: 16) - ], + ), + if (Platform.isIOS) const SizedBox(height: 16) + ], + ); + } + + if (widget.dialog == true) { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 400 + ), + child: content() ), - ), - ); + ); + } + else { + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28) + ), + color: Theme.of(context).dialogBackgroundColor + ), + child: content() + ), + ); + } } } \ No newline at end of file diff --git a/lib/screens/filters/blocked_services_screen.dart b/lib/screens/filters/blocked_services_screen.dart index fba09d2..2c0912e 100644 --- a/lib/screens/filters/blocked_services_screen.dart +++ b/lib/screens/filters/blocked_services_screen.dart @@ -12,7 +12,12 @@ import 'package:adguard_home_manager/providers/app_config_provider.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart'; class BlockedServicesScreen extends StatelessWidget { - const BlockedServicesScreen({Key? key}) : super(key: key); + final bool dialog; + + const BlockedServicesScreen({ + Key? key, + required this.dialog + }) : super(key: key); @override Widget build(BuildContext context) { @@ -21,7 +26,8 @@ class BlockedServicesScreen extends StatelessWidget { return BlockedServicesScreenWidget( serversProvider: serversProvider, - appConfigProvider: appConfigProvider + appConfigProvider: appConfigProvider, + dialog: dialog, ); } } @@ -29,11 +35,13 @@ class BlockedServicesScreen extends StatelessWidget { class BlockedServicesScreenWidget extends StatefulWidget { final ServersProvider serversProvider; final AppConfigProvider appConfigProvider; + final bool dialog; const BlockedServicesScreenWidget({ Key? key, required this.serversProvider, required this.appConfigProvider, + required this.dialog }) : super(key: key); @override @@ -209,24 +217,74 @@ class _BlockedServicesScreenStateWidget extends State Navigator.pop(context), + icon: const Icon(Icons.clear_rounded), + tooltip: AppLocalizations.of(context)!.close, + ), + const SizedBox(width: 8), + Text( + AppLocalizations.of(context)!.blockedServices, + style: const TextStyle( + fontSize: 22 + ), + ) + ], + ), + IconButton( + onPressed: updateBlockedServices, + icon: const Icon( + Icons.save_rounded + ), + tooltip: AppLocalizations.of(context)!.save, + ), + ], + ), + ), + Expanded( + child: body() + ), + ], + ) + ), + ); + } + else { + return Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context)!.blockedServices), + actions: [ + IconButton( + onPressed: updateBlockedServices, + icon: const Icon( + Icons.save_rounded + ), + tooltip: AppLocalizations.of(context)!.save, + ), + const SizedBox(width: 10) + ], + ), + body: RefreshIndicator( + onRefresh: loadBlockedServices, + child: body() + ), + ); + } } } \ No newline at end of file diff --git a/lib/screens/filters/check_host_modal.dart b/lib/screens/filters/check_host_modal.dart index 18f1f0f..6678ddb 100644 --- a/lib/screens/filters/check_host_modal.dart +++ b/lib/screens/filters/check_host_modal.dart @@ -10,7 +10,12 @@ import 'package:adguard_home_manager/services/http_requests.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart'; class CheckHostModal extends StatefulWidget { - const CheckHostModal({Key? key}) : super(key: key); + final bool dialog; + + const CheckHostModal({ + Key? key, + required this.dialog + }) : super(key: key); @override State createState() => _CheckHostModalState(); @@ -117,126 +122,141 @@ class _CheckHostModalState extends State { } } - return Padding( - padding: MediaQuery.of(context).viewInsets, - child: Container( - height: 330, - width: double.maxFinite, - decoration: BoxDecoration( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28), - ), - color: Theme.of(context).dialogBackgroundColor - ), - child: Center( - child: Column( - children: [ - Expanded( - child: ListView( - physics: 350 < MediaQuery.of(context).size.height - ? const NeverScrollableScrollPhysics() - : null, - children: [ - Padding( - padding: const EdgeInsets.only(top: 24), - child: Icon( - Icons.shield_rounded, - size: 24, - color: Theme.of(context).listTileTheme.iconColor - ), - ), - const SizedBox(height: 16), - Text( - AppLocalizations.of(context)!.checkHostFiltered, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSurface - ), - ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: TextFormField( - controller: domainController, - onChanged: validateDomain, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.link_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) + Widget content() { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: SingleChildScrollView( + child: Wrap( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 24), + child: Icon( + Icons.shield_rounded, + size: 24, + color: Theme.of(context).listTileTheme.iconColor + ), ), - errorText: domainError, - labelText: AppLocalizations.of(context)!.domain, + const SizedBox(height: 16), + Text( + AppLocalizations.of(context)!.checkHostFiltered, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + color: Theme.of(context).colorScheme.onSurface + ), + ), + const SizedBox(height: 16), + ], + ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: TextFormField( + controller: domainController, + onChanged: validateDomain, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.link_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + errorText: domainError, + labelText: AppLocalizations.of(context)!.domain, + ), + ), + ), + if (resultWidget != null) Padding( + padding: const EdgeInsets.all(24), + child: resultWidget, + ), + if (resultWidget == null) Padding( + padding: const EdgeInsets.all(24), + child: Center( + child: Text( + AppLocalizations.of(context)!.insertDomain, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 16, ), ), ), - if (resultWidget != null) Padding( - padding: const EdgeInsets.only( - top: 20, - left: 20, - right: 20 - ), - child: resultWidget, + ), + ], + ), + ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.only( + bottom: 24, + right: 24 + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.close), ), - if (resultWidget == null) Padding( - padding: const EdgeInsets.only( - top: 20, - left: 20, - right: 20 - ), - child: Center( - child: Text( - AppLocalizations.of(context)!.insertDomain, - style: const TextStyle( - fontSize: 16, - ), + const SizedBox(width: 20), + TextButton( + onPressed: domainController.text != '' && domainError == null + ? () => checkHost() + : null, + child: Text( + AppLocalizations.of(context)!.check, + style: TextStyle( + color: domainController.text != '' && domainError == null + ? Theme.of(context).colorScheme.primary + : Colors.grey ), ), ), ], ), - ), - Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Padding( - padding: const EdgeInsets.only( - bottom: 24, - right: 24 - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(AppLocalizations.of(context)!.close), - ), - const SizedBox(width: 20), - TextButton( - onPressed: domainController.text != '' && domainError == null - ? () => checkHost() - : null, - child: Text( - AppLocalizations.of(context)!.check, - style: TextStyle( - color: domainController.text != '' && domainError == null - ? Theme.of(context).colorScheme.primary - : Colors.grey - ), - ), - ), - ], - ), - ) - ], ) ], + ) + ], + ); + } + + if (widget.dialog == true) { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 400 ), + child: content() ), - ), - ); + ); + } + else { + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: Container( + width: double.maxFinite, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28), + ), + color: Theme.of(context).dialogBackgroundColor + ), + child: content() + ), + ); + } } } \ No newline at end of file diff --git a/lib/screens/filters/custom_rules_list.dart b/lib/screens/filters/custom_rules_list.dart index 41b1d0f..5a96716 100644 --- a/lib/screens/filters/custom_rules_list.dart +++ b/lib/screens/filters/custom_rules_list.dart @@ -2,33 +2,27 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:adguard_home_manager/screens/filters/fab.dart'; -import 'package:adguard_home_manager/screens/filters/remove_custom_rule_modal.dart'; +import 'package:adguard_home_manager/screens/filters/add_button.dart'; import 'package:adguard_home_manager/widgets/tab_content_list.dart'; -import 'package:adguard_home_manager/functions/snackbar.dart'; import 'package:adguard_home_manager/constants/enums.dart'; -import 'package:adguard_home_manager/models/filtering.dart'; -import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; -import 'package:adguard_home_manager/classes/process_modal.dart'; class CustomRulesList extends StatefulWidget { final LoadStatus loadStatus; final ScrollController scrollController; final List data; final Future Function() fetchData; + final void Function(String) onRemoveCustomRule; const CustomRulesList({ Key? key, required this.loadStatus, required this.scrollController, required this.data, - required this.fetchData + required this.fetchData, + required this.onRemoveCustomRule }) : super(key: key); @override @@ -61,52 +55,6 @@ class _CustomRulesListState extends State { @override Widget build(BuildContext context) { - final serversProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - - void removeCustomRule(String rule) async { - ProcessModal processModal = ProcessModal(context: context); - processModal.open(AppLocalizations.of(context)!.deletingRule); - - final List newRules = serversProvider.filtering.data!.userRules.where((r) => r != rule).toList(); - - final result = await setCustomRules(server: serversProvider.selectedServer!, rules: newRules); - - processModal.close(); - - if (result['result'] == 'success') { - FilteringData filteringData = serversProvider.filtering.data!; - filteringData.userRules = newRules; - serversProvider.setFilteringData(filteringData); - - showSnacbkar( - context: context, - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.ruleRemovedSuccessfully, - color: Colors.green - ); - } - else { - appConfigProvider.addLog(result['log']); - - showSnacbkar( - context: context, - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.ruleNotRemoved, - color: Colors.red - ); - } - } - - void openRemoveCustomRuleModal(String rule) { - showDialog( - context: context, - builder: (context) => RemoveCustomRule( - onConfirm: () => removeCustomRule(rule), - ) - ); - } - bool checkIfComment(String value) { final regex = RegExp(r'^(!|#).*$'); if (regex.hasMatch(value)) { @@ -184,7 +132,7 @@ class _CustomRulesListState extends State { ), subtitle: generateSubtitle(widget.data[index]), trailing: IconButton( - onPressed: () => openRemoveCustomRuleModal(widget.data[index]), + onPressed: () => widget.onRemoveCustomRule(widget.data[index]), icon: const Icon(Icons.delete) ), ), @@ -239,8 +187,12 @@ class _CustomRulesListState extends State { ), loadStatus: widget.loadStatus, onRefresh: widget.fetchData, - fab: const FiltersFab( + fab: AddFiltersButton( type: 'custom_rule', + widget: (fn) => FloatingActionButton( + onPressed: fn, + child: const Icon(Icons.add), + ), ), fabVisible: isVisible, ); diff --git a/lib/screens/filters/filter_list_tile.dart b/lib/screens/filters/filter_list_tile.dart deleted file mode 100644 index 1782d62..0000000 --- a/lib/screens/filters/filter_list_tile.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:flutter/material.dart'; - -class FilterListTile extends StatelessWidget { - final IconData icon; - final String title; - final String subtitle; - final Color? color; - final bool? bold; - - const FilterListTile({ - Key? key, - required this.icon, - required this.title, - required this.subtitle, - this.color, - this.bold, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Icon( - icon, - size: 24, - color: Theme.of(context).listTileTheme.iconColor, - ), - const SizedBox(width: 16), - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurface - ), - ), - const SizedBox(height: 3), - Text( - subtitle, - style: TextStyle( - fontSize: 14, - color: color ?? Theme.of(context).listTileTheme.textColor, - fontWeight: bold == true ? FontWeight.bold : FontWeight.w400 - ), - ), - ], - ), - ) - ], - ), - ); - } -} \ No newline at end of file diff --git a/lib/screens/filters/filters.dart b/lib/screens/filters/filters.dart index f985f48..607d791 100644 --- a/lib/screens/filters/filters.dart +++ b/lib/screens/filters/filters.dart @@ -1,18 +1,23 @@ // ignore_for_file: use_build_context_synchronously +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:adguard_home_manager/screens/filters/filters_list.dart'; import 'package:adguard_home_manager/screens/filters/check_host_modal.dart'; -import 'package:adguard_home_manager/screens/filters/custom_rules_list.dart'; +import 'package:adguard_home_manager/screens/filters/filters_tabs_view.dart'; +import 'package:adguard_home_manager/screens/filters/filters_triple_column.dart'; +import 'package:adguard_home_manager/screens/filters/list_details_screen.dart'; +import 'package:adguard_home_manager/screens/filters/remove_custom_rule_modal.dart'; import 'package:adguard_home_manager/screens/filters/blocked_services_screen.dart'; import 'package:adguard_home_manager/screens/filters/update_interval_lists_modal.dart'; import 'package:adguard_home_manager/functions/snackbar.dart'; import 'package:adguard_home_manager/classes/process_modal.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; +import 'package:adguard_home_manager/models/filtering.dart'; import 'package:adguard_home_manager/constants/enums.dart'; import 'package:adguard_home_manager/services/http_requests.dart'; import 'package:adguard_home_manager/models/clients.dart'; @@ -47,10 +52,7 @@ class FiltersWidget extends StatefulWidget { State createState() => _FiltersWidgetState(); } -class _FiltersWidgetState extends State with TickerProviderStateMixin { - late TabController tabController; - final ScrollController scrollController = ScrollController(); - +class _FiltersWidgetState extends State { Future fetchFilters() async { widget.serversProvider.setFilteringLoadStatus(LoadStatus.loading, false); @@ -68,20 +70,14 @@ class _FiltersWidgetState extends State with TickerProviderStateM } } + List generateClientsList(List clients, List ips) { + return clients.where((client) => ips.contains(client.ip)).toList(); + } + @override void initState() { fetchFilters(); super.initState(); - tabController = TabController( - initialIndex: 0, - length: 3, - vsync: this, - ); - tabController.addListener(() => widget.appConfigProvider.setSelectedFiltersTab(tabController.index)); - } - - List generateClientsList(List clients, List ips) { - return clients.where((client) => ips.contains(client.ip)).toList(); } @override @@ -89,6 +85,8 @@ class _FiltersWidgetState extends State with TickerProviderStateM final serversProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + void fetchUpdateLists() async { ProcessModal processModal = ProcessModal(context: context); processModal.open(AppLocalizations.of(context)!.updatingLists); @@ -139,12 +137,24 @@ class _FiltersWidgetState extends State with TickerProviderStateM void showCheckHostModal() { Future.delayed(const Duration(seconds: 0), () { - showModalBottomSheet( - context: context, - builder: (context) => const CheckHostModal(), - backgroundColor: Colors.transparent, - isScrollControlled: true, - ); + if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + context: context, + builder: (context) => const CheckHostModal( + dialog: true, + ), + ); + } + else { + showModalBottomSheet( + context: context, + builder: (context) => const CheckHostModal( + dialog: false, + ), + backgroundColor: Colors.transparent, + isScrollControlled: true, + ); + } }); } @@ -216,169 +226,222 @@ class _FiltersWidgetState extends State with TickerProviderStateM void openBlockedServicesModal() { Future.delayed(const Duration(seconds: 0), () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const BlockedServicesScreen(), - ) - ); + if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + context: context, + builder: (context) => const BlockedServicesScreen( + dialog: true, + ), + barrierDismissible: false + ); + } + else { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const BlockedServicesScreen( + dialog: false, + ), + ) + ); + } }); } - return DefaultTabController( - length: 3, - child: NestedScrollView( - controller: scrollController, - headerSliverBuilder: ((context, innerBoxIsScrolled) { - return [ - SliverOverlapAbsorber( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - sliver: SliverAppBar( - title: Text(AppLocalizations.of(context)!.filters), - pinned: true, - floating: true, - forceElevated: innerBoxIsScrolled, - centerTitle: false, - actions: serversProvider.filtering.loadStatus == LoadStatus.loaded ? [ - IconButton( - onPressed: enableDisableFiltering, - tooltip: serversProvider.filtering.data!.enabled == true - ? AppLocalizations.of(context)!.disableFiltering - : AppLocalizations.of(context)!.enableFiltering, - icon: Stack( - children: [ - const Icon(Icons.power_settings_new_rounded), - Positioned( - bottom: 0, - right: 0, - child: Stack( - children: [ - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30), - color: Colors.white - ), - child: Icon( - serversProvider.filtering.data!.enabled == true - ? Icons.check_circle_rounded - : Icons.cancel, - size: 12, - color: serversProvider.filtering.data!.enabled == true - ? appConfigProvider.useThemeColorForStatus == true - ? Theme.of(context).colorScheme.primary - : Colors.green - : appConfigProvider.useThemeColorForStatus == true - ? Colors.grey - : Colors.red - ), - ), - ], - ), - ) - ], - ) - ), - IconButton( - onPressed: () { - showModalBottomSheet( - context: context, - builder: (context) => UpdateIntervalListsModal( - interval: serversProvider.filtering.data!.interval, - onChange: setUpdateFrequency + void removeCustomRule(String rule) async { + ProcessModal processModal = ProcessModal(context: context); + processModal.open(AppLocalizations.of(context)!.deletingRule); + + final List newRules = serversProvider.filtering.data!.userRules.where((r) => r != rule).toList(); + + final result = await setCustomRules(server: serversProvider.selectedServer!, rules: newRules); + + processModal.close(); + + if (result['result'] == 'success') { + FilteringData filteringData = serversProvider.filtering.data!; + filteringData.userRules = newRules; + serversProvider.setFilteringData(filteringData); + + showSnacbkar( + context: context, + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.ruleRemovedSuccessfully, + color: Colors.green + ); + } + else { + appConfigProvider.addLog(result['log']); + + showSnacbkar( + context: context, + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.ruleNotRemoved, + color: Colors.red + ); + } + } + + void openRemoveCustomRuleModal(String rule) { + showDialog( + context: context, + builder: (context) => RemoveCustomRule( + onConfirm: () => removeCustomRule(rule), + ) + ); + } + + void openListDetails(Filter filter, String type) { + if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + context: context, + builder: (context) => ListDetailsScreen( + list: filter, + type: type, + dialog: true, + ), + barrierDismissible: false + ); + } + else { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => ListDetailsScreen( + list: filter, + type: type, + dialog: false, + ) + ) + ); + } + } + + List actions() { + if (serversProvider.filtering.loadStatus == LoadStatus.loaded) { + return [ + IconButton( + onPressed: enableDisableFiltering, + tooltip: serversProvider.filtering.data!.enabled == true + ? AppLocalizations.of(context)!.disableFiltering + : AppLocalizations.of(context)!.enableFiltering, + icon: Stack( + children: [ + const Icon(Icons.power_settings_new_rounded), + Positioned( + bottom: 0, + right: 0, + child: Stack( + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + color: Colors.white ), - backgroundColor: Colors.transparent, - isScrollControlled: true - ); - }, - icon: const Icon(Icons.update_rounded) + child: Icon( + serversProvider.filtering.data!.enabled == true + ? Icons.check_circle_rounded + : Icons.cancel, + size: 12, + color: serversProvider.filtering.data!.enabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red + ), + ), + ], ), - PopupMenuButton( - itemBuilder: (context) => [ - PopupMenuItem( - onTap: fetchUpdateLists, - child: Row( - children: [ - const Icon(Icons.sync_rounded), - const SizedBox(width: 10), - Text(AppLocalizations.of(context)!.updateLists) - ], - ) - ), - PopupMenuItem( - onTap: openBlockedServicesModal, - child: Row( - children: [ - const Icon(Icons.block), - const SizedBox(width: 10), - Text(AppLocalizations.of(context)!.blockedServices) - ], - ) - ), - PopupMenuItem( - onTap: showCheckHostModal, - child: Row( - children: [ - const Icon(Icons.shield_rounded), - const SizedBox(width: 10), - Text(AppLocalizations.of(context)!.checkHostFiltered) - ], - ) - ), - ] + ) + ], + ) + ), + IconButton( + onPressed: () { + if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + context: context, + builder: (context) => UpdateIntervalListsModal( + interval: serversProvider.filtering.data!.interval, + onChange: setUpdateFrequency, + dialog: true, ), - const SizedBox(width: 5), - ] : [], - bottom: TabBar( - controller: tabController, - isScrollable: false, - unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant, - tabs: [ - Tab( - icon: const Icon(Icons.verified_user_rounded), - text: AppLocalizations.of(context)!.whitelists, - ), - Tab( - icon: const Icon(Icons.gpp_bad_rounded), - text: AppLocalizations.of(context)!.blacklist, - ), - Tab( - icon: const Icon(Icons.shield_rounded), - text: AppLocalizations.of(context)!.customRules, - ), - ] + ); + } + else { + showModalBottomSheet( + context: context, + builder: (context) => UpdateIntervalListsModal( + interval: serversProvider.filtering.data!.interval, + onChange: setUpdateFrequency, + dialog: false, + ), + backgroundColor: Colors.transparent, + isScrollControlled: true + ); + } + }, + icon: const Icon(Icons.update_rounded), + tooltip: AppLocalizations.of(context)!.updateFrequency, + ), + PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + onTap: fetchUpdateLists, + child: Row( + children: [ + const Icon(Icons.sync_rounded), + const SizedBox(width: 10), + Text(AppLocalizations.of(context)!.updateLists) + ], ) ), - ) - ]; - }), - body: TabBarView( - controller: tabController, - children: [ - FiltersList( - loadStatus: serversProvider.filtering.loadStatus, - scrollController: scrollController, - type: 'whitelist', - data: serversProvider.filtering.loadStatus == LoadStatus.loaded - ? serversProvider.filtering.data!.whitelistFilters : [], - fetchData: fetchFilters, - ), - FiltersList( - loadStatus: serversProvider.filtering.loadStatus, - scrollController: scrollController, - type: 'blacklist', - data: serversProvider.filtering.loadStatus == LoadStatus.loaded - ? serversProvider.filtering.data!.filters : [], - fetchData: fetchFilters, - ), - CustomRulesList( - loadStatus: serversProvider.filtering.loadStatus, - scrollController: scrollController, - data: serversProvider.filtering.loadStatus == LoadStatus.loaded - ? serversProvider.filtering.data!.userRules : [], - fetchData: fetchFilters, - ), - ] - ) - ) - ); + PopupMenuItem( + onTap: openBlockedServicesModal, + child: Row( + children: [ + const Icon(Icons.block), + const SizedBox(width: 10), + Text(AppLocalizations.of(context)!.blockedServices) + ], + ) + ), + PopupMenuItem( + onTap: showCheckHostModal, + child: Row( + children: [ + const Icon(Icons.shield_rounded), + const SizedBox(width: 10), + Text(AppLocalizations.of(context)!.checkHostFiltered) + ], + ) + ), + ] + ), + const SizedBox(width: 5), + ]; + } + else { + return []; + } + } + + if (width > 1200) { + return FiltersTripleColumn( + onRemoveCustomRule: openRemoveCustomRuleModal, + onOpenDetailsModal: openListDetails, + actions: actions(), + refreshData: fetchFilters, + ); + } + else { + return FiltersTabsView( + appConfigProvider: appConfigProvider, + fetchFilters: fetchFilters, + actions: actions(), + onRemoveCustomRule: openRemoveCustomRuleModal, + onOpenDetailsModal: openListDetails, + ); + } } } \ No newline at end of file diff --git a/lib/screens/filters/filters_list.dart b/lib/screens/filters/filters_list.dart index effe42d..be4ff75 100644 --- a/lib/screens/filters/filters_list.dart +++ b/lib/screens/filters/filters_list.dart @@ -7,8 +7,7 @@ import 'package:provider/provider.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:adguard_home_manager/screens/filters/fab.dart'; -import 'package:adguard_home_manager/screens/filters/list_details_screen.dart'; +import 'package:adguard_home_manager/screens/filters/add_button.dart'; import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; import 'package:adguard_home_manager/widgets/tab_content_list.dart'; @@ -23,6 +22,7 @@ class FiltersList extends StatefulWidget { final List data; final Future Function() fetchData; final String type; + final void Function(Filter, String) onOpenDetailsScreen; const FiltersList({ Key? key, @@ -31,6 +31,7 @@ class FiltersList extends StatefulWidget { required this.data, required this.fetchData, required this.type, + required this.onOpenDetailsScreen }) : super(key: key); @override @@ -64,17 +65,6 @@ class _FiltersListState extends State { @override Widget build(BuildContext context) { final appConfigProvider = Provider.of(context); - - void openDetailsModal(Filter filter) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => ListDetailsScreen( - list: filter, - type: widget.type, - ) - ) - ); - } return CustomTabContentList( loadingGenerator: () => SizedBox( @@ -112,7 +102,7 @@ class _FiltersListState extends State { ? Colors.grey : Colors.red ), - onTap: () => openDetailsModal(widget.data[index]), + onTap: () => widget.onOpenDetailsScreen(widget.data[index], widget.type), ), noData: Container( width: double.maxFinite, @@ -166,8 +156,12 @@ class _FiltersListState extends State { ), loadStatus: widget.loadStatus, onRefresh: widget.fetchData, - fab: FiltersFab( + fab: AddFiltersButton( type: widget.type, + widget: (fn) => FloatingActionButton( + onPressed: fn, + child: const Icon(Icons.add), + ), ), fabVisible: isVisible, ); diff --git a/lib/screens/filters/filters_tabs_view.dart b/lib/screens/filters/filters_tabs_view.dart new file mode 100644 index 0000000..17f74a9 --- /dev/null +++ b/lib/screens/filters/filters_tabs_view.dart @@ -0,0 +1,143 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/screens/filters/custom_rules_list.dart'; +import 'package:adguard_home_manager/screens/filters/filters_list.dart'; + +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/models/filtering.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; + +class FiltersTabsView extends StatefulWidget { + final AppConfigProvider appConfigProvider; + final Future Function() fetchFilters; + final List actions; + final void Function(String) onRemoveCustomRule; + final void Function(Filter, String) onOpenDetailsModal; + + const FiltersTabsView({ + Key? key, + required this.appConfigProvider, + required this.fetchFilters, + required this.actions, + required this.onOpenDetailsModal, + required this.onRemoveCustomRule + }) : super(key: key); + + @override + State createState() => _FiltersTabsViewState(); +} + +class _FiltersTabsViewState extends State with TickerProviderStateMixin { + late TabController tabController; + final ScrollController scrollController = ScrollController(); + + @override + void initState() { + widget.fetchFilters(); + super.initState(); + tabController = TabController( + initialIndex: 0, + length: 3, + vsync: this, + ); + tabController.addListener(() => widget.appConfigProvider.setSelectedFiltersTab(tabController.index)); + } + + @override + Widget build(BuildContext context) { + final serversProvider = Provider.of(context); + + return DefaultTabController( + length: 3, + child: NestedScrollView( + controller: scrollController, + headerSliverBuilder: ((context, innerBoxIsScrolled) { + return [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverAppBar( + title: Text(AppLocalizations.of(context)!.filters), + pinned: true, + floating: true, + forceElevated: innerBoxIsScrolled, + centerTitle: false, + actions: widget.actions, + bottom: TabBar( + controller: tabController, + isScrollable: true, + unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant, + tabs: [ + Tab( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.verified_user_rounded), + const SizedBox(width: 8), + Text(AppLocalizations.of(context)!.whitelists,) + ], + ), + ), + Tab( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.gpp_bad_rounded), + const SizedBox(width: 8), + Text(AppLocalizations.of(context)!.blacklists) + ], + ), + ), + Tab( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.shield_rounded), + const SizedBox(width: 8), + Text(AppLocalizations.of(context)!.customRules) + ], + ), + ), + ] + ) + ), + ) + ]; + }), + body: TabBarView( + controller: tabController, + children: [ + FiltersList( + loadStatus: serversProvider.filtering.loadStatus, + scrollController: scrollController, + type: 'whitelist', + data: serversProvider.filtering.loadStatus == LoadStatus.loaded + ? serversProvider.filtering.data!.whitelistFilters : [], + fetchData: widget.fetchFilters, + onOpenDetailsScreen: widget.onOpenDetailsModal, + ), + FiltersList( + loadStatus: serversProvider.filtering.loadStatus, + scrollController: scrollController, + type: 'blacklist', + data: serversProvider.filtering.loadStatus == LoadStatus.loaded + ? serversProvider.filtering.data!.filters : [], + fetchData: widget.fetchFilters, + onOpenDetailsScreen: widget.onOpenDetailsModal, + ), + CustomRulesList( + loadStatus: serversProvider.filtering.loadStatus, + scrollController: scrollController, + data: serversProvider.filtering.loadStatus == LoadStatus.loaded + ? serversProvider.filtering.data!.userRules : [], + fetchData: widget.fetchFilters, + onRemoveCustomRule: widget.onRemoveCustomRule, + ), + ] + ) + ) + ); + } +} \ No newline at end of file diff --git a/lib/screens/filters/filters_triple_column.dart b/lib/screens/filters/filters_triple_column.dart new file mode 100644 index 0000000..3e82fc5 --- /dev/null +++ b/lib/screens/filters/filters_triple_column.dart @@ -0,0 +1,311 @@ +import 'dart:io'; + +import 'package:adguard_home_manager/screens/filters/add_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:provider/provider.dart'; + +import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; + +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/models/filtering.dart'; +import 'package:adguard_home_manager/functions/number_format.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; + +class FiltersTripleColumn extends StatelessWidget { + final void Function(String) onRemoveCustomRule; + final void Function(Filter, String) onOpenDetailsModal; + final List actions; + final Future Function() refreshData; + + const FiltersTripleColumn({ + Key? key, + required this.onRemoveCustomRule, + required this.onOpenDetailsModal, + required this.actions, + required this.refreshData + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final serversProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + + bool checkIfComment(String value) { + final regex = RegExp(r'^(!|#).*$'); + if (regex.hasMatch(value)) { + return true; + } + else { + return false; + } + } + + Widget? generateSubtitle(String rule) { + final allowRegex = RegExp(r'^@@.*$'); + final blockRegex = RegExp(r'^\|\|.*$'); + final commentRegex = RegExp(r'^(#|!).*$'); + + if (allowRegex.hasMatch(rule)) { + return Text( + AppLocalizations.of(context)!.allowed, + style: const TextStyle( + color: Colors.green + ), + ); + } + else if (blockRegex.hasMatch(rule)) { + return Text( + AppLocalizations.of(context)!.blocked, + style: const TextStyle( + color: Colors.red + ), + ); + } + else if (commentRegex.hasMatch(rule)) { + return Text( + AppLocalizations.of(context)!.comment, + style: const TextStyle( + color: Colors.grey + ), + ); + } + else { + return null; + } + } + + Widget content() { + switch (serversProvider.filtering.loadStatus) { + case LoadStatus.loading: + return Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.loadingFilters, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ], + ); + + case LoadStatus.loaded: + return Row( + children: [ + Expanded( + flex: 1, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + AppLocalizations.of(context)!.whitelists, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500 + ), + ), + AddFiltersButton( + type: 'whitelist', + widget: (fn) => IconButton( + onPressed: fn, + icon: const Icon(Icons.add_rounded) + ) + ) + ], + ), + ), + Expanded( + child: ListView.builder( + itemCount: serversProvider.filtering.data!.whitelistFilters.length, + itemBuilder: (context, index) => CustomListTile( + title: serversProvider.filtering.data!.whitelistFilters[index].name, + subtitle: "${intFormat(serversProvider.filtering.data!.whitelistFilters[index].rulesCount, Platform.localeName)} ${AppLocalizations.of(context)!.enabledRules}", + trailing: Icon( + serversProvider.filtering.data!.whitelistFilters[index].enabled == true + ? Icons.check_circle_rounded + : Icons.cancel, + color: serversProvider.filtering.data!.whitelistFilters[index].enabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red + ), + onTap: () => onOpenDetailsModal(serversProvider.filtering.data!.whitelistFilters[index], 'whitelist'), + ), + ), + ) + ], + ), + ), + Expanded( + flex: 1, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + AppLocalizations.of(context)!.blacklists, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500 + ), + ), + AddFiltersButton( + type: 'blacklist', + widget: (fn) => IconButton( + onPressed: fn, + icon: const Icon(Icons.add_rounded) + ) + ) + ], + ), + ), + Expanded( + child: ListView.builder( + itemCount: serversProvider.filtering.data!.filters.length, + itemBuilder: (context, index) => CustomListTile( + title: serversProvider.filtering.data!.filters[index].name, + subtitle: "${intFormat(serversProvider.filtering.data!.filters[index].rulesCount, Platform.localeName)} ${AppLocalizations.of(context)!.enabledRules}", + trailing: Icon( + serversProvider.filtering.data!.filters[index].enabled == true + ? Icons.check_circle_rounded + : Icons.cancel, + color: serversProvider.filtering.data!.filters[index].enabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red + ), + onTap: () => onOpenDetailsModal(serversProvider.filtering.data!.filters[index], 'blacklist'), + ), + ), + ) + ], + ), + ), + Expanded( + flex: 1, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + AppLocalizations.of(context)!.customRules, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500 + ), + ), + AddFiltersButton( + type: '', + widget: (fn) => IconButton( + onPressed: fn, + icon: const Icon(Icons.add_rounded) + ) + ) + ], + ), + ), + Expanded( + child: ListView.builder( + itemCount: serversProvider.filtering.data!.userRules.length, + itemBuilder: (context, index) => ListTile( + title: Text( + serversProvider.filtering.data!.userRules[index], + style: TextStyle( + color: checkIfComment(serversProvider.filtering.data!.userRules[index]) == true + ? Theme.of(context).colorScheme.onSurface.withOpacity(0.6) + : Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.normal, + ), + ), + subtitle: generateSubtitle(serversProvider.filtering.data!.userRules[index]), + trailing: IconButton( + onPressed: () => onRemoveCustomRule(serversProvider.filtering.data!.userRules[index]), + icon: const Icon(Icons.delete) + ), + ), + ), + ) + ], + ), + ), + ], + ); + + case LoadStatus.error: + return SizedBox.expand( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon( + Icons.error, + color: Colors.red, + size: 50, + ), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.filtersNotLoaded, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ], + ), + ); + + default: + return const SizedBox(); + } + } + + return Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context)!.filters), + actions: [ + IconButton( + onPressed: refreshData, + icon: const Icon(Icons.refresh_rounded), + tooltip: AppLocalizations.of(context)!.refresh, + ), + ...actions + ], + ), + body: content(), + ); + } +} \ No newline at end of file diff --git a/lib/screens/filters/list_details_screen.dart b/lib/screens/filters/list_details_screen.dart index 7d8cf53..92ae4e0 100644 --- a/lib/screens/filters/list_details_screen.dart +++ b/lib/screens/filters/list_details_screen.dart @@ -7,9 +7,9 @@ import 'package:flutter/rendering.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:adguard_home_manager/screens/filters/filter_list_tile.dart'; import 'package:adguard_home_manager/screens/filters/add_list_modal.dart'; import 'package:adguard_home_manager/screens/filters/delete_list_modal.dart'; +import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; import 'package:adguard_home_manager/functions/format_time.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; @@ -23,11 +23,13 @@ import 'package:adguard_home_manager/models/filtering.dart'; class ListDetailsScreen extends StatefulWidget { final Filter list; final String type; + final bool dialog; const ListDetailsScreen({ Key? key, required this.list, required this.type, + required this.dialog }) : super(key: key); @override @@ -68,6 +70,8 @@ class _ListDetailsScreenState extends State { final serversProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + void enableDisableList(Filter list, bool newStatus) async { ProcessModal processModal = ProcessModal(context: context); processModal.open( @@ -216,108 +220,234 @@ class _ListDetailsScreenState extends State { } } - return Scaffold( - appBar: AppBar( - title: Text(AppLocalizations.of(context)!.listDetails), - actions: [ - IconButton( - onPressed: () => { + List content() { + return [ + CustomListTile( + icon: Icons.shield_rounded, + title: AppLocalizations.of(context)!.currentStatus, + subtitleWidget: Text( + enabled == true + ? AppLocalizations.of(context)!.enabled + : AppLocalizations.of(context)!.disabled, + style: TextStyle( + color: enabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red, + fontWeight: FontWeight.w500 + ), + ), + padding: widget.dialog == true + ? const EdgeInsets.symmetric( + horizontal: 24, + vertical: 8 + ) + : null, + ), + CustomListTile( + icon: Icons.badge_rounded, + title: AppLocalizations.of(context)!.name, + subtitle: name, + padding: widget.dialog == true + ? const EdgeInsets.symmetric( + horizontal: 24, + vertical: 8 + ) + : null, + ), + CustomListTile( + icon: Icons.link_rounded, + title: "URL", + subtitle: widget.list.url, + padding: widget.dialog == true + ? const EdgeInsets.symmetric( + horizontal: 24, + vertical: 8 + ) + : null, + ), + CustomListTile( + icon: Icons.list_rounded, + title: AppLocalizations.of(context)!.rules, + subtitle: widget.list.rulesCount.toString(), + padding: widget.dialog == true + ? const EdgeInsets.symmetric( + horizontal: 24, + vertical: 8 + ) + : null, + ), + CustomListTile( + icon: Icons.shield_rounded, + title: AppLocalizations.of(context)!.listType, + subtitle: widget.type == 'whitelist' + ? AppLocalizations.of(context)!.whitelist + : AppLocalizations.of(context)!.blacklist, + padding: widget.dialog == true + ? const EdgeInsets.symmetric( + horizontal: 24, + vertical: 8 + ) + : null, + ), + if (widget.list.lastUpdated != null) CustomListTile( + icon: Icons.schedule_rounded, + title: AppLocalizations.of(context)!.latestUpdate, + subtitle: convertTimestampLocalTimezone(widget.list.lastUpdated!, 'dd-MM-yyyy HH:mm'), + padding: widget.dialog == true + ? const EdgeInsets.symmetric( + horizontal: 24, + vertical: 8 + ) + : null, + ), + if (widget.dialog == true) Container(height: 16) + ]; + } + + List actions() { + return [ + IconButton( + onPressed: () => { + if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + context: context, + builder: (ctx) => AddListModal( + list: widget.list, + type: widget.type, + onEdit: confirmEditList, + dialog: true, + ), + ) + } + else { showModalBottomSheet( context: context, builder: (ctx) => AddListModal( list: widget.list, type: widget.type, - onEdit: confirmEditList + onEdit: confirmEditList, + dialog: false, ), isScrollControlled: true, backgroundColor: Colors.transparent ) - }, - icon: const Icon(Icons.edit), - tooltip: AppLocalizations.of(context)!.edit, + } + }, + icon: const Icon(Icons.edit), + tooltip: AppLocalizations.of(context)!.edit, + ), + IconButton( + onPressed: () { + showDialog( + context: context, + builder: (context) => DeleteListModal( + onConfirm: () => deleteList(widget.list, widget.type), + ) + ); + }, + icon: const Icon(Icons.delete), + tooltip: AppLocalizations.of(context)!.delete, + ), + const SizedBox(width: 10), + ]; + } + + if (widget.dialog == true) { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500 ), - IconButton( - onPressed: () { - showDialog( - context: context, - builder: (context) => DeleteListModal( - onConfirm: () => deleteList(widget.list, widget.type), - ) - ); - }, - icon: const Icon(Icons.delete), - tooltip: AppLocalizations.of(context)!.delete, - ), - const SizedBox(width: 10), - ], - ), - body: Stack( - children: [ - ListView( + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - FilterListTile( - icon: Icons.shield_rounded, - title: AppLocalizations.of(context)!.currentStatus, - subtitle: enabled == true - ? AppLocalizations.of(context)!.enabled - : AppLocalizations.of(context)!.disabled, - color: enabled == true - ? appConfigProvider.useThemeColorForStatus == true - ? Theme.of(context).colorScheme.primary - : Colors.green - : appConfigProvider.useThemeColorForStatus == true - ? Colors.grey - : Colors.red, - bold: true, - ), - FilterListTile( - icon: Icons.badge_rounded, - title: AppLocalizations.of(context)!.name, - subtitle: name - ), - FilterListTile( - icon: Icons.link_rounded, - title: "URL", - subtitle: widget.list.url - ), - FilterListTile( - icon: Icons.list_rounded, - title: AppLocalizations.of(context)!.rules, - subtitle: widget.list.rulesCount.toString() - ), - FilterListTile( - icon: Icons.shield_rounded, - title: AppLocalizations.of(context)!.listType, - subtitle: widget.type == 'whitelist' - ? AppLocalizations.of(context)!.whitelist - : AppLocalizations.of(context)!.blacklist, - ), - if (widget.list.lastUpdated != null) FilterListTile( - icon: Icons.schedule_rounded, - title: AppLocalizations.of(context)!.latestUpdate, - subtitle: convertTimestampLocalTimezone(widget.list.lastUpdated!, 'dd-MM-yyyy HH:mm'), + Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.clear_rounded), + tooltip: AppLocalizations.of(context)!.close, + ), + const SizedBox(width: 8), + Text( + AppLocalizations.of(context)!.listDetails, + style: const TextStyle( + fontSize: 22 + ), + ) + ], + ), + Row( + children: [ + IconButton( + onPressed: () => enableDisableList(widget.list, !enabled), + icon: Icon( + enabled == true + ? Icons.gpp_bad_rounded + : Icons.verified_user_rounded, + ), + tooltip: enabled == true + ? AppLocalizations.of(context)!.disableList + : AppLocalizations.of(context)!.enableList, + ), + ...actions() + ], + ) + ], + ), ), + Flexible( + child: SingleChildScrollView( + child: Wrap( + children: content(), + ), + ) + ) ], ), - AnimatedPositioned( - duration: const Duration(milliseconds: 100), - curve: Curves.easeInOut, - bottom: fabVisible ? - appConfigProvider.showingSnackbar - ? 70 : (Platform.isIOS ? 40 : 20) - : -70, - right: 20, - child: FloatingActionButton( - onPressed: () => enableDisableList(widget.list, !enabled), - child: Icon( - enabled == true - ? Icons.gpp_bad_rounded - : Icons.verified_user_rounded, - ), + ) + ); + } + else { + return Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context)!.listDetails), + actions: actions(), + ), + body: Stack( + children: [ + ListView( + children: content(), ), - ) - ], - ), - ); + AnimatedPositioned( + duration: const Duration(milliseconds: 100), + curve: Curves.easeInOut, + bottom: fabVisible ? + appConfigProvider.showingSnackbar + ? 70 : (Platform.isIOS ? 40 : 20) + : -70, + right: 20, + child: FloatingActionButton( + onPressed: () => enableDisableList(widget.list, !enabled), + child: Icon( + enabled == true + ? Icons.gpp_bad_rounded + : Icons.verified_user_rounded, + ), + ), + ) + ], + ), + ); + } } } \ No newline at end of file diff --git a/lib/screens/filters/update_interval_lists_modal.dart b/lib/screens/filters/update_interval_lists_modal.dart index 1779bf9..c168793 100644 --- a/lib/screens/filters/update_interval_lists_modal.dart +++ b/lib/screens/filters/update_interval_lists_modal.dart @@ -9,11 +9,13 @@ import 'package:adguard_home_manager/widgets/option_box.dart'; class UpdateIntervalListsModal extends StatefulWidget { final int interval; final void Function(int) onChange; + final bool dialog; const UpdateIntervalListsModal({ Key? key, required this.interval, required this.onChange, + required this.dialog }) : super(key: key); @override @@ -37,272 +39,266 @@ class _UpdateIntervalListsModalState extends State { @override Widget build(BuildContext context) { - final MediaQueryData mediaQueryData = MediaQuery.of(context); + final MediaQueryData mediaQueryData = MediaQuery.of(context); - return Padding( - padding: mediaQueryData.viewInsets, - child: Container( - height: Platform.isIOS ? 406 : 390, - decoration: BoxDecoration( - color: Theme.of(context).dialogBackgroundColor, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28) - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: ListView( - physics: (Platform.isIOS ? 426 : 410) < MediaQuery.of(context).size.height - ? const NeverScrollableScrollPhysics() - : null, + Widget content() { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: SingleChildScrollView( + child: Wrap( children: [ - Padding( - padding: const EdgeInsets.only(top: 24), - child: Icon( - Icons.update_rounded, - size: 24, - color: Theme.of(context).listTileTheme.iconColor - ), - ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 16 - ), - width: double.maxFinite, - child: Text( - AppLocalizations.of(context)!.updateFrequency, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSurface - ), - ), - ), - SizedBox( - width: double.maxFinite, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Column( + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Container( - width: (mediaQueryData.size.width-70)/2, - margin: const EdgeInsets.only( - top: 10, - right: 5, - bottom: 5 - ), - child: OptionBox( - optionsValue: selectedOption, - itemValue: 0, - onTap: _updateRadioValue, - child: Center( - child: AnimatedDefaultTextStyle( - duration: const Duration(milliseconds: 250), - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - color: selectedOption == 0 - ? Theme.of(context).colorScheme.onInverseSurface - : Theme.of(context).colorScheme.onSurface - ), - child: Text(AppLocalizations.of(context)!.never), - ), - ), - ), - ), - Container( - width: (mediaQueryData.size.width-70)/2, - margin: const EdgeInsets.only( - top: 10, - left: 5, - bottom: 5 - ), - child: OptionBox( - optionsValue: selectedOption, - itemValue: 1, - onTap: _updateRadioValue, - child: Center( - child: AnimatedDefaultTextStyle( - duration: const Duration(milliseconds: 250), - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - color: selectedOption == 1 - ? Theme.of(context).colorScheme.onInverseSurface - : Theme.of(context).colorScheme.onSurface - ), - child: Text(AppLocalizations.of(context)!.hour1), - ), - ), - ), - ), - ], + Padding( + padding: const EdgeInsets.only(top: 24), + child: Icon( + Icons.update_rounded, + size: 24, + color: Theme.of(context).listTileTheme.iconColor + ), ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Container( - width: (mediaQueryData.size.width-70)/2, - margin: const EdgeInsets.only( - top: 5, - right: 5, - bottom: 5 - ), - child: OptionBox( - optionsValue: selectedOption, - itemValue: 12, - onTap: _updateRadioValue, - child: Center( - child: AnimatedDefaultTextStyle( - duration: const Duration(milliseconds: 250), - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - color: selectedOption == 12 - ? Theme.of(context).colorScheme.onInverseSurface - : Theme.of(context).colorScheme.onSurface - ), - child: Text(AppLocalizations.of(context)!.hours12), - ), - ), - ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 16 + ), + child: Text( + AppLocalizations.of(context)!.updateFrequency, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 24, + color: Theme.of(context).colorScheme.onSurface ), - Container( - width: (mediaQueryData.size.width-70)/2, - margin: const EdgeInsets.only( - top: 5, - left: 5, - bottom: 5 - ), - child: OptionBox( - optionsValue: selectedOption, - itemValue: 24, - onTap: _updateRadioValue, - child: Center( - child: AnimatedDefaultTextStyle( - duration: const Duration(milliseconds: 250), - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - color: selectedOption == 24 - ? Theme.of(context).colorScheme.onInverseSurface - : Theme.of(context).colorScheme.onSurface - ), - child: Text(AppLocalizations.of(context)!.hours24), - ), - ), - ), - ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Container( - width: (mediaQueryData.size.width-70)/2, - margin: const EdgeInsets.only( - top: 5, - right: 5, - bottom: 10 - ), - child: OptionBox( - optionsValue: selectedOption, - itemValue: 72, - onTap: _updateRadioValue, - child: Center( - child: AnimatedDefaultTextStyle( - duration: const Duration(milliseconds: 250), - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - color: selectedOption == 72 - ? Theme.of(context).colorScheme.onInverseSurface - : Theme.of(context).colorScheme.onSurface - ), - child: Text(AppLocalizations.of(context)!.days3), - ), - ), - ), - ), - Container( - width: (mediaQueryData.size.width-70)/2, - margin: const EdgeInsets.only( - top: 5, - left: 5, - bottom: 10 - ), - child: OptionBox( - optionsValue: selectedOption, - itemValue: 168, - onTap: _updateRadioValue, - child: Center( - child: AnimatedDefaultTextStyle( - duration: const Duration(milliseconds: 250), - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - color: selectedOption == 168 - ? Theme.of(context).colorScheme.onInverseSurface - : Theme.of(context).colorScheme.onSurface - ), - child: Text(AppLocalizations.of(context)!.days7), - ), - ), - ), - ), - ], + ), ), ], - ), + ) + ], + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Wrap( + runSpacing: 16, + children: [ + FractionallySizedBox( + widthFactor: 0.5, + child: Padding( + padding: const EdgeInsets.only(right: 6), + child: OptionBox( + optionsValue: selectedOption, + itemValue: 0, + onTap: _updateRadioValue, + child: Center( + child: AnimatedDefaultTextStyle( + duration: const Duration(milliseconds: 250), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: selectedOption == 0 + ? Theme.of(context).colorScheme.onInverseSurface + : Theme.of(context).colorScheme.onSurface + ), + child: Text(AppLocalizations.of(context)!.never), + ), + ), + ), + ), + ), + FractionallySizedBox( + widthFactor: 0.5, + child: Padding( + padding: const EdgeInsets.only(left: 6), + child: OptionBox( + optionsValue: selectedOption, + itemValue: 1, + onTap: _updateRadioValue, + child: Center( + child: AnimatedDefaultTextStyle( + duration: const Duration(milliseconds: 250), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: selectedOption == 1 + ? Theme.of(context).colorScheme.onInverseSurface + : Theme.of(context).colorScheme.onSurface + ), + child: Text(AppLocalizations.of(context)!.hour1), + ), + ), + ), + ), + ), + FractionallySizedBox( + widthFactor: 0.5, + child: Padding( + padding: const EdgeInsets.only(right: 6), + child: OptionBox( + optionsValue: selectedOption, + itemValue: 12, + onTap: _updateRadioValue, + child: Center( + child: AnimatedDefaultTextStyle( + duration: const Duration(milliseconds: 250), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: selectedOption == 12 + ? Theme.of(context).colorScheme.onInverseSurface + : Theme.of(context).colorScheme.onSurface + ), + child: Text(AppLocalizations.of(context)!.hours12), + ), + ), + ), + ), + ), + FractionallySizedBox( + widthFactor: 0.5, + child: Padding( + padding: const EdgeInsets.only(left: 6), + child: OptionBox( + optionsValue: selectedOption, + itemValue: 24, + onTap: _updateRadioValue, + child: Center( + child: AnimatedDefaultTextStyle( + duration: const Duration(milliseconds: 250), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: selectedOption == 24 + ? Theme.of(context).colorScheme.onInverseSurface + : Theme.of(context).colorScheme.onSurface + ), + child: Text(AppLocalizations.of(context)!.hours24), + ), + ), + ), + ), + ), + FractionallySizedBox( + widthFactor: 0.5, + child: Padding( + padding: const EdgeInsets.only(right: 6), + child: OptionBox( + optionsValue: selectedOption, + itemValue: 72, + onTap: _updateRadioValue, + child: Center( + child: AnimatedDefaultTextStyle( + duration: const Duration(milliseconds: 250), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: selectedOption == 72 + ? Theme.of(context).colorScheme.onInverseSurface + : Theme.of(context).colorScheme.onSurface + ), + child: Text(AppLocalizations.of(context)!.days3), + ), + ), + ), + ), + ), + FractionallySizedBox( + widthFactor: 0.5, + child: Padding( + padding: const EdgeInsets.only(left: 6), + child: OptionBox( + optionsValue: selectedOption, + itemValue: 168, + onTap: _updateRadioValue, + child: Center( + child: AnimatedDefaultTextStyle( + duration: const Duration(milliseconds: 250), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: selectedOption == 168 + ? Theme.of(context).colorScheme.onInverseSurface + : Theme.of(context).colorScheme.onSurface + ), + child: Text(AppLocalizations.of(context)!.days7), + ), + ), + ), + ), + ), + ], ), - ), + ) ], ), ), - Padding( - padding: const EdgeInsets.all(24), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(AppLocalizations.of(context)!.cancel), - ), - const SizedBox(width: 20), - TextButton( - onPressed: selectedOption != null - ? () { - Navigator.pop(context); - widget.onChange(selectedOption!); - } - : null, - style: ButtonStyle( - overlayColor: MaterialStateProperty.all( - Theme.of(context).colorScheme.primary.withOpacity(0.1) - ), - foregroundColor: MaterialStateProperty.all( - selectedOption != null - ? Theme.of(context).colorScheme.primary - : Colors.grey, - ), - ), - child: Text(AppLocalizations.of(context)!.confirm), - ), - ], - ), + ), + Padding( + padding: const EdgeInsets.all(24), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.cancel), + ), + const SizedBox(width: 20), + TextButton( + onPressed: selectedOption != null + ? () { + Navigator.pop(context); + widget.onChange(selectedOption!); + } + : null, + style: ButtonStyle( + overlayColor: MaterialStateProperty.all( + Theme.of(context).colorScheme.primary.withOpacity(0.1) + ), + foregroundColor: MaterialStateProperty.all( + selectedOption != null + ? Theme.of(context).colorScheme.primary + : Colors.grey, + ), + ), + child: Text(AppLocalizations.of(context)!.confirm), + ), + ], ), - if (Platform.isIOS) const SizedBox(height: 16) - ], + ), + if (Platform.isIOS) const SizedBox(height: 16) + ], + ); + } + + if (widget.dialog == true) { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500 + ), + child: content() ), - ), - ); + ); + } + else { + return Padding( + padding: mediaQueryData.viewInsets, + child: Container( + height: Platform.isIOS ? 406 : 390, + decoration: BoxDecoration( + color: Theme.of(context).dialogBackgroundColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28) + ), + ), + child: content() + ), + ); + } } } \ No newline at end of file diff --git a/lib/screens/home/chart.dart b/lib/screens/home/chart.dart index 315d421..2ea4535 100644 --- a/lib/screens/home/chart.dart +++ b/lib/screens/home/chart.dart @@ -48,12 +48,15 @@ class HomeChart extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - label, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.onSurface + Flexible( + child: Text( + label, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface + ), ), ), !isEmpty diff --git a/lib/screens/home/fab.dart b/lib/screens/home/fab.dart index ce5f8a9..1369535 100644 --- a/lib/screens/home/fab.dart +++ b/lib/screens/home/fab.dart @@ -12,13 +12,27 @@ class HomeFab extends StatelessWidget { Widget build(BuildContext context) { final serversProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + void openManagementBottomSheet() { - showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: (context) => const ManagementModal(), - backgroundColor: Colors.transparent, - ); + if (width > 700) { + showDialog( + context: context, + builder: (context) => const ManagementModal( + dialog: true, + ), + ); + } + else { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => const ManagementModal( + dialog: false, + ), + backgroundColor: Colors.transparent, + ); + } } return serversProvider.serverStatus.loadStatus == 1 diff --git a/lib/screens/home/home.dart b/lib/screens/home/home.dart index 932ddfe..541f732 100644 --- a/lib/screens/home/home.dart +++ b/lib/screens/home/home.dart @@ -55,6 +55,8 @@ class _HomeState extends State { final serversProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + Widget status() { switch (serversProvider.serverStatus.loadStatus) { case 0: @@ -92,72 +94,138 @@ class _HomeState extends State { ), const SizedBox(height: 20), - HomeChart( - data: serversProvider.serverStatus.data!.stats.dnsQueries, - label: AppLocalizations.of(context)!.dnsQueries, - primaryValue: intFormat(serversProvider.serverStatus.data!.stats.numDnsQueries, Platform.localeName), - secondaryValue: "${doubleFormat(serversProvider.serverStatus.data!.stats.avgProcessingTime*1000, Platform.localeName)} ms", - color: Colors.blue, - ), - - HomeChart( - data: serversProvider.serverStatus.data!.stats.blockedFiltering, - label: AppLocalizations.of(context)!.blockedFilters, - primaryValue: intFormat(serversProvider.serverStatus.data!.stats.numBlockedFiltering, Platform.localeName), - secondaryValue: "${serversProvider.serverStatus.data!.stats.numDnsQueries > 0 ? doubleFormat((serversProvider.serverStatus.data!.stats.numBlockedFiltering/serversProvider.serverStatus.data!.stats.numDnsQueries)*100, Platform.localeName) : 0}%", - color: Colors.red, + Wrap( + children: [ + FractionallySizedBox( + widthFactor: width > 700 ? 0.5 : 1, + child: HomeChart( + data: serversProvider.serverStatus.data!.stats.dnsQueries, + label: AppLocalizations.of(context)!.dnsQueries, + primaryValue: intFormat(serversProvider.serverStatus.data!.stats.numDnsQueries, Platform.localeName), + secondaryValue: "${doubleFormat(serversProvider.serverStatus.data!.stats.avgProcessingTime*1000, Platform.localeName)} ms", + color: Colors.blue, + ), + ), + FractionallySizedBox( + widthFactor: width > 700 ? 0.5 : 1, + child: HomeChart( + data: serversProvider.serverStatus.data!.stats.blockedFiltering, + label: AppLocalizations.of(context)!.blockedFilters, + primaryValue: intFormat(serversProvider.serverStatus.data!.stats.numBlockedFiltering, Platform.localeName), + secondaryValue: "${serversProvider.serverStatus.data!.stats.numDnsQueries > 0 ? doubleFormat((serversProvider.serverStatus.data!.stats.numBlockedFiltering/serversProvider.serverStatus.data!.stats.numDnsQueries)*100, Platform.localeName) : 0}%", + color: Colors.red, + ), + ), + FractionallySizedBox( + widthFactor: width > 700 ? 0.5 : 1, + child: HomeChart( + data: serversProvider.serverStatus.data!.stats.replacedSafebrowsing, + label: AppLocalizations.of(context)!.malwarePhisingBlocked, + primaryValue: intFormat(serversProvider.serverStatus.data!.stats.numReplacedSafebrowsing, Platform.localeName), + secondaryValue: "${serversProvider.serverStatus.data!.stats.numDnsQueries > 0 ? doubleFormat((serversProvider.serverStatus.data!.stats.numReplacedSafebrowsing/serversProvider.serverStatus.data!.stats.numDnsQueries)*100, Platform.localeName) : 0}%", + color: Colors.green, + ), + ), + FractionallySizedBox( + widthFactor: width > 700 ? 0.5 : 1, + child: HomeChart( + data: serversProvider.serverStatus.data!.stats.replacedParental, + label: AppLocalizations.of(context)!.blockedAdultWebsites, + primaryValue: intFormat(serversProvider.serverStatus.data!.stats.numReplacedParental, Platform.localeName), + secondaryValue: "${serversProvider.serverStatus.data!.stats.numDnsQueries > 0 ? doubleFormat((serversProvider.serverStatus.data!.stats.numReplacedParental/serversProvider.serverStatus.data!.stats.numDnsQueries)*100, Platform.localeName) : 0}%", + color: Colors.orange, + ), + ), + + ], ), - HomeChart( - data: serversProvider.serverStatus.data!.stats.replacedSafebrowsing, - label: AppLocalizations.of(context)!.malwarePhisingBlocked, - primaryValue: intFormat(serversProvider.serverStatus.data!.stats.numReplacedSafebrowsing, Platform.localeName), - secondaryValue: "${serversProvider.serverStatus.data!.stats.numDnsQueries > 0 ? doubleFormat((serversProvider.serverStatus.data!.stats.numReplacedSafebrowsing/serversProvider.serverStatus.data!.stats.numDnsQueries)*100, Platform.localeName) : 0}%", - color: Colors.green, - ), - - HomeChart( - data: serversProvider.serverStatus.data!.stats.replacedParental, - label: AppLocalizations.of(context)!.blockedAdultWebsites, - primaryValue: intFormat(serversProvider.serverStatus.data!.stats.numReplacedParental, Platform.localeName), - secondaryValue: "${serversProvider.serverStatus.data!.stats.numDnsQueries > 0 ? doubleFormat((serversProvider.serverStatus.data!.stats.numReplacedParental/serversProvider.serverStatus.data!.stats.numDnsQueries)*100, Platform.localeName) : 0}%", - color: Colors.orange, - ), - - TopItems( - label: AppLocalizations.of(context)!.topQueriedDomains, - data: serversProvider.serverStatus.data!.stats.topQueriedDomains, - type: 'topQueriedDomains', - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Divider( - thickness: 1, - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.2), + if (width <= 700) ...[ + TopItems( + label: AppLocalizations.of(context)!.topQueriedDomains, + data: serversProvider.serverStatus.data!.stats.topQueriedDomains, + type: 'topQueriedDomains', ), - ), - const SizedBox(height: 20), - - TopItems( - label: AppLocalizations.of(context)!.topBlockedDomains, - data: serversProvider.serverStatus.data!.stats.topBlockedDomains, - type: 'topBlockedDomains', - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Divider( - thickness: 1, - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.2), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Divider( + thickness: 1, + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.2), + ), ), - ), - const SizedBox(height: 20), - TopItems( - label: AppLocalizations.of(context)!.topClients, - data: serversProvider.serverStatus.data!.stats.topClients, - type: 'topClients', - clients: true, - ), + const SizedBox(height: 20), + + TopItems( + label: AppLocalizations.of(context)!.topBlockedDomains, + data: serversProvider.serverStatus.data!.stats.topBlockedDomains, + type: 'topBlockedDomains', + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Divider( + thickness: 1, + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.2), + ), + ), + const SizedBox(height: 20), + + TopItems( + label: AppLocalizations.of(context)!.topClients, + data: serversProvider.serverStatus.data!.stats.topClients, + type: 'topClients', + clients: true, + ), + ], + if (width > 700) Column( + children: [ + const SizedBox(height: 16), + Wrap( + alignment: WrapAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500 + ), + child: TopItems( + label: AppLocalizations.of(context)!.topQueriedDomains, + data: serversProvider.serverStatus.data!.stats.topQueriedDomains, + type: 'topQueriedDomains', + ), + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500 + ), + child: TopItems( + label: AppLocalizations.of(context)!.topBlockedDomains, + data: serversProvider.serverStatus.data!.stats.topBlockedDomains, + type: 'topBlockedDomains', + ), + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500 + ), + child: TopItems( + label: AppLocalizations.of(context)!.topBlockedDomains, + data: serversProvider.serverStatus.data!.stats.topBlockedDomains, + type: 'topBlockedDomains', + ), + ), + ), + ], + ), + ], + ) ], ); diff --git a/lib/screens/home/management_modal.dart b/lib/screens/home/management_modal.dart index e36fbc7..15eac57 100644 --- a/lib/screens/home/management_modal.dart +++ b/lib/screens/home/management_modal.dart @@ -15,7 +15,12 @@ import 'package:adguard_home_manager/services/http_requests.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart'; class ManagementModal extends StatefulWidget { - const ManagementModal({Key? key}) : super(key: key); + final bool dialog; + + const ManagementModal({ + Key? key, + required this.dialog + }) : super(key: key); @override State createState() => _ManagementModalState(); @@ -364,8 +369,112 @@ class _ManagementModalState extends State with SingleTickerProv ); } - return SafeArea( - child: Container( + Widget header() { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(top: 24), + child: Icon( + Icons.shield_rounded, + size: 24, + color: Theme.of(context).listTileTheme.iconColor + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Text( + AppLocalizations.of(context)!.manageServer, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ], + ), + ], + ); + } + + List toggles() { + return [ + mainSwitch(), + Container(height: 10), + smallSwitch( + AppLocalizations.of(context)!.ruleFiltering, + Icons.filter_list_rounded, + serversProvider.serverStatus.data!.filteringEnabled, + (value) => updateBlocking(value: value, filter: 'filtering'), + serversProvider.protectionsManagementProcess.contains('filtering') + ), + smallSwitch( + AppLocalizations.of(context)!.safeBrowsing, + Icons.vpn_lock_rounded, + serversProvider.serverStatus.data!.safeBrowsingEnabled, + (value) => updateBlocking(value: value, filter: 'safeBrowsing'), + serversProvider.protectionsManagementProcess.contains('safeBrowsing') + ), + smallSwitch( + AppLocalizations.of(context)!.parentalFiltering, + Icons.block, + serversProvider.serverStatus.data!.parentalControlEnabled, + (value) => updateBlocking(value: value, filter: 'parentalControl'), + serversProvider.protectionsManagementProcess.contains('parentalControl') + ), + smallSwitch( + AppLocalizations.of(context)!.safeSearch, + Icons.search_rounded, + serversProvider.serverStatus.data!.safeSearchEnabled, + (value) => updateBlocking(value: value, filter: 'safeSearch'), + serversProvider.protectionsManagementProcess.contains('safeSearch') + ), + ]; + } + + if (widget.dialog == true) { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 400 + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: SingleChildScrollView( + child: Wrap( + children: [ + header(), + ...toggles() + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(24), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.close), + ), + ], + ), + ), + if (Platform.isIOS) const SizedBox(height: 16) + ], + ), + ), + ); + } + else { + return Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: const BorderRadius.only( @@ -373,66 +482,18 @@ class _ManagementModalState extends State with SingleTickerProv topRight: Radius.circular(28) ) ), - child: Wrap( + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.center, + Flexible( + child: SingleChildScrollView( + child: Wrap( children: [ - Padding( - padding: const EdgeInsets.only(top: 24), - child: Icon( - Icons.shield_rounded, - size: 24, - color: Theme.of(context).listTileTheme.iconColor - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 16), - child: Text( - AppLocalizations.of(context)!.manageServer, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ), + header(), + ...toggles() ], ), - ], - ), - mainSwitch(), - const SizedBox(height: 10), - smallSwitch( - AppLocalizations.of(context)!.ruleFiltering, - Icons.filter_list_rounded, - serversProvider.serverStatus.data!.filteringEnabled, - (value) => updateBlocking(value: value, filter: 'filtering'), - serversProvider.protectionsManagementProcess.contains('filtering') - ), - smallSwitch( - AppLocalizations.of(context)!.safeBrowsing, - Icons.vpn_lock_rounded, - serversProvider.serverStatus.data!.safeBrowsingEnabled, - (value) => updateBlocking(value: value, filter: 'safeBrowsing'), - serversProvider.protectionsManagementProcess.contains('safeBrowsing') - ), - smallSwitch( - AppLocalizations.of(context)!.parentalFiltering, - Icons.block, - serversProvider.serverStatus.data!.parentalControlEnabled, - (value) => updateBlocking(value: value, filter: 'parentalControl'), - serversProvider.protectionsManagementProcess.contains('parentalControl') - ), - smallSwitch( - AppLocalizations.of(context)!.safeSearch, - Icons.search_rounded, - serversProvider.serverStatus.data!.safeSearchEnabled, - (value) => updateBlocking(value: value, filter: 'safeSearch'), - serversProvider.protectionsManagementProcess.contains('safeSearch') + ), ), Padding( padding: const EdgeInsets.all(24), @@ -449,7 +510,7 @@ class _ManagementModalState extends State with SingleTickerProv if (Platform.isIOS) const SizedBox(height: 16) ], ), - ), - ); + ); + } } } \ No newline at end of file diff --git a/lib/screens/home/server_status.dart b/lib/screens/home/server_status.dart index c4d8d97..1df9767 100644 --- a/lib/screens/home/server_status.dart +++ b/lib/screens/home/server_status.dart @@ -15,8 +15,12 @@ class ServerStatus extends StatelessWidget { @override Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width; + return Container( - padding: const EdgeInsets.all(20), + padding: width > 700 + ? const EdgeInsets.only(left: 20, right: 20, bottom: 20) + : const EdgeInsets.all(20), child: Column( children: [ Text( @@ -29,11 +33,11 @@ class ServerStatus extends StatelessWidget { ), const SizedBox(height: 20), SizedBox( - height: 140, + height: width > 700 ? 70 : 140, child: GridView( physics: const NeverScrollableScrollPhysics(), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: width > 700 ? 4 : 2, crossAxisSpacing: 10, mainAxisSpacing: 10, mainAxisExtent: 65 diff --git a/lib/screens/home/status_box.dart b/lib/screens/home/status_box.dart index 62d4279..2f57106 100644 --- a/lib/screens/home/status_box.dart +++ b/lib/screens/home/status_box.dart @@ -44,13 +44,15 @@ class StatusBox extends StatelessWidget { : Colors.grey.computeLuminance() > 0.5 ? Colors.black : Colors.white, ), const SizedBox(width: 12), - Text( - label, - style: TextStyle( - color: appConfigProvider.useThemeColorForStatus == true - ? Theme.of(context).colorScheme.primary.computeLuminance() > 0.5 ? Colors.black : Colors.white - : Colors.grey.computeLuminance() > 0.5 ? Colors.black : Colors.white, - fontWeight: FontWeight.w500 + Flexible( + child: Text( + label, + style: TextStyle( + color: appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary.computeLuminance() > 0.5 ? Colors.black : Colors.white + : Colors.grey.computeLuminance() > 0.5 ? Colors.black : Colors.white, + fontWeight: FontWeight.w500 + ), ), ) ], diff --git a/lib/screens/home/top_items.dart b/lib/screens/home/top_items.dart index 5883523..0bf85d2 100644 --- a/lib/screens/home/top_items.dart +++ b/lib/screens/home/top_items.dart @@ -1,5 +1,9 @@ // ignore_for_file: use_build_context_synchronously +import 'dart:io'; + +import 'package:adguard_home_manager/screens/top_items/top_items_modal.dart'; +import 'package:animations/animations.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; @@ -35,6 +39,8 @@ class TopItems extends StatelessWidget { final appConfigProvider = Provider.of(context); final logsProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + bool? getIsBlocked() { if (type == 'topBlockedDomains') { return true; @@ -266,16 +272,32 @@ class TopItems extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( - onPressed: () => Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => TopItemsScreen( - type: type, - title: label, - isClient: clients, - data: generateData(), + onPressed: () => { + if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => TopItemsModal( + type: type, + title: label, + isClient: clients, + data: generateData(), + ) ) - ) - ), + } + else { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => TopItemsScreen( + type: type, + title: label, + isClient: clients, + data: generateData(), + ) + ) + ) + } + }, child: Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/screens/logs/clients_modal.dart b/lib/screens/logs/clients_modal.dart index f9b9099..bd39541 100644 --- a/lib/screens/logs/clients_modal.dart +++ b/lib/screens/logs/clients_modal.dart @@ -8,10 +8,12 @@ import 'package:adguard_home_manager/providers/logs_provider.dart'; class ClientsModal extends StatefulWidget { final List? value; + final bool dialog; const ClientsModal({ Key? key, - required this.value + required this.value, + required this.dialog }) : super(key: key); @override @@ -94,44 +96,36 @@ class _ClientsModalState extends State { }); } - return Container( - height: height >= (logsProvider.clients!.length*64) == true - ? logsProvider.clients!.length*64 - : height-50, - decoration: BoxDecoration( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28) - ), - color: Theme.of(context).dialogBackgroundColor - ), - child: Column( + Widget content() { + return Column( + mainAxisSize: MainAxisSize.min, children: [ - Padding( - padding: const EdgeInsets.only( - top: 24, - bottom: 16, - ), - child: Icon( - Icons.smartphone_rounded, - size: 24, - color: Theme.of(context).listTileTheme.iconColor - ), + Column( + children: [ + Padding( + padding: const EdgeInsets.only( + top: 24, + bottom: 16, + ), + child: Icon( + Icons.smartphone_rounded, + size: 24, + color: Theme.of(context).listTileTheme.iconColor + ), + ), + Text( + AppLocalizations.of(context)!.clients, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface + ), + ), + const SizedBox(height: 16), + ], ), - Text( - AppLocalizations.of(context)!.clients, - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurface - ), - ), - const SizedBox(height: 16), - Expanded( + Flexible( child: ListView.builder( - physics: height >= (logsProvider.clients!.length*64) == true - ? const NeverScrollableScrollPhysics() - : null, itemCount: logsProvider.clients!.length, itemBuilder: (context, index) => listItem( label: logsProvider.clients![index].ip, @@ -150,7 +144,7 @@ class _ClientsModalState extends State { } } ) - ), + ) ), Padding( padding: const EdgeInsets.all(24), @@ -176,7 +170,35 @@ class _ClientsModalState extends State { ), if (Platform.isIOS) const SizedBox(height: 16) ], - ), - ); + ); + } + + if (widget.dialog == true) { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500 + ), + child: content() + ), + ); + } + else { + return ConstrainedBox( + constraints: BoxConstraints( + maxHeight: height-50 + ), + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28) + ), + color: Theme.of(context).dialogBackgroundColor + ), + child: content() + ), + ); + } } } \ No newline at end of file diff --git a/lib/screens/logs/filter_status_modal.dart b/lib/screens/logs/filter_status_modal.dart index 16d9c5c..aec5aac 100644 --- a/lib/screens/logs/filter_status_modal.dart +++ b/lib/screens/logs/filter_status_modal.dart @@ -8,10 +8,12 @@ import 'package:adguard_home_manager/providers/logs_provider.dart'; class FilterStatusModal extends StatefulWidget { final String value; + final bool dialog; const FilterStatusModal({ Key? key, - required this.value + required this.value, + required this.dialog }) : super(key: key); @override @@ -31,8 +33,6 @@ class _FilterStatusModalState extends State { Widget build(BuildContext context) { final logsProvider = Provider.of(context); - final height = MediaQuery.of(context).size.height; - void apply() async { logsProvider.setSelectedResultStatus(selectedResultStatus); @@ -83,95 +83,94 @@ class _FilterStatusModalState extends State { ); } - return Container( - height: height >= (Platform.isIOS ? 736 : 720) == true - ? (Platform.isIOS ? 736 : 720) - : height-25, - decoration: BoxDecoration( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28) - ), - color: Theme.of(context).dialogBackgroundColor - ), - child: Column( + Widget content() { + return Column( + mainAxisSize: MainAxisSize.min, children: [ - Padding( - padding: const EdgeInsets.only( - top: 24, - bottom: 16, - ), - child: Icon( - Icons.shield_rounded, - size: 24, - color: Theme.of(context).listTileTheme.iconColor - ), - ), - Text( - AppLocalizations.of(context)!.responseStatus, - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurface - ), - ), - const SizedBox(height: 16), - Expanded( - child: ListView( - physics: height >= 720 == true - ? const NeverScrollableScrollPhysics() - : null, - children: [ - filterStatusListItem( - id: "all", - icon: Icons.shield_rounded, - label: AppLocalizations.of(context)!.all, - onChanged: (value) => setState(() => selectedResultStatus = value!) - ), - filterStatusListItem( - id: "filtered", - icon: Icons.shield_rounded, - label: AppLocalizations.of(context)!.filtered, - onChanged: (value) => setState(() => selectedResultStatus = value!) - ), - filterStatusListItem( - id: "processed", - icon: Icons.verified_user_rounded, - label: AppLocalizations.of(context)!.processedRow, - onChanged: (value) => setState(() => selectedResultStatus = value!) - ), - filterStatusListItem( - id: "whitelisted", - icon: Icons.verified_user_rounded, - label: AppLocalizations.of(context)!.processedWhitelistRow, - onChanged: (value) => setState(() => selectedResultStatus = value!) - ), - filterStatusListItem( - id: "blocked", - icon: Icons.gpp_bad_rounded, - label: AppLocalizations.of(context)!.blocked, - onChanged: (value) => setState(() => selectedResultStatus = value!) - ), - filterStatusListItem( - id: "blocked_safebrowsing", - icon: Icons.gpp_bad_rounded, - label: AppLocalizations.of(context)!.blockedSafeBrowsingRow, - onChanged: (value) => setState(() => selectedResultStatus = value!) - ), - filterStatusListItem( - id: "blocked_parental", - icon: Icons.gpp_bad_rounded, - label: AppLocalizations.of(context)!.blockedParentalRow, - onChanged: (value) => setState(() => selectedResultStatus = value!) - ), - filterStatusListItem( - id: "safe_search", - icon: Icons.gpp_bad_rounded, - label: AppLocalizations.of(context)!.blockedSafeSearchRow, - onChanged: (value) => setState(() => selectedResultStatus = value!) - ), - - ], + Flexible( + child: SingleChildScrollView( + child: Wrap( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + Padding( + padding: const EdgeInsets.only( + top: 24, + bottom: 16, + ), + child: Icon( + Icons.shield_rounded, + size: 24, + color: Theme.of(context).listTileTheme.iconColor + ), + ), + Text( + AppLocalizations.of(context)!.responseStatus, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface + ), + ), + ], + ) + ], + ), + Container(height: 16), + filterStatusListItem( + id: "all", + icon: Icons.shield_rounded, + label: AppLocalizations.of(context)!.all, + onChanged: (value) => setState(() => selectedResultStatus = value!) + ), + filterStatusListItem( + id: "filtered", + icon: Icons.shield_rounded, + label: AppLocalizations.of(context)!.filtered, + onChanged: (value) => setState(() => selectedResultStatus = value!) + ), + filterStatusListItem( + id: "processed", + icon: Icons.verified_user_rounded, + label: AppLocalizations.of(context)!.processedRow, + onChanged: (value) => setState(() => selectedResultStatus = value!) + ), + filterStatusListItem( + id: "whitelisted", + icon: Icons.verified_user_rounded, + label: AppLocalizations.of(context)!.processedWhitelistRow, + onChanged: (value) => setState(() => selectedResultStatus = value!) + ), + filterStatusListItem( + id: "blocked", + icon: Icons.gpp_bad_rounded, + label: AppLocalizations.of(context)!.blocked, + onChanged: (value) => setState(() => selectedResultStatus = value!) + ), + filterStatusListItem( + id: "blocked_safebrowsing", + icon: Icons.gpp_bad_rounded, + label: AppLocalizations.of(context)!.blockedSafeBrowsingRow, + onChanged: (value) => setState(() => selectedResultStatus = value!) + ), + filterStatusListItem( + id: "blocked_parental", + icon: Icons.gpp_bad_rounded, + label: AppLocalizations.of(context)!.blockedParentalRow, + onChanged: (value) => setState(() => selectedResultStatus = value!) + ), + filterStatusListItem( + id: "safe_search", + icon: Icons.gpp_bad_rounded, + label: AppLocalizations.of(context)!.blockedSafeSearchRow, + onChanged: (value) => setState(() => selectedResultStatus = value!) + ), + + ], + ), ), ), Padding( @@ -188,7 +187,30 @@ class _FilterStatusModalState extends State { ), if (Platform.isIOS) const SizedBox(height: 16) ], - ), - ); + ); + } + + if (widget.dialog == true) { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 400 + ), + child: content() + ), + ); + } + else { + return Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28) + ), + color: Theme.of(context).dialogBackgroundColor + ), + child: content() + ); + } } } \ No newline at end of file diff --git a/lib/screens/logs/log_details_screen.dart b/lib/screens/logs/log_details_screen.dart index a6960a1..5ee50b7 100644 --- a/lib/screens/logs/log_details_screen.dart +++ b/lib/screens/logs/log_details_screen.dart @@ -19,10 +19,12 @@ import 'package:adguard_home_manager/providers/app_config_provider.dart'; class LogDetailsScreen extends StatelessWidget { final Log log; + final bool dialog; const LogDetailsScreen({ Key? key, - required this.log + required this.log, + required this.dialog }) : super(key: key); @override @@ -105,25 +107,8 @@ class LogDetailsScreen extends StatelessWidget { } } - return Scaffold( - appBar: AppBar( - title: Text(AppLocalizations.of(context)!.logDetails), - actions: [ - IconButton( - onPressed: () => blockUnblock(log, getFilteredStatus(context, appConfigProvider, log.reason, true)['filtered'] == true ? 'unblock' : 'block'), - icon: Icon( - getFilteredStatus(context, appConfigProvider, log.reason, true)['filtered'] == true - ? Icons.check_circle_rounded - : Icons.block - ), - tooltip: getFilteredStatus(context, appConfigProvider, log.reason, true)['filtered'] == true - ? AppLocalizations.of(context)!.unblockDomain - : AppLocalizations.of(context)!.blockDomain, - ), - const SizedBox(width: 10) - ], - ), - body: ListView( + Widget content() { + return ListView( children: [ SectionLabel(label: AppLocalizations.of(context)!.status), LogListTile( @@ -247,7 +232,87 @@ class LogDetailsScreen extends StatelessWidget { )).toList() ] ], - ), - ); + ); + } + + if (dialog) { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500 + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.clear_rounded) + ), + const SizedBox(width: 16), + Text( + AppLocalizations.of(context)!.logDetails, + style: const TextStyle( + fontSize: 22 + ), + ), + ], + ), + Row( + children: [ + IconButton( + onPressed: () => blockUnblock(log, getFilteredStatus(context, appConfigProvider, log.reason, true)['filtered'] == true ? 'unblock' : 'block'), + icon: Icon( + getFilteredStatus(context, appConfigProvider, log.reason, true)['filtered'] == true + ? Icons.check_circle_rounded + : Icons.block + ), + tooltip: getFilteredStatus(context, appConfigProvider, log.reason, true)['filtered'] == true + ? AppLocalizations.of(context)!.unblockDomain + : AppLocalizations.of(context)!.blockDomain, + ), + ], + ) + ], + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: content(), + ), + ) + ], + ), + ), + ); + } + else { + return Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context)!.logDetails), + actions: [ + IconButton( + onPressed: () => blockUnblock(log, getFilteredStatus(context, appConfigProvider, log.reason, true)['filtered'] == true ? 'unblock' : 'block'), + icon: Icon( + getFilteredStatus(context, appConfigProvider, log.reason, true)['filtered'] == true + ? Icons.check_circle_rounded + : Icons.block + ), + tooltip: getFilteredStatus(context, appConfigProvider, log.reason, true)['filtered'] == true + ? AppLocalizations.of(context)!.unblockDomain + : AppLocalizations.of(context)!.blockDomain, + ), + const SizedBox(width: 10) + ], + ), + body: content() + ); + } } } \ No newline at end of file diff --git a/lib/screens/logs/log_tile.dart b/lib/screens/logs/log_tile.dart index ff7b058..a4c082f 100644 --- a/lib/screens/logs/log_tile.dart +++ b/lib/screens/logs/log_tile.dart @@ -1,10 +1,10 @@ // ignore_for_file: use_build_context_synchronously + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/screens/home/top_items_options_modal.dart'; -import 'package:adguard_home_manager/screens/logs/log_details_screen.dart'; import 'package:adguard_home_manager/functions/copy_clipboard.dart'; import 'package:adguard_home_manager/functions/block_unblock_domain.dart'; @@ -18,12 +18,18 @@ class LogTile extends StatelessWidget { final Log log; final int length; final int index; + final bool? isLogSelected; + final void Function(Log) onLogTap; + final bool? useAlwaysNormalTile; const LogTile({ Key? key, required this.log, required this.length, - required this.index + required this.index, + this.isLogSelected, + required this.onLogTap, + this.useAlwaysNormalTile }) : super(key: key); @override @@ -38,7 +44,7 @@ class LogTile extends StatelessWidget { required String text }) { return SizedBox( - width: 70, + width: 80, child: Column( children: [ Icon( @@ -95,133 +101,250 @@ class LogTile extends StatelessWidget { ) ); } - - return Material( - color: Colors.transparent, - child: InkWell( - onTap: () => Navigator.push(context, MaterialPageRoute( - builder: (context) => LogDetailsScreen(log: log) - )), - onLongPress: () => openOptionsModal(log), - child: Container( - width: double.maxFinite, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SizedBox( - width: width-130, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - log.question.name, - style: TextStyle( - fontSize: 16, - height: 1.5, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurface - ), - ), - const SizedBox(height: 5), - if (log.client.length <= 15 && appConfigProvider.showNameTimeLogs == false) Row( + + if (width > 1100 && !(useAlwaysNormalTile == true)) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(28), + child: InkWell( + borderRadius: BorderRadius.circular(28), + onTap: () => onLogTap(log), + child: Container( + width: double.maxFinite, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(28), + color: isLogSelected == true + ? Theme.of(context).colorScheme.primaryContainer + : null + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Row( + mainAxisSize: MainAxisSize.min, children: [ - ...[ - Icon( - Icons.smartphone_rounded, - size: 16, - color: Theme.of(context).listTileTheme.textColor, - ), - const SizedBox(width: 5), - Flexible( - child: Text( - log.client, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: Theme.of(context).listTileTheme.textColor, - fontSize: 14, - height: 1.4, - fontWeight: FontWeight.w400, + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + log.question.name, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface, + ), ), - ), - ) - ], - const SizedBox(width: 15), - ...[ - Icon( - Icons.schedule_rounded, - size: 16, - color: Theme.of(context).listTileTheme.textColor, - ), - const SizedBox(width: 5), - Flexible( - child: Text( - convertTimestampLocalTimezone(log.time, 'HH:mm:ss'), - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: Theme.of(context).listTileTheme.textColor, - fontSize: 13 + const SizedBox(height: 5), + if (log.client.length <= 15 && appConfigProvider.showNameTimeLogs == false) Row( + children: [ + ...[ + Icon( + Icons.smartphone_rounded, + size: 16, + color: Theme.of(context).listTileTheme.textColor, + ), + const SizedBox(width: 5), + Flexible( + child: Text( + log.client, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Theme.of(context).listTileTheme.textColor, + fontSize: 14, + height: 1.4, + fontWeight: FontWeight.w400, + ), + ), + ) + ], + const SizedBox(width: 15), + ...[ + Icon( + Icons.schedule_rounded, + size: 16, + color: Theme.of(context).listTileTheme.textColor, + ), + const SizedBox(width: 5), + Flexible( + child: Text( + convertTimestampLocalTimezone(log.time, 'HH:mm:ss'), + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Theme.of(context).listTileTheme.textColor, + fontSize: 13 + ), + ), + ), + ], + ], ), - ), + if (log.client.length > 15 || appConfigProvider.showNameTimeLogs == true) Column( + children: [ + Row( + children: [ + Icon( + Icons.smartphone_rounded, + size: 16, + color: Theme.of(context).listTileTheme.textColor, + ), + const SizedBox(width: 15), + Flexible( + child: Text( + log.client, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Theme.of(context).listTileTheme.textColor, + fontSize: 13 + ), + ), + ) + ], + ), + if (appConfigProvider.showNameTimeLogs == true && log.clientInfo!.name != '') ...[ + const SizedBox(height: 10), + Row( + children: [ + Icon( + Icons.badge_rounded, + size: 16, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 15), + Flexible( + child: Text( + log.clientInfo!.name, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Theme.of(context).listTileTheme.textColor, + fontSize: 13 + ), + ), + ) + ], + ), + ], + const SizedBox(height: 10), + Row( + children: [ + Icon( + Icons.schedule_rounded, + size: 16, + color: Theme.of(context).listTileTheme.textColor, + ), + const SizedBox(width: 15), + SizedBox( + child: Text( + convertTimestampLocalTimezone(log.time, 'HH:mm:ss'), + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Theme.of(context).listTileTheme.textColor, + fontSize: 13 + ), + ), + ) + ], + ), + if (appConfigProvider.showNameTimeLogs == true && log.elapsedMs != '') ...[ + const SizedBox(height: 10), + Row( + children: [ + Icon( + Icons.timer, + size: 16, + color: Theme.of(context).listTileTheme.textColor, + ), + const SizedBox(width: 15), + SizedBox( + child: Text( + "${double.parse(log.elapsedMs).toStringAsFixed(2)} ms", + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Theme.of(context).listTileTheme.textColor, + fontSize: 13 + ), + ), + ) + ], + ), + ], + ], + ), + ], ), - ], + ) ], ), - if (log.client.length > 15 || appConfigProvider.showNameTimeLogs == true) Column( - children: [ - Row( - children: [ + ), + generateLogStatus() + ], + ) + ), + ), + ), + ); + } + else { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () => onLogTap(log), + onLongPress: () => openOptionsModal(log), + child: Container( + width: double.maxFinite, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + log.question.name, + style: TextStyle( + fontSize: 16, + height: 1.5, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface + ), + ), + const SizedBox(height: 5), + if (log.client.length <= 15 && appConfigProvider.showNameTimeLogs == false) Row( + children: [ + ...[ Icon( Icons.smartphone_rounded, size: 16, color: Theme.of(context).listTileTheme.textColor, ), - const SizedBox(width: 15), + const SizedBox(width: 5), Flexible( child: Text( log.client, overflow: TextOverflow.ellipsis, style: TextStyle( color: Theme.of(context).listTileTheme.textColor, - fontSize: 13 + fontSize: 14, + height: 1.4, + fontWeight: FontWeight.w400, ), ), ) ], - ), - if (appConfigProvider.showNameTimeLogs == true && log.clientInfo!.name != '') ...[ - const SizedBox(height: 10), - Row( - children: [ - Icon( - Icons.badge_rounded, - size: 16, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - const SizedBox(width: 15), - Flexible( - child: Text( - log.clientInfo!.name, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: Theme.of(context).listTileTheme.textColor, - fontSize: 13 - ), - ), - ) - ], - ), - ], - const SizedBox(height: 10), - Row( - children: [ + const SizedBox(width: 15), + ...[ Icon( Icons.schedule_rounded, size: 16, color: Theme.of(context).listTileTheme.textColor, ), - const SizedBox(width: 15), - SizedBox( + const SizedBox(width: 5), + Flexible( child: Text( convertTimestampLocalTimezone(log.time, 'HH:mm:ss'), overflow: TextOverflow.ellipsis, @@ -230,22 +353,23 @@ class LogTile extends StatelessWidget { fontSize: 13 ), ), - ) + ), ], - ), - if (appConfigProvider.showNameTimeLogs == true && log.elapsedMs != '') ...[ - const SizedBox(height: 10), + ], + ), + if (log.client.length > 15 || appConfigProvider.showNameTimeLogs == true) Column( + children: [ Row( children: [ Icon( - Icons.timer, + Icons.smartphone_rounded, size: 16, color: Theme.of(context).listTileTheme.textColor, ), const SizedBox(width: 15), - SizedBox( + Flexible( child: Text( - "${double.parse(log.elapsedMs).toStringAsFixed(2)} ms", + log.client, overflow: TextOverflow.ellipsis, style: TextStyle( color: Theme.of(context).listTileTheme.textColor, @@ -255,18 +379,85 @@ class LogTile extends StatelessWidget { ) ], ), + if (appConfigProvider.showNameTimeLogs == true && log.clientInfo!.name != '') ...[ + const SizedBox(height: 10), + Row( + children: [ + Icon( + Icons.badge_rounded, + size: 16, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 15), + Flexible( + child: Text( + log.clientInfo!.name, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Theme.of(context).listTileTheme.textColor, + fontSize: 13 + ), + ), + ) + ], + ), + ], + const SizedBox(height: 10), + Row( + children: [ + Icon( + Icons.schedule_rounded, + size: 16, + color: Theme.of(context).listTileTheme.textColor, + ), + const SizedBox(width: 15), + SizedBox( + child: Text( + convertTimestampLocalTimezone(log.time, 'HH:mm:ss'), + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Theme.of(context).listTileTheme.textColor, + fontSize: 13 + ), + ), + ) + ], + ), + if (appConfigProvider.showNameTimeLogs == true && log.elapsedMs != '') ...[ + const SizedBox(height: 10), + Row( + children: [ + Icon( + Icons.timer, + size: 16, + color: Theme.of(context).listTileTheme.textColor, + ), + const SizedBox(width: 15), + SizedBox( + child: Text( + "${double.parse(log.elapsedMs).toStringAsFixed(2)} ms", + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Theme.of(context).listTileTheme.textColor, + fontSize: 13 + ), + ), + ) + ], + ), + ], ], - ], - ), - ], + ), + ], + ), ), - ), - const SizedBox(width: 10), - generateLogStatus() - ], + const SizedBox(width: 10), + generateLogStatus() + ], + ), ), ), - ), - ); + ); + } } } \ No newline at end of file diff --git a/lib/screens/logs/logs.dart b/lib/screens/logs/logs.dart index 5393c04..45494e6 100644 --- a/lib/screens/logs/logs.dart +++ b/lib/screens/logs/logs.dart @@ -1,5 +1,7 @@ // ignore_for_file: use_build_context_synchronously +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -7,6 +9,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/screens/logs/logs_filters_modal.dart'; import 'package:adguard_home_manager/screens/logs/logs_config_modal.dart'; import 'package:adguard_home_manager/screens/logs/log_tile.dart'; +import 'package:adguard_home_manager/screens/logs/log_details_screen.dart'; import 'package:adguard_home_manager/functions/snackbar.dart'; import 'package:adguard_home_manager/classes/process_modal.dart'; @@ -64,6 +67,8 @@ class _LogsWidgetState extends State { bool showDivider = true; + Log? selectedLog; + Future fetchLogs({ int? inOffset, bool? loadingMore, @@ -188,6 +193,8 @@ class _LogsWidgetState extends State { final appConfigProvider = Provider.of(context); final logsProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + void updateConfig(Map data) async { ProcessModal processModal = ProcessModal(context: context); processModal.open(AppLocalizations.of(context)!.updatingSettings); @@ -252,12 +259,25 @@ class _LogsWidgetState extends State { void openFilersModal() { - showModalBottomSheet( - context: context, - builder: (context) => const LogsFiltersModal(), - backgroundColor: Colors.transparent, - isScrollControlled: true - ); + if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + context: context, + builder: (context) => const LogsFiltersModal( + dialog: true, + ), + barrierDismissible: false + ); + } + else { + showModalBottomSheet( + context: context, + builder: (context) => const LogsFiltersModal( + dialog: false, + ), + backgroundColor: Colors.transparent, + isScrollControlled: true + ); + } } final Map translatedString = { @@ -319,6 +339,18 @@ class _LogsWidgetState extends State { log: logsProvider.logsData!.data[index], index: index, length: logsProvider.logsData!.data.length, + isLogSelected: selectedLog != null && selectedLog == logsProvider.logsData!.data[index], + onLogTap: (log) { + if (width <= 1100) { + Navigator.push(context, MaterialPageRoute( + builder: (context) => LogDetailsScreen( + log: log, + dialog: false, + ) + )); + } + setState(() => selectedLog = log); + } ); } } @@ -383,161 +415,210 @@ class _LogsWidgetState extends State { } } - return Scaffold( - appBar: AppBar( - title: Text(AppLocalizations.of(context)!.logs), - centerTitle: false, - actions: [ - logsProvider.loadStatus == 1 - ? IconButton( - onPressed: openFilersModal, - icon: const Icon(Icons.filter_list_rounded) - ) - : const SizedBox(), - IconButton( - onPressed: () => { - showModalBottomSheet( - context: context, - builder: (context) => LogsConfigModal( - onConfirm: updateConfig, - onClear: clearQueries, - ), - backgroundColor: Colors.transparent, - isScrollControlled: true - ) - }, - icon: const Icon(Icons.settings) - ), - const SizedBox(width: 5), - ], - bottom: logsProvider.appliedFilters.searchText != null || logsProvider.appliedFilters.selectedResultStatus != 'all' || logsProvider.appliedFilters.clients != null - ? PreferredSize( - preferredSize: const Size(double.maxFinite, 50), - child: Container( - height: 50, - width: double.maxFinite, - padding: const EdgeInsets.only(bottom: 10), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: showDivider == true - ? Theme.of(context).colorScheme.onSurface.withOpacity(0.1) - : Colors.transparent, - ) + Widget logsScreen() { + return Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context)!.logs), + centerTitle: false, + actions: [ + if (!(Platform.isAndroid || Platform.isIOS)) IconButton( + onPressed: () => fetchLogs(inOffset: 0), + icon: const Icon(Icons.refresh_rounded), + tooltip: AppLocalizations.of(context)!.refresh, + ), + logsProvider.loadStatus == 1 + ? IconButton( + onPressed: openFilersModal, + icon: const Icon(Icons.filter_list_rounded), + tooltip: AppLocalizations.of(context)!.filters, + ) + : const SizedBox(), + IconButton( + tooltip: AppLocalizations.of(context)!.settings, + onPressed: () => { + if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + context: context, + builder: (context) => LogsConfigModal( + onConfirm: updateConfig, + onClear: clearQueries, + dialog: true, + ), + barrierDismissible: false ) - ), - child: ListView( - scrollDirection: Axis.horizontal, - children: [ - if (logsProvider.appliedFilters.searchText != null) ...[ + } + else { + showModalBottomSheet( + context: context, + builder: (context) => LogsConfigModal( + onConfirm: updateConfig, + onClear: clearQueries, + dialog: false, + ), + backgroundColor: Colors.transparent, + isScrollControlled: true + ) + } + }, + icon: const Icon(Icons.settings) + ), + const SizedBox(width: 5), + ], + bottom: logsProvider.appliedFilters.searchText != null || logsProvider.appliedFilters.selectedResultStatus != 'all' || logsProvider.appliedFilters.clients != null + ? PreferredSize( + preferredSize: const Size(double.maxFinite, 50), + child: Container( + height: 50, + width: double.maxFinite, + padding: const EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: showDivider == true + ? Theme.of(context).colorScheme.onSurface.withOpacity(0.1) + : Colors.transparent, + ) + ) + ), + child: ListView( + scrollDirection: Axis.horizontal, + children: [ + if (logsProvider.appliedFilters.searchText != null) ...[ + const SizedBox(width: 15), + Chip( + avatar: const Icon( + Icons.link_rounded, + ), + label: Row( + children: [ + Text( + logsProvider.appliedFilters.searchText!, + ), + ], + ), + deleteIcon: const Icon( + Icons.clear, + size: 18, + ), + onDeleted: () { + logsProvider.setAppliedFilters( + AppliedFiters( + selectedResultStatus: logsProvider.appliedFilters.selectedResultStatus, + searchText: null, + clients: logsProvider.appliedFilters.clients + ) + ); + logsProvider.setSearchText(null); + fetchLogs( + inOffset: 0, + searchText: '' + ); + }, + ), + ], + if (logsProvider.appliedFilters.selectedResultStatus != 'all') ...[ + const SizedBox(width: 15), + Chip( + avatar: const Icon( + Icons.shield_rounded, + ), + label: Row( + children: [ + Text( + translatedString[logsProvider.appliedFilters.selectedResultStatus]!, + ), + ], + ), + deleteIcon: const Icon( + Icons.clear, + size: 18, + ), + onDeleted: () { + logsProvider.setAppliedFilters( + AppliedFiters( + selectedResultStatus: 'all', + searchText: logsProvider.appliedFilters.searchText, + clients: logsProvider.appliedFilters.clients + ) + ); + logsProvider.setSelectedResultStatus('all'); + fetchLogs( + inOffset: 0, + responseStatus: 'all' + ); + }, + ), + ], + if (logsProvider.appliedFilters.clients != null) ...[ + const SizedBox(width: 15), + Chip( + avatar: const Icon( + Icons.smartphone_rounded, + ), + label: Row( + children: [ + Text( + logsProvider.appliedFilters.clients!.length == 1 + ? logsProvider.appliedFilters.clients![0] + : "${logsProvider.appliedFilters.clients!.length} ${AppLocalizations.of(context)!.clients}", + ), + ], + ), + deleteIcon: const Icon( + Icons.clear, + size: 18, + ), + onDeleted: () { + logsProvider.setAppliedFilters( + AppliedFiters( + selectedResultStatus: logsProvider.appliedFilters.selectedResultStatus, + searchText: logsProvider.appliedFilters.searchText, + clients: null + ) + ); + logsProvider.setSelectedClients(null); + fetchLogs( + inOffset: 0, + responseStatus: logsProvider.appliedFilters.selectedResultStatus + ); + }, + ), + ], const SizedBox(width: 15), - Chip( - avatar: const Icon( - Icons.link_rounded, - ), - label: Row( - children: [ - Text( - logsProvider.appliedFilters.searchText!, - ), - ], - ), - deleteIcon: const Icon( - Icons.clear, - size: 18, - ), - onDeleted: () { - logsProvider.setAppliedFilters( - AppliedFiters( - selectedResultStatus: logsProvider.appliedFilters.selectedResultStatus, - searchText: null, - clients: logsProvider.appliedFilters.clients - ) - ); - logsProvider.setSearchText(null); - fetchLogs( - inOffset: 0, - searchText: '' - ); - }, - ), ], - if (logsProvider.appliedFilters.selectedResultStatus != 'all') ...[ - const SizedBox(width: 15), - Chip( - avatar: const Icon( - Icons.shield_rounded, - ), - label: Row( - children: [ - Text( - translatedString[logsProvider.appliedFilters.selectedResultStatus]!, - ), - ], - ), - deleteIcon: const Icon( - Icons.clear, - size: 18, - ), - onDeleted: () { - logsProvider.setAppliedFilters( - AppliedFiters( - selectedResultStatus: 'all', - searchText: logsProvider.appliedFilters.searchText, - clients: logsProvider.appliedFilters.clients - ) - ); - logsProvider.setSelectedResultStatus('all'); - fetchLogs( - inOffset: 0, - responseStatus: 'all' - ); - }, - ), - ], - if (logsProvider.appliedFilters.clients != null) ...[ - const SizedBox(width: 15), - Chip( - avatar: const Icon( - Icons.smartphone_rounded, - ), - label: Row( - children: [ - Text( - logsProvider.appliedFilters.clients!.length == 1 - ? logsProvider.appliedFilters.clients![0] - : "${logsProvider.appliedFilters.clients!.length} ${AppLocalizations.of(context)!.clients}", - ), - ], - ), - deleteIcon: const Icon( - Icons.clear, - size: 18, - ), - onDeleted: () { - logsProvider.setAppliedFilters( - AppliedFiters( - selectedResultStatus: logsProvider.appliedFilters.selectedResultStatus, - searchText: logsProvider.appliedFilters.searchText, - clients: null - ) - ); - logsProvider.setSelectedClients(null); - fetchLogs( - inOffset: 0, - responseStatus: logsProvider.appliedFilters.selectedResultStatus - ); - }, - ), - ], - const SizedBox(width: 15), - ], - ), + ), + ) ) + : null, + ), + body: generateBody() + ); + } + + if (width > 1100) { + return Material( + color: Colors.transparent, + child: Row( + children: [ + Expanded( + flex: 1, + child: logsScreen() + ), + Expanded( + flex: 2, + child: selectedLog != null + ? LogDetailsScreen( + log: selectedLog!, + dialog: false, + ) + : const SizedBox() ) - : null, - ), - body: generateBody() - ); + ], + ), + ); + } + else { + return logsScreen(); + } } } \ No newline at end of file diff --git a/lib/screens/logs/logs_config_modal.dart b/lib/screens/logs/logs_config_modal.dart index 9af7335..a540559 100644 --- a/lib/screens/logs/logs_config_modal.dart +++ b/lib/screens/logs/logs_config_modal.dart @@ -12,11 +12,13 @@ import 'package:adguard_home_manager/providers/servers_provider.dart'; class LogsConfigModal extends StatelessWidget { final void Function(Map) onConfirm; final void Function() onClear; + final bool dialog; const LogsConfigModal({ Key? key, required this.onConfirm, required this.onClear, + required this.dialog }) : super(key: key); @override @@ -30,6 +32,7 @@ class LogsConfigModal extends StatelessWidget { context: context, onConfirm: onConfirm, onClear: onClear, + dialog: dialog, ); } } @@ -40,6 +43,7 @@ class LogsConfigModalWidget extends StatefulWidget { final BuildContext context; final void Function(Map) onConfirm; final void Function() onClear; + final bool dialog; const LogsConfigModalWidget({ Key? key, @@ -48,6 +52,7 @@ class LogsConfigModalWidget extends StatefulWidget { required this.context, required this.onConfirm, required this.onClear, + required this.dialog }) : super(key: key); @override @@ -146,125 +151,154 @@ class _LogsConfigModalWidgetState extends State { Widget generateBody() { switch (loadStatus) { case 0: - return const Center( - child: CircularProgressIndicator(), + return Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 30), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + AppLocalizations.of(context)!.loadingLogsSettings, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ), + ) + ], + ), ); case 1: return Column( + mainAxisSize: MainAxisSize.min, children: [ - Expanded( - child: ListView( - physics: (Platform.isIOS ? 436 : 420) < MediaQuery.of(context).size.height - ? const NeverScrollableScrollPhysics() - : null, - children: [ - Padding( - padding: const EdgeInsets.only(top: 24), - child: Icon( - Icons.settings, - size: 24, - color: Theme.of(context).listTileTheme.iconColor - ), - ), - const SizedBox(height: 16), - Text( - AppLocalizations.of(context)!.logsSettings, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSurface - ), - ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Material( - color: Theme.of(context).colorScheme.primary.withOpacity(0.1), - borderRadius: BorderRadius.circular(28), - child: InkWell( - onTap: () => setState(() => generalSwitch = !generalSwitch), - borderRadius: BorderRadius.circular(28), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 8 - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - AppLocalizations.of(context)!.enableLog, - style: const TextStyle( - fontSize: 18, - ), + Flexible( + child: SingleChildScrollView( + child: Wrap( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 24), + child: Icon( + Icons.settings, + size: 24, + color: Theme.of(context).listTileTheme.iconColor ), - Switch( - value: generalSwitch, - onChanged: (value) => setState(() => generalSwitch = value), - ) - ], + ), + const SizedBox(height: 16), + Text( + AppLocalizations.of(context)!.logsSettings, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + color: Theme.of(context).colorScheme.onSurface + ), + ), + const SizedBox(height: 16), + ], + ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Material( + color: Theme.of(context).colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(28), + child: InkWell( + onTap: () => setState(() => generalSwitch = !generalSwitch), + borderRadius: BorderRadius.circular(28), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 8 + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + AppLocalizations.of(context)!.enableLog, + style: const TextStyle( + fontSize: 18, + ), + ), + Switch( + value: generalSwitch, + onChanged: (value) => setState(() => generalSwitch = value), + ) + ], + ), ), ), ), ), - ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 14), - child: Column( - children: [ - Material( - color: Colors.transparent, - child: InkWell( - onTap: () => setState(() => anonymizeClientIp = !anonymizeClientIp), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 30), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - AppLocalizations.of(context)!.anonymizeClientIp, - style: const TextStyle( - fontSize: 16 + Container(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 14), + child: Column( + children: [ + Material( + color: Colors.transparent, + child: InkWell( + onTap: () => setState(() => anonymizeClientIp = !anonymizeClientIp), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 30), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + AppLocalizations.of(context)!.anonymizeClientIp, + style: const TextStyle( + fontSize: 16 + ), ), - ), - Switch( - value: anonymizeClientIp, - onChanged: (value) => setState(() => anonymizeClientIp = value), + Switch( + value: anonymizeClientIp, + onChanged: (value) => setState(() => anonymizeClientIp = value), + ) + ], + ), + ), + ), + ), + Container(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: DropdownButtonFormField( + items: retentionItems.map>((Map item) { + return DropdownMenuItem( + value: item['value'].toString(), + child: Text(item['label']), + ); + }).toList(), + value: retentionTime, + onChanged: (value) => setState(() => retentionTime = value), + decoration: InputDecoration( + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) ) - ], + ), + label: Text(AppLocalizations.of(context)!.retentionTime) ), + borderRadius: BorderRadius.circular(20), ), ), - ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: DropdownButtonFormField( - items: retentionItems.map>((Map item) { - return DropdownMenuItem( - value: item['value'].toString(), - child: Text(item['label']), - ); - }).toList(), - value: retentionTime, - onChanged: (value) => setState(() => retentionTime = value), - decoration: InputDecoration( - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - label: Text(AppLocalizations.of(context)!.retentionTime) - ), - borderRadius: BorderRadius.circular(20), - ), - ), - ], - ), - ) - ], + ], + ), + ) + ], + ), ), ), Padding( @@ -316,31 +350,29 @@ class _LogsConfigModalWidgetState extends State { ); case 2: - return SizedBox( - width: double.maxFinite, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon( - Icons.error, - color: Colors.red, - size: 50, - ), - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Text( - AppLocalizations.of(context)!.logSettingsNotLoaded, - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 22, - color: Colors.grey, - ), + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.error, + color: Colors.red, + size: 50, + ), + const SizedBox(height: 30), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + AppLocalizations.of(context)!.logSettingsNotLoaded, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant ), - ) - ], - ), + ), + ) + ], ); default: @@ -348,16 +380,28 @@ class _LogsConfigModalWidgetState extends State { } } - return Container( - height: Platform.isIOS ? 436 : 420, - decoration: BoxDecoration( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28) + if (widget.dialog == true) { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500 + ), + child: generateBody() ), - color: Theme.of(context).dialogBackgroundColor - ), - child: generateBody() - ); + ); + } + else { + return Container( + height: Platform.isIOS ? 436 : 420, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28) + ), + color: Theme.of(context).dialogBackgroundColor + ), + child: generateBody() + ); + } } } \ No newline at end of file diff --git a/lib/screens/logs/logs_filters_modal.dart b/lib/screens/logs/logs_filters_modal.dart index e9a6052..19ba0c0 100644 --- a/lib/screens/logs/logs_filters_modal.dart +++ b/lib/screens/logs/logs_filters_modal.dart @@ -18,24 +18,32 @@ import 'package:adguard_home_manager/models/applied_filters.dart'; import 'package:adguard_home_manager/providers/logs_provider.dart'; class LogsFiltersModal extends StatelessWidget { - const LogsFiltersModal({Key? key}) : super(key: key); + final bool dialog; + + const LogsFiltersModal({ + Key? key, + required this.dialog + }) : super(key: key); @override Widget build(BuildContext context) { final logsProvider = Provider.of(context); return LogsFiltersModalWidget( - logsProvider: logsProvider + logsProvider: logsProvider, + dialog: dialog, ); } } class LogsFiltersModalWidget extends StatefulWidget { final LogsProvider logsProvider; + final bool dialog; const LogsFiltersModalWidget({ Key? key, - required this.logsProvider + required this.logsProvider, + required this.dialog }) : super(key: key); @override @@ -57,6 +65,8 @@ class _LogsFiltersModalWidgetState extends State { final serversProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + final Map translatedString = { "all": AppLocalizations.of(context)!.all, "filtered": AppLocalizations.of(context)!.filtered, @@ -101,25 +111,51 @@ class _LogsFiltersModalWidgetState extends State { } void openSelectFilterStatus() { - showModalBottomSheet( - context: context, - builder: (context) => FilterStatusModal( - value: logsProvider.selectedResultStatus, - ), - isScrollControlled: true, - backgroundColor: Colors.transparent - ); + if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + barrierDismissible: false, + context: context, + builder: (context) => FilterStatusModal( + value: logsProvider.selectedResultStatus, + dialog: true, + ), + ); + } + else { + showModalBottomSheet( + context: context, + builder: (context) => FilterStatusModal( + value: logsProvider.selectedResultStatus, + dialog: false, + ), + isScrollControlled: true, + backgroundColor: Colors.transparent + ); + } } void openSelectClients() { - showModalBottomSheet( - context: context, - builder: (context) => ClientsModal( - value: logsProvider.selectedClients, - ), - isScrollControlled: true, - backgroundColor: Colors.transparent - ); + if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + context: context, + builder: (context) => ClientsModal( + value: logsProvider.selectedClients, + dialog: true, + ), + barrierDismissible: false + ); + } + else { + showModalBottomSheet( + context: context, + builder: (context) => ClientsModal( + value: logsProvider.selectedClients, + dialog: false, + ), + isScrollControlled: true, + backgroundColor: Colors.transparent + ); + } } void filterLogs() async { @@ -161,47 +197,45 @@ class _LogsFiltersModalWidgetState extends State { } } - return Padding( - padding: MediaQuery.of(context).viewInsets, - child: Container( - height: Platform.isIOS ? 446 : 430, - decoration: BoxDecoration( - color: Theme.of(context).dialogBackgroundColor, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28) - ) - ), - child: Column( - children: [ - Expanded( - child: ListView( - physics: (Platform.isIOS ? 416 : 400) < MediaQuery.of(context).size.height - ? const NeverScrollableScrollPhysics() - : null, + Widget content() { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: SingleChildScrollView( + child: Wrap( children: [ - Padding( - padding: const EdgeInsets.only( - top: 24, - bottom: 16, - ), - child: Icon( - Icons.filter_list_rounded, - size: 24, - color: Theme.of(context).listTileTheme.iconColor - ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + Padding( + padding: const EdgeInsets.only( + top: 24, + bottom: 16, + ), + child: Icon( + Icons.filter_list_rounded, + size: 24, + color: Theme.of(context).listTileTheme.iconColor + ), + ), + Text( + AppLocalizations.of(context)!.filters, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w400, + height: 1.3, + color: Theme.of(context).colorScheme.onSurface + ), + ), + const SizedBox(height: 16), + ], + ), + ], ), - Text( - AppLocalizations.of(context)!.filters, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.w400, - height: 1.3, - color: Theme.of(context).colorScheme.onSurface - ), - ), - const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: Row( @@ -233,7 +267,7 @@ class _LogsFiltersModalWidgetState extends State { ], ), ), - const SizedBox(height: 16), + Container(height: 16), CustomListTile( title: AppLocalizations.of(context)!.client, subtitle: logsProvider.selectedClients != null @@ -270,26 +304,55 @@ class _LogsFiltersModalWidgetState extends State { ], ), ), - Padding( - padding: const EdgeInsets.all(24), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - TextButton( - onPressed: resetFilters, - child: Text(AppLocalizations.of(context)!.resetFilters) - ), - TextButton( - onPressed: () => filterLogs(), - child: Text(AppLocalizations.of(context)!.apply) - ), - ], - ), + ), + Padding( + padding: const EdgeInsets.all(24), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + onPressed: resetFilters, + child: Text(AppLocalizations.of(context)!.resetFilters) + ), + TextButton( + onPressed: () => filterLogs(), + child: Text(AppLocalizations.of(context)!.apply) + ), + ], ), - if (Platform.isIOS) const SizedBox(height: 16) - ], + ), + if (Platform.isIOS) const SizedBox(height: 16) + ], + ); + } + + if (widget.dialog == true) { + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500 + ), + child: content() + ) ), - ), - ); + ); + } + else { + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).dialogBackgroundColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28) + ) + ), + child: content() + ), + ); + } } } \ No newline at end of file diff --git a/lib/screens/servers/servers.dart b/lib/screens/servers/servers.dart index 9e98e29..ba1e7b3 100644 --- a/lib/screens/servers/servers.dart +++ b/lib/screens/servers/servers.dart @@ -13,7 +13,12 @@ import 'package:adguard_home_manager/providers/servers_provider.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; class Servers extends StatefulWidget { - const Servers({Key? key}) : super(key: key); + final double? breakingWidth; + + const Servers({ + Key? key, + this.breakingWidth + }) : super(key: key); @override State createState() => _ServersState(); @@ -55,16 +60,31 @@ class _ServersState extends State { final serversProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + for (var i = 0; i < serversProvider.serversList.length; i++) { expandableControllerList.add(ExpandableController()); } void openAddServerModal() async { await Future.delayed(const Duration(seconds: 0), (() => { - Navigator.push(context, MaterialPageRoute( - fullscreenDialog: true, - builder: (BuildContext context) => const AddServerModal() - )) + if (width > 700) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => const AddServerModal( + window: true, + ), + ) + } + else { + Navigator.push(context, MaterialPageRoute( + fullscreenDialog: true, + builder: (BuildContext context) => const AddServerModal( + window: false, + ) + )) + } })); } @@ -79,7 +99,8 @@ class _ServersState extends State { context: context, controllers: expandableControllerList, onChange: expandOrContract, - scrollController: scrollController + scrollController: scrollController, + breakingWidth: widget.breakingWidth ?? 700, ), AnimatedPositioned( duration: const Duration(milliseconds: 100), diff --git a/lib/screens/settings/access_settings/access_settings.dart b/lib/screens/settings/access_settings/access_settings.dart index 9fc8a86..9f03328 100644 --- a/lib/screens/settings/access_settings/access_settings.dart +++ b/lib/screens/settings/access_settings/access_settings.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -72,78 +74,114 @@ class _AccessSettingsWidgetState extends State with Ticker @override Widget build(BuildContext context) { final serversProvider = Provider.of(context); - return Scaffold( - body: DefaultTabController( - length: 3, - child: NestedScrollView( - controller: scrollController, - headerSliverBuilder: ((context, innerBoxIsScrolled) { - return [ - SliverOverlapAbsorber( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - sliver: SliverSafeArea( - top: false, - sliver: SliverAppBar( - title: Text(AppLocalizations.of(context)!.accessSettings), - pinned: true, - floating: true, - centerTitle: false, - forceElevated: innerBoxIsScrolled, - bottom: TabBar( - controller: tabController, - isScrollable: true, - unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant, - tabs: [ - Tab( - icon: const Icon(Icons.check), - text: AppLocalizations.of(context)!.allowedClients, - ), - Tab( - icon: const Icon(Icons.block), - text: AppLocalizations.of(context)!.disallowedClients, - ), - Tab( - icon: const Icon(Icons.link_rounded), - text: AppLocalizations.of(context)!.disallowedDomains, - ), - ] - ) + + Widget body() { + return TabBarView( + controller: tabController, + children: [ + ClientsList( + type: 'allowed', + scrollController: scrollController, + loadStatus: serversProvider.clients.loadStatus, + data: serversProvider.clients.loadStatus == LoadStatus.loaded + ? serversProvider.clients.data!.clientsAllowedBlocked!.allowedClients : [], + fetchClients: fetchClients + ), + ClientsList( + type: 'disallowed', + scrollController: scrollController, + loadStatus: serversProvider.clients.loadStatus, + data: serversProvider.clients.loadStatus == LoadStatus.loaded + ? serversProvider.clients.data!.clientsAllowedBlocked!.disallowedClients : [], + fetchClients: fetchClients + ), + ClientsList( + type: 'domains', + scrollController: scrollController, + loadStatus: serversProvider.clients.loadStatus, + data: serversProvider.clients.loadStatus == LoadStatus.loaded + ? serversProvider.clients.data!.clientsAllowedBlocked!.blockedHosts : [], + fetchClients: fetchClients + ), + ] + ); + } + + PreferredSizeWidget tabBar() { + return TabBar( + controller: tabController, + isScrollable: true, + unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant, + tabs: [ + Tab( + child: Row( + children: [ + const Icon(Icons.check), + const SizedBox(width: 8), + Text(AppLocalizations.of(context)!.allowedClients) + ], + ), + ), + Tab( + child: Row( + children: [ + const Icon(Icons.block), + const SizedBox(width: 8), + Text(AppLocalizations.of(context)!.disallowedClients) + ], + ), + ), + Tab( + child: Row( + children: [ + const Icon(Icons.link_rounded), + const SizedBox(width: 8), + Text(AppLocalizations.of(context)!.disallowedDomains) + ], + ), + ), + ] + ); + } + + if (Platform.isAndroid || Platform.isIOS) { + return Scaffold( + body: DefaultTabController( + length: 3, + child: NestedScrollView( + controller: scrollController, + headerSliverBuilder: ((context, innerBoxIsScrolled) { + return [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverSafeArea( + top: false, + sliver: SliverAppBar( + title: Text(AppLocalizations.of(context)!.accessSettings), + pinned: true, + floating: true, + centerTitle: false, + forceElevated: innerBoxIsScrolled, + bottom: tabBar() + ), ), - ), - ) - ]; - }), - body: TabBarView( - controller: tabController, - children: [ - ClientsList( - type: 'allowed', - scrollController: scrollController, - loadStatus: serversProvider.clients.loadStatus, - data: serversProvider.clients.loadStatus == LoadStatus.loaded - ? serversProvider.clients.data!.clientsAllowedBlocked!.allowedClients : [], - fetchClients: fetchClients - ), - ClientsList( - type: 'disallowed', - scrollController: scrollController, - loadStatus: serversProvider.clients.loadStatus, - data: serversProvider.clients.loadStatus == LoadStatus.loaded - ? serversProvider.clients.data!.clientsAllowedBlocked!.disallowedClients : [], - fetchClients: fetchClients - ), - ClientsList( - type: 'domains', - scrollController: scrollController, - loadStatus: serversProvider.clients.loadStatus, - data: serversProvider.clients.loadStatus == LoadStatus.loaded - ? serversProvider.clients.data!.clientsAllowedBlocked!.blockedHosts : [], - fetchClients: fetchClients - ), - ] + ) + ]; + }), + body: body() ) - ) - ), - ); + ), + ); + } + else { + return Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context)!.accessSettings), + centerTitle: false, + bottom: tabBar() + ), + body: body(), + ); + } } } \ No newline at end of file diff --git a/lib/screens/settings/access_settings/add_client_modal.dart b/lib/screens/settings/access_settings/add_client_modal.dart index 385409b..c7e4012 100644 --- a/lib/screens/settings/access_settings/add_client_modal.dart +++ b/lib/screens/settings/access_settings/add_client_modal.dart @@ -6,11 +6,13 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class AddClientModal extends StatefulWidget { final String type; final void Function(String, String) onConfirm; + final bool dialog; const AddClientModal({ Key? key, required this.type, - required this.onConfirm + required this.onConfirm, + required this.dialog, }) : super(key: key); @override @@ -65,59 +67,61 @@ class _AddClientModalState extends State { } } - return Padding( - padding: MediaQuery.of(context).viewInsets, - child: Container( - height: Platform.isIOS ? 321 : 305, + Widget content() { + return Padding( padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - color: Theme.of(context).dialogBackgroundColor, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28) - ) - ), child: Column( + mainAxisSize: MainAxisSize.min, children: [ - Expanded( - child: ListView( - physics: (Platform.isIOS ? 338 : 322) < MediaQuery.of(context).size.height - ? const NeverScrollableScrollPhysics() - : null, - children: [ - Icon( - icon(), - size: 24, - color: Theme.of(context).listTileTheme.iconColor - ), - const SizedBox(height: 16), - Text( - title(), - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSurface + Flexible( + child: SingleChildScrollView( + child: Wrap( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icon(), + size: 24, + color: Theme.of(context).listTileTheme.iconColor + ), + ], ), - ), - const SizedBox(height: 16), - TextFormField( - controller: fieldController, - onChanged: (_) => checkValidValues(), - decoration: InputDecoration( - prefixIcon: const Icon(Icons.link_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + title(), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + color: Theme.of(context).colorScheme.onSurface + ), + ), + ], ), - helperText: widget.type == 'allowed' || widget.type == 'disallowed' - ? AppLocalizations.of(context)!.addClientFieldDescription : null, - labelText: widget.type == 'allowed' || widget.type == 'disallowed' - ? AppLocalizations.of(context)!.clientIdentifier - : AppLocalizations.of(context)!.domain, ), - ), - ], + TextFormField( + controller: fieldController, + onChanged: (_) => checkValidValues(), + decoration: InputDecoration( + prefixIcon: const Icon(Icons.link_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + helperText: widget.type == 'allowed' || widget.type == 'disallowed' + ? AppLocalizations.of(context)!.addClientFieldDescription : null, + labelText: widget.type == 'allowed' || widget.type == 'disallowed' + ? AppLocalizations.of(context)!.clientIdentifier + : AppLocalizations.of(context)!.domain, + ), + ), + ], + ), ), ), Padding( @@ -129,7 +133,7 @@ class _AddClientModalState extends State { onPressed: () => Navigator.pop(context), child: Text(AppLocalizations.of(context)!.cancel) ), - const SizedBox(width: 20), + const SizedBox(width: 16), TextButton( onPressed: validData == true ? () { @@ -149,10 +153,38 @@ class _AddClientModalState extends State { ], ), ), - if (Platform.isIOS) const SizedBox(height: 16) ], ), - ), - ); + ); + } + + if (widget.dialog == true) { + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 400 + ), + child: content() + ), + ), + ); + } + else { + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).dialogBackgroundColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28) + ) + ), + child: content() + ), + ); + } } } \ No newline at end of file diff --git a/lib/screens/settings/access_settings/clients_list.dart b/lib/screens/settings/access_settings/clients_list.dart index 6e561db..5b495a1 100644 --- a/lib/screens/settings/access_settings/clients_list.dart +++ b/lib/screens/settings/access_settings/clients_list.dart @@ -1,5 +1,7 @@ // ignore_for_file: use_build_context_synchronously +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:provider/provider.dart'; @@ -68,6 +70,8 @@ class _ClientsListState extends State { final serversProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + void confirmRemoveItem(String client, String type) async { Map> body = { "allowed_clients": serversProvider.clients.data!.clientsAllowedBlocked?.allowedClients ?? [], @@ -209,6 +213,7 @@ class _ClientsListState extends State { } return CustomTabContentList( + noSliver: !(Platform.isAndroid || Platform.isIOS) ? true : false, loadingGenerator: () => SizedBox( width: double.maxFinite, height: MediaQuery.of(context).size.height-171, @@ -362,15 +367,28 @@ class _ClientsListState extends State { refreshIndicatorOffset: 0, fab: FloatingActionButton( onPressed: () { - showModalBottomSheet( - context: context, - builder: (context) => AddClientModal( - type: widget.type, - onConfirm: confirmAddItem - ), - backgroundColor: Colors.transparent, - isScrollControlled: true - ); + if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + context: context, + builder: (context) => AddClientModal( + type: widget.type, + onConfirm: confirmAddItem, + dialog: true, + ), + ); + } + else { + showModalBottomSheet( + context: context, + builder: (context) => AddClientModal( + type: widget.type, + onConfirm: confirmAddItem, + dialog: false, + ), + backgroundColor: Colors.transparent, + isScrollControlled: true + ); + } }, child: const Icon(Icons.add), ), diff --git a/lib/screens/settings/advanced_setings.dart b/lib/screens/settings/advanced_setings.dart index 987d406..0012259 100644 --- a/lib/screens/settings/advanced_setings.dart +++ b/lib/screens/settings/advanced_setings.dart @@ -1,6 +1,9 @@ // ignore_for_file: use_build_context_synchronously +import 'dart:io'; + import 'package:flutter/material.dart'; +import 'package:flutter_split_view/flutter_split_view.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -15,6 +18,8 @@ class AdvancedSettings extends StatelessWidget { @override Widget build(BuildContext context) { final appConfigProvider = Provider.of(context); + + final width = MediaQuery.of(context).size.width; Future updateSslCheck(bool newStatus) async { final result = await appConfigProvider.setOverrideSslCheck(newStatus); @@ -64,11 +69,16 @@ class AdvancedSettings extends StatelessWidget { title: AppLocalizations.of(context)!.logs, subtitle: AppLocalizations.of(context)!.checkAppLogs, onTap: () => { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const AppLogs() + if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) { + SplitView.of(context).push(const AppLogs()) + } + else { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const AppLogs() + ) ) - ) + } }, padding: const EdgeInsets.only( top: 10, diff --git a/lib/screens/settings/dhcp/add_static_lease_modal.dart b/lib/screens/settings/dhcp/add_static_lease_modal.dart index 2c8e316..a7f2f5f 100644 --- a/lib/screens/settings/dhcp/add_static_lease_modal.dart +++ b/lib/screens/settings/dhcp/add_static_lease_modal.dart @@ -5,10 +5,12 @@ import 'package:adguard_home_manager/models/dhcp.dart'; class AddStaticLeaseModal extends StatefulWidget { final void Function(Lease) onConfirm; + final bool dialog; const AddStaticLeaseModal({ Key? key, required this.onConfirm, + required this.dialog }) : super(key: key); @override @@ -65,45 +67,47 @@ class _AddStaticLeaseModalState extends State { @override Widget build(BuildContext context) { - return Padding( - padding: MediaQuery.of(context).viewInsets, - child: Container( - height: 510, - decoration: BoxDecoration( - color: Theme.of(context).dialogBackgroundColor, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28) - ) - ), - child: Column( - children: [ - Expanded( - child: ListView( - physics: 550 < MediaQuery.of(context).size.height - ? const NeverScrollableScrollPhysics() - : null, + Widget content() { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: SingleChildScrollView( + child: Wrap( children: [ Padding( - padding: const EdgeInsets.only(top: 24), - child: Icon( - Icons.add, - size: 24, - color: Theme.of(context).listTileTheme.iconColor + padding: const EdgeInsets.only(bottom: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 24), + child: Icon( + Icons.add, + size: 24, + color: Theme.of(context).listTileTheme.iconColor + ), + ), + const SizedBox(height: 16), + Text( + AppLocalizations.of(context)!.addStaticLease, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + color: Theme.of(context).colorScheme.onSurface + ), + ), + ], + ), + ], ), ), - const SizedBox(height: 16), - Text( - AppLocalizations.of(context)!.addStaticLease, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSurface - ), - ), - const SizedBox(height: 16), Padding( - padding: const EdgeInsets.symmetric(horizontal: 28), + padding: const EdgeInsets.only( + left: 24, right: 24, bottom: 12 + ), child: TextFormField( controller: macController, onChanged: validateMac, @@ -119,9 +123,8 @@ class _AddStaticLeaseModalState extends State { ), ), ), - const SizedBox(height: 30), Padding( - padding: const EdgeInsets.symmetric(horizontal: 28), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), child: TextFormField( controller: ipController, onChanged: validateIp, @@ -137,9 +140,10 @@ class _AddStaticLeaseModalState extends State { ), ), ), - const SizedBox(height: 30), Padding( - padding: const EdgeInsets.symmetric(horizontal: 28), + padding: const EdgeInsets.only( + left: 24, right: 24, top: 12 + ), child: TextFormField( controller: hostNameController, onChanged: (value) { @@ -166,44 +170,70 @@ class _AddStaticLeaseModalState extends State { ], ), ), - Padding( - padding: const EdgeInsets.all(20), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(AppLocalizations.of(context)!.cancel), - ), - const SizedBox(width: 20), - TextButton( - onPressed: validData == true - ? () { - Navigator.pop(context); - widget.onConfirm( - Lease( - mac: macController.text, - hostname: hostNameController.text, - ip: ipController.text - ) - ); - } - : null, - child: Text( - AppLocalizations.of(context)!.confirm, - style: TextStyle( - color: validData == true - ? Theme.of(context).colorScheme.primary - : Colors.grey - ), + ), + Padding( + padding: const EdgeInsets.all(24), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.cancel), + ), + const SizedBox(width: 20), + TextButton( + onPressed: validData == true + ? () { + Navigator.pop(context); + widget.onConfirm( + Lease( + mac: macController.text, + hostname: hostNameController.text, + ip: ipController.text + ) + ); + } + : null, + child: Text( + AppLocalizations.of(context)!.confirm, + style: TextStyle( + color: validData == true + ? Theme.of(context).colorScheme.primary + : Colors.grey ), ), - ], - ), - ) - ], + ), + ], + ), + ) + ], + ); + } + + if (widget.dialog == true) { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 400 + ), + child: content(), ), - ), - ); + ); + } + else { + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).dialogBackgroundColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28) + ) + ), + child: content() + ), + ); + } } } \ No newline at end of file diff --git a/lib/screens/settings/dhcp/dhcp.dart b/lib/screens/settings/dhcp/dhcp.dart index a4a8319..a12fe81 100644 --- a/lib/screens/settings/dhcp/dhcp.dart +++ b/lib/screens/settings/dhcp/dhcp.dart @@ -1,8 +1,10 @@ // ignore_for_file: use_build_context_synchronously +import 'dart:io'; + import 'package:flutter/material.dart'; +import 'package:flutter_split_view/flutter_split_view.dart'; import 'package:provider/provider.dart'; -import 'package:bottom_sheet/bottom_sheet.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/widgets/section_label.dart'; @@ -204,7 +206,7 @@ class _DhcpWidgetState extends State { @override void initState() { - loadDhcpStatus(); + if (mounted) loadDhcpStatus(); super.initState(); } @@ -213,6 +215,8 @@ class _DhcpWidgetState extends State { final serversProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + void saveSettings() async { ProcessModal processModal = ProcessModal(context: context); processModal.open(AppLocalizations.of(context)!.savingSettings); @@ -354,24 +358,33 @@ class _DhcpWidgetState extends State { void selectInterface() { ScaffoldMessenger.of(context).clearSnackBars(); Future.delayed(const Duration(seconds: 0), () { - showFlexibleBottomSheet( - minHeight: 0.6, - initHeight: 0.6, - maxHeight: 0.95, - isCollapsible: true, - duration: const Duration(milliseconds: 250), - anchors: [0.95], - context: context, - builder: (ctx, controller, offset) => SelectInterfaceModal( - interfaces: serversProvider.dhcp.data!.networkInterfaces, - scrollController: controller, - onSelect: (interface) => setState(() { - clearAll(); - selectedInterface = interface; - }) - ), - bottomSheetColor: Colors.transparent - ); + if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + context: context, + builder: (context) => SelectInterfaceModal( + interfaces: serversProvider.dhcp.data!.networkInterfaces, + onSelect: (interface) => setState(() { + clearAll(); + selectedInterface = interface; + }), + dialog: true, + ) + ); + } + else { + showModalBottomSheet( + context: context, + builder: (context) => SelectInterfaceModal( + interfaces: serversProvider.dhcp.data!.networkInterfaces, + onSelect: (i) => setState(() { + clearAll(); + selectedInterface = i; + }), + dialog: false, + ), + isScrollControlled: true + ); + } }); } @@ -399,334 +412,436 @@ class _DhcpWidgetState extends State { case 1: if (selectedInterface != null) { - return ListView( - children: [ - Padding( - padding: const EdgeInsets.only( - top: 10, - left: 16, - right: 16 - ), - child: Material( - color: Theme.of(context).colorScheme.primary.withOpacity(0.1), - borderRadius: BorderRadius.circular(28), - child: InkWell( - onTap: selectedInterface != null - ? () => setState(() => enabled = !enabled) - : null, + return SingleChildScrollView( + child: Wrap( + children: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + left: 16, + right: 16 + ), + child: Material( + color: Theme.of(context).colorScheme.primary.withOpacity(0.1), borderRadius: BorderRadius.circular(28), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 12 + child: InkWell( + onTap: selectedInterface != null + ? () => setState(() => enabled = !enabled) + : null, + borderRadius: BorderRadius.circular(28), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 12 + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppLocalizations.of(context)!.enableDhcpServer, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface + ), + ), + if (selectedInterface != null) ...[ + Text( + selectedInterface!.name, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).listTileTheme.textColor + ), + ) + ] + ], + ), + Switch( + value: enabled, + onChanged: selectedInterface != null + ? (value) => setState(() => enabled = value) + : null, + ), + ], + ), ), + ), + ), + ), + if (selectedInterface!.ipv4Addresses.isNotEmpty) ...[ + SectionLabel( + label: AppLocalizations.of(context)!.ipv4settings, + padding: const EdgeInsets.only( + top: 24, left: 16, right: 16, bottom: 8 + ) + ), + FractionallySizedBox( + widthFactor: width > 900 ? 0.5 : 1, + child: Padding( + padding: width > 900 + ? const EdgeInsets.only(top: 12, bottom: 12, left: 16, right: 8) + : const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: TextFormField( + controller: ipv4StartRangeController, + onChanged: (value) => validateIpV4(value, 'ipv4StartRangeError', AppLocalizations.of(context)!.ipNotValid), + decoration: InputDecoration( + prefixIcon: const Icon(Icons.skip_previous_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + errorText: ipv4StartRangeError, + labelText: AppLocalizations.of(context)!.startOfRange, + ), + keyboardType: TextInputType.number, + ), + ), + ), + FractionallySizedBox( + widthFactor: width > 900 ? 0.5 : 1, + child: Padding( + padding: width > 900 + ? const EdgeInsets.only(top: 12, bottom: 12, left: 8, right: 16) + : const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: TextFormField( + controller: ipv4EndRangeController, + onChanged: (value) => validateIpV4(value, 'ipv4EndRangeError', AppLocalizations.of(context)!.ipNotValid), + decoration: InputDecoration( + prefixIcon: const Icon(Icons.skip_next_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + errorText: ipv4EndRangeError, + labelText: AppLocalizations.of(context)!.endOfRange, + ), + keyboardType: TextInputType.number, + ), + ), + ), + FractionallySizedBox( + widthFactor: width > 900 ? 0.5 : 1, + child: Padding( + padding: width > 900 + ? const EdgeInsets.only(top: 12, bottom: 12, left: 16, right: 8) + : const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: TextFormField( + controller: ipv4SubnetMaskController, + onChanged: (value) => validateIpV4(value, 'ipv4SubnetMaskError', AppLocalizations.of(context)!.subnetMaskNotValid), + decoration: InputDecoration( + prefixIcon: const Icon(Icons.hub_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + errorText: ipv4SubnetMaskError, + labelText: AppLocalizations.of(context)!.subnetMask, + ), + keyboardType: TextInputType.number, + ), + ), + ), + FractionallySizedBox( + widthFactor: width > 900 ? 0.5 : 1, + child: Padding( + padding: width > 900 + ? const EdgeInsets.only(top: 12, bottom: 12, left: 8, right: 16) + : const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: TextFormField( + controller: ipv4GatewayController, + onChanged: (value) => validateIpV4(value, 'ipv4GatewayError', AppLocalizations.of(context)!.gatewayNotValid), + decoration: InputDecoration( + prefixIcon: const Icon(Icons.router_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + errorText: ipv4GatewayError, + labelText: AppLocalizations.of(context)!.gateway, + ), + keyboardType: TextInputType.number, + ), + ), + ), + FractionallySizedBox( + widthFactor: 1, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: TextFormField( + controller: ipv4LeaseTimeController, + onChanged: (value) { + if (int.tryParse(value).runtimeType == int) { + setState(() => ipv4LeaseTimeError = null); + } + else { + setState(() => ipv4LeaseTimeError = AppLocalizations.of(context)!.leaseTimeNotValid); + } + }, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.timer), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + errorText: ipv4LeaseTimeError, + labelText: AppLocalizations.of(context)!.leaseTime, + ), + keyboardType: TextInputType.number, + ), + ), + ), + ], + if (selectedInterface!.ipv6Addresses.isNotEmpty) ...[ + SectionLabel( + label: AppLocalizations.of(context)!.ipv6settings, + padding: const EdgeInsets.all(16) + ), + FractionallySizedBox( + widthFactor: width > 900 ? 0.5 : 1, + child: Padding( + padding: width > 900 + ? const EdgeInsets.only(top: 8, bottom: 12, left: 16, right: 8) + : const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: TextFormField( + controller: ipv6StartRangeController, + onChanged: (value) => validateIpV4(value, 'ipv6StartRangeError', AppLocalizations.of(context)!.ipNotValid), + decoration: InputDecoration( + prefixIcon: const Icon(Icons.skip_next_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + errorText: ipv6StartRangeError, + labelText: AppLocalizations.of(context)!.startOfRange, + ), + keyboardType: TextInputType.number, + ), + ), + ), + FractionallySizedBox( + widthFactor: width > 900 ? 0.5 : 1, + child: Padding( + padding: width > 900 + ? const EdgeInsets.only(top: 8, bottom: 12, left: 8, right: 16) + : const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: TextFormField( + controller: ipv6EndRangeController, + onChanged: (value) => validateIpV4(value, 'ipv6EndRangeError', AppLocalizations.of(context)!.ipNotValid), + decoration: InputDecoration( + prefixIcon: const Icon(Icons.skip_previous_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + errorText: ipv6EndRangeError, + labelText: AppLocalizations.of(context)!.endOfRange, + ), + keyboardType: TextInputType.number, + ), + ), + ), + FractionallySizedBox( + widthFactor: 1, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: TextFormField( + controller: ipv6LeaseTimeController, + onChanged: (value) { + if (int.tryParse(value).runtimeType == int) { + setState(() => ipv6LeaseTimeError = null); + } + else { + setState(() => ipv6LeaseTimeError = AppLocalizations.of(context)!.leaseTimeNotValid); + } + }, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.timer), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + errorText: ipv6LeaseTimeError, + labelText: AppLocalizations.of(context)!.leaseTime, + ), + keyboardType: TextInputType.number, + ), + ), + ), + ], + const SizedBox(height: 20), + SectionLabel( + label: AppLocalizations.of(context)!.dhcpLeases, + padding: const EdgeInsets.all(16), + ), + if (width <= 900) Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + Navigator.push(context, MaterialPageRoute( + builder: (context) => DhcpLeases( + items: serversProvider.dhcp.data!.dhcpStatus.leases, + staticLeases: false, + ) + )); + }, + child: Container( + padding: const EdgeInsets.all(16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - AppLocalizations.of(context)!.enableDhcpServer, - style: TextStyle( - fontSize: 16, - color: Theme.of(context).colorScheme.onSurface - ), - ), - if (selectedInterface != null) ...[ - Text( - selectedInterface!.name, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).listTileTheme.textColor - ), - ) - ] - ], - ), - Switch( - value: enabled, - onChanged: selectedInterface != null - ? (value) => setState(() => enabled = value) - : null, + Text( + AppLocalizations.of(context)!.dhcpLeases, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface, + ), ), + Icon( + Icons.arrow_forward_rounded, + color: Theme.of(context).colorScheme.onSurface, + ) ], ), ), ), ), - ), - if (selectedInterface!.ipv4Addresses.isNotEmpty) ...[ - SectionLabel( - label: AppLocalizations.of(context)!.ipv4settings, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: TextFormField( - controller: ipv4StartRangeController, - onChanged: (value) => validateIpV4(value, 'ipv4StartRangeError', AppLocalizations.of(context)!.ipNotValid), - decoration: InputDecoration( - prefixIcon: const Icon(Icons.skip_previous_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) + if (width <= 900) Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + Navigator.push(context, MaterialPageRoute( + builder: (context) => DhcpLeases( + items: serversProvider.dhcp.data!.dhcpStatus.staticLeases, + staticLeases: true, ) - ), - errorText: ipv4StartRangeError, - labelText: AppLocalizations.of(context)!.startOfRange, - ), - keyboardType: TextInputType.number, - ), - ), - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: TextFormField( - controller: ipv4EndRangeController, - onChanged: (value) => validateIpV4(value, 'ipv4EndRangeError', AppLocalizations.of(context)!.ipNotValid), - decoration: InputDecoration( - prefixIcon: const Icon(Icons.skip_next_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - errorText: ipv4EndRangeError, - labelText: AppLocalizations.of(context)!.endOfRange, - ), - keyboardType: TextInputType.number, - ), - ), - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: TextFormField( - controller: ipv4SubnetMaskController, - onChanged: (value) => validateIpV4(value, 'ipv4SubnetMaskError', AppLocalizations.of(context)!.subnetMaskNotValid), - decoration: InputDecoration( - prefixIcon: const Icon(Icons.hub_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - errorText: ipv4SubnetMaskError, - labelText: AppLocalizations.of(context)!.subnetMask, - ), - keyboardType: TextInputType.number, - ), - ), - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: TextFormField( - controller: ipv4GatewayController, - onChanged: (value) => validateIpV4(value, 'ipv4GatewayError', AppLocalizations.of(context)!.gatewayNotValid), - decoration: InputDecoration( - prefixIcon: const Icon(Icons.router_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - errorText: ipv4GatewayError, - labelText: AppLocalizations.of(context)!.gateway, - ), - keyboardType: TextInputType.number, - ), - ), - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: TextFormField( - controller: ipv4LeaseTimeController, - onChanged: (value) { - if (int.tryParse(value).runtimeType == int) { - setState(() => ipv4LeaseTimeError = null); - } - else { - setState(() => ipv4LeaseTimeError = AppLocalizations.of(context)!.leaseTimeNotValid); - } + )); }, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.timer), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - errorText: ipv4LeaseTimeError, - labelText: AppLocalizations.of(context)!.leaseTime, - ), - keyboardType: TextInputType.number, - ), - ), - ], - if (selectedInterface!.ipv6Addresses.isNotEmpty) ...[ - SectionLabel( - label: AppLocalizations.of(context)!.ipv6settings, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: TextFormField( - controller: ipv6StartRangeController, - onChanged: (value) => validateIpV4(value, 'ipv6StartRangeError', AppLocalizations.of(context)!.ipNotValid), - decoration: InputDecoration( - prefixIcon: const Icon(Icons.skip_next_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - errorText: ipv6StartRangeError, - labelText: AppLocalizations.of(context)!.startOfRange, - ), - keyboardType: TextInputType.number, - ), - ), - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: TextFormField( - controller: ipv6EndRangeController, - onChanged: (value) => validateIpV4(value, 'ipv6EndRangeError', AppLocalizations.of(context)!.ipNotValid), - decoration: InputDecoration( - prefixIcon: const Icon(Icons.skip_previous_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - errorText: ipv6EndRangeError, - labelText: AppLocalizations.of(context)!.endOfRange, - ), - keyboardType: TextInputType.number, - ), - ), - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: TextFormField( - controller: ipv6LeaseTimeController, - onChanged: (value) { - if (int.tryParse(value).runtimeType == int) { - setState(() => ipv6LeaseTimeError = null); - } - else { - setState(() => ipv6LeaseTimeError = AppLocalizations.of(context)!.leaseTimeNotValid); - } - }, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.timer), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - errorText: ipv6LeaseTimeError, - labelText: AppLocalizations.of(context)!.leaseTime, - ), - keyboardType: TextInputType.number, - ), - ), - ], - const SizedBox(height: 20), - SectionLabel( - label: AppLocalizations.of(context)!.dhcpLeases, - ), - Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - Navigator.push(context, MaterialPageRoute( - builder: (context) => DhcpLeases( - items: serversProvider.dhcp.data!.dhcpStatus.leases, - staticLeases: false, - ) - )); - }, - child: Container( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - AppLocalizations.of(context)!.dhcpLeases, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, - color: Theme.of(context).colorScheme.onSurface, + child: Container( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + AppLocalizations.of(context)!.dhcpStatic, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface, + ), ), - ), - Icon( - Icons.arrow_forward_rounded, - color: Theme.of(context).colorScheme.onSurface, - ) - ], - ), - ), - ), - ), - Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - Navigator.push(context, MaterialPageRoute( - builder: (context) => DhcpLeases( - items: serversProvider.dhcp.data!.dhcpStatus.staticLeases, - staticLeases: true, - ) - )); - }, - child: Container( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - AppLocalizations.of(context)!.dhcpStatic, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, + Icon( + Icons.arrow_forward_rounded, color: Theme.of(context).colorScheme.onSurface, - ), - ), - Icon( - Icons.arrow_forward_rounded, - color: Theme.of(context).colorScheme.onSurface, - ) - ], + ) + ], + ), ), ), ), - ), - const SizedBox(height: 10) - ], + if (width > 900) Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + onPressed: () { + if (!(Platform.isAndroid || Platform.isIOS)) { + SplitView.of(context).push( + DhcpLeases( + items: serversProvider.dhcp.data!.dhcpStatus.leases, + staticLeases: false, + ) + ); + } + else { + Navigator.push(context, MaterialPageRoute( + builder: (context) => DhcpLeases( + items: serversProvider.dhcp.data!.dhcpStatus.leases, + staticLeases: false, + ) + )); + } + }, + child: Row( + children: [ + Text(AppLocalizations.of(context)!.dhcpLeases), + const SizedBox(width: 8), + const Icon(Icons.arrow_forward_rounded) + ], + ) + ), + ElevatedButton( + onPressed: () { + if (!(Platform.isAndroid || Platform.isIOS)) { + SplitView.of(context).push( + DhcpLeases( + items: serversProvider.dhcp.data!.dhcpStatus.staticLeases, + staticLeases: true, + ) + ); + } + else { + Navigator.push(context, MaterialPageRoute( + builder: (context) => DhcpLeases( + items: serversProvider.dhcp.data!.dhcpStatus.staticLeases, + staticLeases: true, + ) + )); + } + }, + child: Row( + children: [ + Text(AppLocalizations.of(context)!.dhcpStatic), + const SizedBox(width: 8), + const Icon(Icons.arrow_forward_rounded) + ], + ) + ), + ], + ), + const SizedBox(height: 10) + ], + ), ); } else { - return Column( - mainAxisSize: MainAxisSize.max, + return Row( mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Text( - AppLocalizations.of(context)!.neededSelectInterface, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5) - ), + Flexible( + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + AppLocalizations.of(context)!.neededSelectInterface, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5) + ), + ), + ), + const SizedBox(height: 30), + ElevatedButton( + onPressed: selectInterface, + child: Text(AppLocalizations.of(context)!.selectInterface) + ), + ], ), ), - const SizedBox(height: 30), - ElevatedButton( - onPressed: selectInterface, - child: Text(AppLocalizations.of(context)!.selectInterface) - ), ], ); } diff --git a/lib/screens/settings/dhcp/dhcp_leases.dart b/lib/screens/settings/dhcp/dhcp_leases.dart index aed4f47..79f478a 100644 --- a/lib/screens/settings/dhcp/dhcp_leases.dart +++ b/lib/screens/settings/dhcp/dhcp_leases.dart @@ -1,5 +1,7 @@ // ignore_for_file: use_build_context_synchronously +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:animations/animations.dart'; @@ -30,6 +32,8 @@ class DhcpLeases extends StatelessWidget { final serversProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + void deleteLease(Lease lease) async { ProcessModal processModal = ProcessModal(context: context); processModal.open(AppLocalizations.of(context)!.deleting); @@ -119,14 +123,26 @@ class DhcpLeases extends StatelessWidget { } void openAddStaticLease() { - showModalBottomSheet( - context: context, - builder: (context) => AddStaticLeaseModal( - onConfirm: createLease - ), - backgroundColor: Colors.transparent, - isScrollControlled: true - ); + if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + context: context, + builder: (context) => AddStaticLeaseModal( + onConfirm: createLease, + dialog: true, + ), + ); + } + else { + showModalBottomSheet( + context: context, + builder: (context) => AddStaticLeaseModal( + onConfirm: createLease, + dialog: false, + ), + backgroundColor: Colors.transparent, + isScrollControlled: true + ); + } } return Scaffold( diff --git a/lib/screens/settings/dhcp/select_interface_modal.dart b/lib/screens/settings/dhcp/select_interface_modal.dart index 202281c..9827f11 100644 --- a/lib/screens/settings/dhcp/select_interface_modal.dart +++ b/lib/screens/settings/dhcp/select_interface_modal.dart @@ -7,173 +7,177 @@ import 'package:adguard_home_manager/models/dhcp.dart'; class SelectInterfaceModal extends StatelessWidget { final List interfaces; - final ScrollController scrollController; final void Function(NetworkInterface) onSelect; + final bool dialog; const SelectInterfaceModal({ Key? key, required this.interfaces, - required this.scrollController, required this.onSelect, + required this.dialog }) : super(key: key); @override Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - color: Theme.of(context).dialogBackgroundColor, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28) - ) - ), - child: Column( + Widget content() { + return Column( + mainAxisSize: MainAxisSize.min, children: [ - Expanded( - child: ListView( - controller: scrollController, - children: [ - Padding( - padding: const EdgeInsets.only(top: 24), - child: Icon( - Icons.settings_ethernet_rounded, - size: 24, - color: Theme.of(context).listTileTheme.iconColor - ), - ), - const SizedBox(height: 16), - Text( - AppLocalizations.of(context)!.selectInterface, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSurface - ), - ), - const SizedBox(height: 16), - ListView.builder( - primary: false, - shrinkWrap: true, - itemCount: interfaces.length, - itemBuilder: (context, index) => Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - Navigator.pop(context); - onSelect(interfaces[index]); - }, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - interfaces[index].name, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurface - ), + Flexible( + child: SingleChildScrollView( + child: Wrap( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 24), + child: Icon( + Icons.settings_ethernet_rounded, + size: 24, + color: Theme.of(context).listTileTheme.iconColor ), - Row( - children: [ - Text( - "${AppLocalizations.of(context)!.hardwareAddress}: ", - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.onSurfaceVariant - ), + ), + const SizedBox(height: 16), + Text( + AppLocalizations.of(context)!.selectInterface, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + color: Theme.of(context).colorScheme.onSurface + ), + ), + ], + ), + ], + ), + const SizedBox(height: 16), + ListView.builder( + primary: false, + shrinkWrap: true, + itemCount: interfaces.length, + itemBuilder: (context, index) => Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + Navigator.pop(context); + onSelect(interfaces[index]); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + interfaces[index].name, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface ), - Text( - interfaces[index].hardwareAddress, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.onSurfaceVariant + ), + Row( + children: [ + Text( + "${AppLocalizations.of(context)!.hardwareAddress}: ", + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), ), + Text( + interfaces[index].hardwareAddress, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ), + ], + ), + const SizedBox(height: 5), + if (interfaces[index].flags.isNotEmpty) ...[ + Row( + children: [ + Text( + "Flags: ", + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ), + Text( + interfaces[index].flags.join(', '), + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ), + ], ), + const SizedBox(height: 5), ], - ), - const SizedBox(height: 5), - if (interfaces[index].flags.isNotEmpty) ...[ - Row( - children: [ - Text( - "Flags: ", - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.onSurfaceVariant - ), - ), - Text( - interfaces[index].flags.join(', '), - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.onSurfaceVariant - ), - ), - ], - ), - const SizedBox(height: 5), - ], - if (interfaces[index].gatewayIp != '') ...[ - Row( - children: [ - Text( - "${AppLocalizations.of(context)!.gatewayIp}: ", - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.onSurfaceVariant - ), - ), - Text( - interfaces[index].gatewayIp, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.onSurfaceVariant - ), - ), - ], - ), - const SizedBox(height: 5), - ], - if (interfaces[index].ipv4Addresses.isNotEmpty) ...[ - Row( - children: [ - Flexible( - child: Text( - "${AppLocalizations.of(context)!.ipv4addresses}: ${interfaces[index].ipv4Addresses.join(', ')}", + if (interfaces[index].gatewayIp != '') ...[ + Row( + children: [ + Text( + "${AppLocalizations.of(context)!.gatewayIp}: ", style: TextStyle( fontSize: 14, color: Theme.of(context).colorScheme.onSurfaceVariant ), ), - ) - ], - ), - const SizedBox(height: 5), - ], - if (interfaces[index].ipv6Addresses.isNotEmpty) ...[ - Row( - children: [ - Flexible( - child: Text( - "${AppLocalizations.of(context)!.ipv6addresses}: ${interfaces[index].ipv6Addresses.join(', ')}", + Text( + interfaces[index].gatewayIp, style: TextStyle( fontSize: 14, color: Theme.of(context).colorScheme.onSurfaceVariant ), ), - ) - ], - ), - ] - ], + ], + ), + const SizedBox(height: 5), + ], + if (interfaces[index].ipv4Addresses.isNotEmpty) ...[ + Row( + children: [ + Flexible( + child: Text( + "${AppLocalizations.of(context)!.ipv4addresses}: ${interfaces[index].ipv4Addresses.join(', ')}", + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ), + ) + ], + ), + const SizedBox(height: 5), + ], + if (interfaces[index].ipv6Addresses.isNotEmpty) ...[ + Row( + children: [ + Flexible( + child: Text( + "${AppLocalizations.of(context)!.ipv6addresses}: ${interfaces[index].ipv6Addresses.join(', ')}", + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ), + ) + ], + ), + ] + ], + ), ), ), - ), - ) - ), - ], + ) + ), + ], + ), ), ), Padding( @@ -190,7 +194,30 @@ class SelectInterfaceModal extends StatelessWidget { ), if (Platform.isIOS) const SizedBox(height: 16) ], - ), - ); + ); + } + + if (dialog == true) { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500 + ), + child: content() + ), + ); + } + else { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).dialogBackgroundColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28) + ) + ), + child: content() + ); + } } } \ No newline at end of file diff --git a/lib/screens/settings/dns/bootstrap_dns.dart b/lib/screens/settings/dns/bootstrap_dns.dart index 32f02ed..7658c4d 100644 --- a/lib/screens/settings/dns/bootstrap_dns.dart +++ b/lib/screens/settings/dns/bootstrap_dns.dart @@ -182,8 +182,7 @@ class _BootstrapDnsScreenState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - SizedBox( - width: MediaQuery.of(context).size.width-74, + Expanded( child: TextFormField( controller: c['controller'], onChanged: (value) => validateIp(c, value), @@ -199,6 +198,7 @@ class _BootstrapDnsScreenState extends State { ) ), ), + const SizedBox(width: 8), IconButton( onPressed: () { setState(() => bootstrapControllers = bootstrapControllers.where((con) => con != c).toList()); diff --git a/lib/screens/settings/dns/cache_config.dart b/lib/screens/settings/dns/cache_config.dart index 16090fb..713f837 100644 --- a/lib/screens/settings/dns/cache_config.dart +++ b/lib/screens/settings/dns/cache_config.dart @@ -257,7 +257,8 @@ class _CacheConfigDnsScreenState extends State { label: Text(AppLocalizations.of(context)!.clearDnsCache), ), ], - ) + ), + const SizedBox(height: 16) ], ), ); diff --git a/lib/screens/settings/dns/comment_modal.dart b/lib/screens/settings/dns/comment_modal.dart index 20ad859..a9dd60d 100644 --- a/lib/screens/settings/dns/comment_modal.dart +++ b/lib/screens/settings/dns/comment_modal.dart @@ -4,11 +4,13 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class CommentModal extends StatefulWidget { final String? comment; final void Function(String) onConfirm; + final bool dialog; const CommentModal({ Key? key, this.comment, - required this.onConfirm + required this.onConfirm, + required this.dialog }) : super(key: key); @override @@ -30,43 +32,41 @@ class _CommentModalState extends State { @override Widget build(BuildContext context) { - return Padding( - padding: MediaQuery.of(context).viewInsets, - child: Container( - height: 310, - decoration: BoxDecoration( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28) - ), - color: Theme.of(context).dialogBackgroundColor - ), - child: Column( - children: [ - Expanded( - child: ListView( - physics: MediaQuery.of(context).size.height >= 330 == true - ? const NeverScrollableScrollPhysics() - : null, + Widget content() { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: SingleChildScrollView( + child: Wrap( children: [ - Padding( - padding: const EdgeInsets.only(top: 24), - child: Icon( - Icons.comment_rounded, - size: 24, - color: Theme.of(context).colorScheme.secondary, - ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 24), + child: Icon( + Icons.comment_rounded, + size: 24, + color: Theme.of(context).colorScheme.secondary, + ), + ), + const SizedBox(height: 16), + Text( + AppLocalizations.of(context)!.comment, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + color: Theme.of(context).colorScheme.onSurface + ), + ), + const SizedBox(height: 16), + ], + ), + ], ), - const SizedBox(height: 16), - Text( - AppLocalizations.of(context)!.comment, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSurface - ), - ), - const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: TextFormField( @@ -95,38 +95,64 @@ class _CommentModalState extends State { ], ), ), - Padding( - padding: const EdgeInsets.all(24), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(AppLocalizations.of(context)!.cancel) - ), - const SizedBox(width: 20), - TextButton( - onPressed: validData == true - ? () { - Navigator.pop(context); - widget.onConfirm("# ${commentController.text}"); - } - : null, - child: Text( - AppLocalizations.of(context)!.confirm, - style: TextStyle( - color: validData == true - ? Theme.of(context).colorScheme.primary - : Colors.grey - ), - ) - ), - ], - ), - ) - ], + ), + Padding( + padding: const EdgeInsets.all(24), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.cancel) + ), + const SizedBox(width: 20), + TextButton( + onPressed: validData == true + ? () { + Navigator.pop(context); + widget.onConfirm("# ${commentController.text}"); + } + : null, + child: Text( + AppLocalizations.of(context)!.confirm, + style: TextStyle( + color: validData == true + ? Theme.of(context).colorScheme.primary + : Colors.grey + ), + ) + ), + ], + ), + ) + ], + ); + } + + if (widget.dialog == true) { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 400 + ), + child: content() ), - ), - ); + ); + } + else { + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28) + ), + color: Theme.of(context).dialogBackgroundColor + ), + child: content() + ), + ); + } } } \ No newline at end of file diff --git a/lib/screens/settings/dns/dns.dart b/lib/screens/settings/dns/dns.dart index 229c167..55cfa19 100644 --- a/lib/screens/settings/dns/dns.dart +++ b/lib/screens/settings/dns/dns.dart @@ -1,6 +1,9 @@ // ignore_for_file: use_build_context_synchronously +import 'dart:io'; + import 'package:flutter/material.dart'; +import 'package:flutter_split_view/flutter_split_view.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -76,6 +79,19 @@ class _DnsSettingsWidgetState extends State { final serversProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + + void navigate(Widget widget) { + if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) { + SplitView.of(context).push(widget); + } + else { + Navigator.push(context, MaterialPageRoute( + builder: (context) => widget + )); + } + } + Widget generateBody() { switch (widget.serversProvider.dnsInfo.loadStatus) { case 0: @@ -105,51 +121,51 @@ class _DnsSettingsWidgetState extends State { CustomListTile( title: AppLocalizations.of(context)!.upstreamDns, subtitle: AppLocalizations.of(context)!.upstreamDnsDescription, - onTap: () => Navigator.push(context, MaterialPageRoute( - builder: (context) => UpstreamDnsScreen( + onTap: () => navigate( + UpstreamDnsScreen( serversProvider: serversProvider ) - )), + ), icon: Icons.upload_rounded, ), CustomListTile( title: AppLocalizations.of(context)!.bootstrapDns, subtitle: AppLocalizations.of(context)!.bootstrapDnsDescription, - onTap: () => Navigator.push(context, MaterialPageRoute( - builder: (context) => BootstrapDnsScreen( + onTap: () => navigate( + BootstrapDnsScreen( serversProvider: serversProvider ) - )), + ), icon: Icons.dns_rounded, ), CustomListTile( title: AppLocalizations.of(context)!.privateReverseDnsServers, subtitle: AppLocalizations.of(context)!.privateReverseDnsDescription, - onTap: () => Navigator.push(context, MaterialPageRoute( - builder: (context) => PrivateReverseDnsServersScreen( + onTap: () => navigate( + PrivateReverseDnsServersScreen( serversProvider: serversProvider ) - )), + ), icon: Icons.person_rounded, ), CustomListTile( title: AppLocalizations.of(context)!.dnsServerSettings, subtitle: AppLocalizations.of(context)!.dnsServerSettingsDescription, - onTap: () => Navigator.push(context, MaterialPageRoute( - builder: (context) => DnsServerSettingsScreen( + onTap: () => navigate( + DnsServerSettingsScreen( serversProvider: serversProvider ) - )), + ), icon: Icons.settings, ), CustomListTile( title: AppLocalizations.of(context)!.dnsCacheConfig, subtitle: AppLocalizations.of(context)!.dnsCacheConfigDescription, - onTap: () => Navigator.push(context, MaterialPageRoute( - builder: (context) => CacheConfigDnsScreen( + onTap: () => navigate( + CacheConfigDnsScreen( serversProvider: serversProvider ) - )), + ), icon: Icons.storage_rounded, ), ], diff --git a/lib/screens/settings/dns/private_reverse_servers.dart b/lib/screens/settings/dns/private_reverse_servers.dart index a6c29bf..a4f8131 100644 --- a/lib/screens/settings/dns/private_reverse_servers.dart +++ b/lib/screens/settings/dns/private_reverse_servers.dart @@ -229,8 +229,7 @@ class _PrivateReverseDnsServersScreenState extends State validateAddress(c, value), @@ -246,6 +245,7 @@ class _PrivateReverseDnsServersScreenState extends State reverseResolversControllers = reverseResolversControllers.where((con) => con != c).toList()); diff --git a/lib/screens/settings/dns/upstream_dns.dart b/lib/screens/settings/dns/upstream_dns.dart index 0005252..66d8b8c 100644 --- a/lib/screens/settings/dns/upstream_dns.dart +++ b/lib/screens/settings/dns/upstream_dns.dart @@ -1,5 +1,7 @@ // ignore_for_file: use_build_context_synchronously +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -71,36 +73,73 @@ class _UpstreamDnsScreenState extends State { Widget build(BuildContext context) { final serversProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + + final width = MediaQuery.of(context).size.width; void openAddCommentModal() { - showModalBottomSheet( - context: context, - builder: (context) => CommentModal( - onConfirm: (value) { - dnsServers.add({ - 'comment': value - }); - }, - ), - backgroundColor: Colors.transparent, - isScrollControlled: true, - isDismissible: true - ); + if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + context: context, + builder: (context) => CommentModal( + onConfirm: (value) { + setState(() { + dnsServers.add({ + 'comment': value + }); + }); + }, + dialog: true, + ), + ); + } + else { + showModalBottomSheet( + context: context, + builder: (context) => CommentModal( + onConfirm: (value) { + setState(() { + dnsServers.add({ + 'comment': value + }); + }); + }, + dialog: false, + ), + backgroundColor: Colors.transparent, + isScrollControlled: true, + isDismissible: true + ); + } } void openEditCommentModal(Map item, int position) { - showModalBottomSheet( - context: context, - builder: (context) => CommentModal( - comment: item['comment'], - onConfirm: (value) { - setState(() => dnsServers[position] = { 'comment': value }); - }, - ), - backgroundColor: Colors.transparent, - isScrollControlled: true, - isDismissible: true - ); + if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + context: context, + builder: (context) => CommentModal( + comment: item['comment'], + onConfirm: (value) { + setState(() => dnsServers[position] = { 'comment': value }); + }, + dialog: true, + ), + ); + } + else { + showModalBottomSheet( + context: context, + builder: (context) => CommentModal( + comment: item['comment'], + onConfirm: (value) { + setState(() => dnsServers[position] = { 'comment': value }); + }, + dialog: false, + ), + backgroundColor: Colors.transparent, + isScrollControlled: true, + isDismissible: true + ); + } } void saveData() async { @@ -185,13 +224,12 @@ class _UpstreamDnsScreenState extends State { ), ...dnsServers.map((item) => Padding( padding: const EdgeInsets.only( - left: 16, right: 6, bottom: 20 + left: 16, right: 6, bottom: 24 ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - if (item['controller'] != null) SizedBox( - width: MediaQuery.of(context).size.width-74, + if (item['controller'] != null) Expanded( child: TextFormField( controller: item['controller'], onChanged: (_) => checkValidValues(), @@ -206,6 +244,7 @@ class _UpstreamDnsScreenState extends State { ) ), ), + const SizedBox(width: 8), if (item['comment'] != null) Expanded( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -232,10 +271,12 @@ class _UpstreamDnsScreenState extends State { }, icon: const Icon(Icons.remove_circle_outline), tooltip: AppLocalizations.of(context)!.remove, - ) + ), + const SizedBox(width: 4), ], ), )).toList(), + const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisSize: MainAxisSize.min, diff --git a/lib/screens/settings/dns_rewrites/add_dns_rewrite_modal.dart b/lib/screens/settings/dns_rewrites/add_dns_rewrite_modal.dart index 53bc3b2..edff851 100644 --- a/lib/screens/settings/dns_rewrites/add_dns_rewrite_modal.dart +++ b/lib/screens/settings/dns_rewrites/add_dns_rewrite_modal.dart @@ -7,10 +7,12 @@ import 'package:adguard_home_manager/models/rewrite_rules.dart'; class AddDnsRewriteModal extends StatefulWidget { final void Function(RewriteRulesData) onConfirm; + final bool dialog; const AddDnsRewriteModal({ Key? key, - required this.onConfirm + required this.onConfirm, + required this.dialog }) : super(key: key); @override @@ -50,45 +52,45 @@ class _AddDnsRewriteModalState extends State { @override Widget build(BuildContext context) { - return Padding( - padding: MediaQuery.of(context).viewInsets, - child: Container( - height: Platform.isIOS ? 416 : 400, - decoration: BoxDecoration( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28) - ), - color: Theme.of(context).dialogBackgroundColor, - ), - child: Column( - children: [ - Expanded( - child: ListView( - physics: (Platform.isIOS ? 426 : 410) < MediaQuery.of(context).size.height - ? const NeverScrollableScrollPhysics() - : null, + Widget content() { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: SingleChildScrollView( + child: Wrap( children: [ - Padding( - padding: const EdgeInsets.only(top: 24), - child: Icon( - Icons.add, - size: 24, - color: Theme.of(context).listTileTheme.iconColor - ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 24), + child: Icon( + Icons.add, + size: 24, + color: Theme.of(context).listTileTheme.iconColor + ), + ), + const SizedBox(height: 16), + Text( + AppLocalizations.of(context)!.addDnsRewrite, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + color: Theme.of(context).colorScheme.onSurface + ), + ), + const SizedBox(height: 16), + ], + ), + ], ), - const SizedBox(height: 16), - Text( - AppLocalizations.of(context)!.addDnsRewrite, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSurface - ), - ), - const SizedBox(height: 16), Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), + padding: const EdgeInsets.only( + left: 24, right: 24, bottom: 12 + ), child: TextFormField( controller: domainController, onChanged: validateDomain, @@ -104,9 +106,10 @@ class _AddDnsRewriteModalState extends State { ), ), ), - const SizedBox(height: 30), Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), + padding: const EdgeInsets.only( + left: 24, right: 24, top: 12 + ), child: TextFormField( controller: answerController, onChanged: (_) => checkValidValues(), @@ -124,44 +127,70 @@ class _AddDnsRewriteModalState extends State { ], ), ), - Padding( - padding: const EdgeInsets.all(24), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(AppLocalizations.of(context)!.cancel), - ), - const SizedBox(width: 20), - TextButton( - onPressed: validData == true - ? () { - Navigator.pop(context); - widget.onConfirm( - RewriteRulesData( - domain: domainController.text, - answer: answerController.text - ) - ); - } - : null, - child: Text( - AppLocalizations.of(context)!.confirm, - style: TextStyle( - color: validData == true - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurface.withOpacity(0.38) - ), + ), + Padding( + padding: const EdgeInsets.all(24), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.cancel), + ), + const SizedBox(width: 20), + TextButton( + onPressed: validData == true + ? () { + Navigator.pop(context); + widget.onConfirm( + RewriteRulesData( + domain: domainController.text, + answer: answerController.text + ) + ); + } + : null, + child: Text( + AppLocalizations.of(context)!.confirm, + style: TextStyle( + color: validData == true + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onSurface.withOpacity(0.38) ), ), - ], - ), + ), + ], ), - if (Platform.isIOS) const SizedBox(height: 16) - ], + ), + if (Platform.isIOS) const SizedBox(height: 16) + ], + ); + } + + if (widget.dialog == true) { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 400 + ), + child: content() ), - ), - ); + ); + } + else { + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28) + ), + color: Theme.of(context).dialogBackgroundColor, + ), + child: content() + ), + ); + } } } \ No newline at end of file diff --git a/lib/screens/settings/dns_rewrites/dns_rewrites.dart b/lib/screens/settings/dns_rewrites/dns_rewrites.dart index 119da4c..d9fe68f 100644 --- a/lib/screens/settings/dns_rewrites/dns_rewrites.dart +++ b/lib/screens/settings/dns_rewrites/dns_rewrites.dart @@ -1,5 +1,7 @@ // ignore_for_file: use_build_context_synchronously +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -70,6 +72,8 @@ class _DnsRewritesWidgetState extends State { final serversProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + void deleteDnsRewrite(RewriteRulesData rule) async { ProcessModal processModal = ProcessModal(context: context); processModal.open(AppLocalizations.of(context)!.deleting); @@ -288,14 +292,26 @@ class _DnsRewritesWidgetState extends State { body: generateBody(), floatingActionButton: FloatingActionButton( onPressed: () => { - showModalBottomSheet( - context: context, - builder: (context) => AddDnsRewriteModal( - onConfirm: addDnsRewrite, - ), - backgroundColor: Colors.transparent, - isScrollControlled: true - ) + if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + context: context, + builder: (context) => AddDnsRewriteModal( + onConfirm: addDnsRewrite, + dialog: true, + ), + ) + } + else { + showModalBottomSheet( + context: context, + builder: (context) => AddDnsRewriteModal( + onConfirm: addDnsRewrite, + dialog: false, + ), + backgroundColor: Colors.transparent, + isScrollControlled: true + ) + } }, child: const Icon(Icons.add), ), diff --git a/lib/screens/settings/encryption/custom_text_field.dart b/lib/screens/settings/encryption/custom_text_field.dart index c149873..6a1cac8 100644 --- a/lib/screens/settings/encryption/custom_text_field.dart +++ b/lib/screens/settings/encryption/custom_text_field.dart @@ -26,8 +26,12 @@ class EncryptionTextField extends StatelessWidget { @override Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width; + return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), + padding: width > 900 + ? const EdgeInsets.symmetric(horizontal: 8) + : const EdgeInsets.symmetric(horizontal: 16), child: ConstrainedBox( constraints: const BoxConstraints( maxHeight: 200 diff --git a/lib/screens/settings/encryption/encryption.dart b/lib/screens/settings/encryption/encryption.dart index e1f3686..2392287 100644 --- a/lib/screens/settings/encryption/encryption.dart +++ b/lib/screens/settings/encryption/encryption.dart @@ -234,6 +234,8 @@ class _EncryptionSettingsWidgetState extends State { final serversProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + void saveData() async { ProcessModal processModal = ProcessModal(context: context); processModal.open(AppLocalizations.of(context)!.savingConfig); @@ -343,43 +345,59 @@ class _EncryptionSettingsWidgetState extends State { disabled: !enabled, ), const SizedBox(height: 10), - EncryptionTextField( - enabled: enabled, - controller: httpsPortController, - icon: Icons.numbers_rounded, - onChanged: (value) { - setState(() => httpsPortError = validatePort(context, value)); - onEditValidate(); - }, - errorText: httpsPortError, - label: AppLocalizations.of(context)!.httpsPort, - keyboardType: TextInputType.number, - ), - const SizedBox(height: 30), - EncryptionTextField( - enabled: enabled, - controller: tlsPortController, - icon: Icons.numbers_rounded, - onChanged: (value) { - setState(() => tlsPortError = validatePort(context, value)); - onEditValidate(); - }, - errorText: tlsPortError, - label: AppLocalizations.of(context)!.tlsPort, - keyboardType: TextInputType.number, - ), - const SizedBox(height: 30), - EncryptionTextField( - enabled: enabled, - controller: dnsOverQuicPortController, - icon: Icons.numbers_rounded, - onChanged: (value) { - setState(() => dnsOverQuicPortError = validatePort(context, value)); - onEditValidate(); - }, - errorText: dnsOverQuicPortError, - label: AppLocalizations.of(context)!.dnsOverQuicPort, - keyboardType: TextInputType.number, + Wrap( + children: [ + FractionallySizedBox( + widthFactor: width > 900 ? 0.33 : 1, + child: EncryptionTextField( + enabled: enabled, + controller: httpsPortController, + icon: Icons.numbers_rounded, + onChanged: (value) { + setState(() => httpsPortError = validatePort(context, value)); + onEditValidate(); + }, + errorText: httpsPortError, + label: AppLocalizations.of(context)!.httpsPort, + keyboardType: TextInputType.number, + ), + ), + Padding( + padding: width <= 900 + ? const EdgeInsets.symmetric(vertical: 24) + : const EdgeInsets.all(0), + child: FractionallySizedBox( + widthFactor: width > 900 ? 0.33 : 1, + child: EncryptionTextField( + enabled: enabled, + controller: tlsPortController, + icon: Icons.numbers_rounded, + onChanged: (value) { + setState(() => tlsPortError = validatePort(context, value)); + onEditValidate(); + }, + errorText: tlsPortError, + label: AppLocalizations.of(context)!.tlsPort, + keyboardType: TextInputType.number, + ), + ), + ), + FractionallySizedBox( + widthFactor: width > 900 ? 0.33 : 1, + child: EncryptionTextField( + enabled: enabled, + controller: dnsOverQuicPortController, + icon: Icons.numbers_rounded, + onChanged: (value) { + setState(() => dnsOverQuicPortError = validatePort(context, value)); + onEditValidate(); + }, + errorText: dnsOverQuicPortError, + label: AppLocalizations.of(context)!.dnsOverQuicPort, + keyboardType: TextInputType.number, + ), + ), + ], ), SectionLabel( label: AppLocalizations.of(context)!.certificates, diff --git a/lib/screens/settings/safe_search_settings.dart b/lib/screens/settings/safe_search_settings.dart index cefd757..fe428fa 100644 --- a/lib/screens/settings/safe_search_settings.dart +++ b/lib/screens/settings/safe_search_settings.dart @@ -52,24 +52,26 @@ class _SafeSearchSettingsScreenWidgetState extends State 900) { + return SplitView.material( + hideDivider: true, + flexWidth: const FlexWidth(mainViewFlexWidth: 1, secondaryViewFlexWidth: 2), + placeholder: Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Text( + AppLocalizations.of(context)!.selectOptionLeftColumn, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ), + ), + ), + child: const SettingsWidget(), + ); + } + else { + return const SettingsWidget(); + } + } +} +class SettingsWidget extends StatelessWidget { + const SettingsWidget({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { final appConfigProvider = Provider.of(context); final serversProvider = Provider.of(context); - void navigateServers() { - Future.delayed(const Duration(milliseconds: 0), (() { - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => const Servers()) + final width = MediaQuery.of(context).size.width; + + if (width <= 900 && appConfigProvider.selectedSettingsScreen != null) { + appConfigProvider.setSelectedSettingsScreen(screen: null); + } + + Widget settingsTile({ + required String title, + required String subtitle, + required IconData icon, + Widget? trailing, + required Widget screenToNavigate, + required int thisItem + }) { + if (width > 900) { + return CustomSettingsTile( + title: title, + subtitle: subtitle, + icon: icon, + trailing: trailing, + thisItem: thisItem, + selectedItem: appConfigProvider.selectedSettingsScreen, + onTap: () { + appConfigProvider.setSelectedSettingsScreen(screen: thisItem, notify: true); + SplitView.of(context).setSecondary(screenToNavigate); + }, ); - })); - } + } + else { + return CustomListTile( + title: title, + subtitle: subtitle, + icon: icon, + trailing: trailing, + onTap: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => screenToNavigate) + ); + }, + ); + } + } return Scaffold( appBar: AppBar( @@ -49,85 +120,55 @@ class Settings extends StatelessWidget { ), body: ListView( children: [ - if (serversProvider.selectedServer != null) ...[ + if (serversProvider.selectedServer != null && serversProvider.serverStatus.data != null) ...[ SectionLabel(label: AppLocalizations.of(context)!.serverSettings), if (serverVersionIsAhead( currentVersion: serversProvider.serverStatus.data!.serverVersion, referenceVersion: 'v0.107.28', referenceVersionBeta: 'v0.108.0-b.33' - ) == true) CustomListTile( + ) == true) settingsTile( icon: Icons.search_rounded, title: AppLocalizations.of(context)!.safeSearch, subtitle: AppLocalizations.of(context)!.safeSearchSettings, - onTap: () => { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const SafeSearchSettingsScreen() - ) - ) - }, + thisItem: 0, + screenToNavigate: const SafeSearchSettingsScreen(), ), - CustomListTile( + settingsTile( icon: Icons.lock_rounded, title: AppLocalizations.of(context)!.accessSettings, subtitle: AppLocalizations.of(context)!.accessSettingsDescription, - onTap: () => { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const AccessSettings() - ) - ) - }, + thisItem: 1, + screenToNavigate: const AccessSettings(), ), - CustomListTile( + settingsTile( icon: Icons.install_desktop_rounded, title: AppLocalizations.of(context)!.dhcpSettings, subtitle: AppLocalizations.of(context)!.dhcpSettingsDescription, - onTap: () => { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const Dhcp() - ) - ) - }, + thisItem: 2, + screenToNavigate: const Dhcp(), ), - CustomListTile( + settingsTile( icon: Icons.dns_rounded, title: AppLocalizations.of(context)!.dnsSettings, subtitle: AppLocalizations.of(context)!.dnsSettingsDescription, - onTap: () => { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const DnsSettings() - ) - ) - }, + thisItem: 3, + screenToNavigate: const DnsSettings(), ), - CustomListTile( + settingsTile( icon: Icons.security_rounded, title: AppLocalizations.of(context)!.encryptionSettings, subtitle: AppLocalizations.of(context)!.encryptionSettingsDescription, - onTap: () => { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const EncryptionSettings() - ) - ) - }, + thisItem: 4, + screenToNavigate: const EncryptionSettings(), ), - CustomListTile( + settingsTile( icon: Icons.route_rounded, title: AppLocalizations.of(context)!.dnsRewrites, subtitle: AppLocalizations.of(context)!.dnsRewritesDescription, - onTap: () => { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const DnsRewrites() - ) - ) - }, + thisItem: 5, + screenToNavigate: const DnsRewrites(), ), - if (serversProvider.updateAvailable.data != null) CustomListTile( + if (serversProvider.updateAvailable.data != null) settingsTile( icon: Icons.system_update_rounded, title: AppLocalizations.of(context)!.updates, subtitle: AppLocalizations.of(context)!.updatesDescription, @@ -144,37 +185,26 @@ class Settings extends StatelessWidget { ), ) : null, - onTap: () => { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const UpdateScreen() - ) - ) - }, + thisItem: 6, + screenToNavigate: const UpdateScreen(), ), - CustomListTile( + settingsTile( icon: Icons.info_rounded, title: AppLocalizations.of(context)!.serverInformation, subtitle: AppLocalizations.of(context)!.serverInformationDescription, - onTap: () => { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const ServerInformation() - ) - ) - }, + thisItem: 7, + screenToNavigate: const ServerInformation(), ), ], SectionLabel(label: AppLocalizations.of(context)!.appSettings), - CustomListTile( + settingsTile( icon: Icons.palette_rounded, title: AppLocalizations.of(context)!.customization, subtitle: AppLocalizations.of(context)!.customizationDescription, - onTap: () => Navigator.push(context, MaterialPageRoute( - builder: (context) => const Customization() - )) + thisItem: 8, + screenToNavigate: const Customization(), ), - CustomListTile( + settingsTile( icon: Icons.storage_rounded, title: AppLocalizations.of(context)!.servers, subtitle: serversProvider.selectedServer != null @@ -182,31 +212,22 @@ class Settings extends StatelessWidget { ? "${AppLocalizations.of(context)!.connectedTo} ${serversProvider.selectedServer!.name}" : "${AppLocalizations.of(context)!.selectedServer} ${serversProvider.selectedServer!.name}" : AppLocalizations.of(context)!.noServerSelected, - onTap: navigateServers, + thisItem: 9, + screenToNavigate: const Servers(), ), - CustomListTile( + settingsTile( icon: Icons.settings, title: AppLocalizations.of(context)!.generalSettings, subtitle: AppLocalizations.of(context)!.generalSettingsDescription, - onTap: () => { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const GeneralSettings() - ) - ) - }, + thisItem: 10, + screenToNavigate: const GeneralSettings(), ), - CustomListTile( + settingsTile( icon: Icons.build_outlined, title: AppLocalizations.of(context)!.advancedSettings, subtitle: AppLocalizations.of(context)!.advancedSetupDescription, - onTap: () => { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const AdvancedSettings() - ) - ) - }, + thisItem: 11, + screenToNavigate: const AdvancedSettings(), ), SectionLabel(label: AppLocalizations.of(context)!.aboutApp), CustomListTile( @@ -222,7 +243,7 @@ class Settings extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - IconButton( + if (Platform.isAndroid) IconButton( onPressed: () => openUrl(Urls.playStore), icon: SvgPicture.asset( 'assets/resources/google-play.svg', diff --git a/lib/screens/settings/update_server/update.dart b/lib/screens/settings/update_server/update.dart index 5fa898e..f92292a 100644 --- a/lib/screens/settings/update_server/update.dart +++ b/lib/screens/settings/update_server/update.dart @@ -65,16 +65,18 @@ class UpdateScreen extends StatelessWidget { Widget headerPortrait() { return Column( children: [ + const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - IconButton( + if (Navigator.canPop(context)) IconButton( icon: Icon( Icons.arrow_back, color: Theme.of(context).colorScheme.onSurfaceVariant, ), onPressed: () => Navigator.pop(context), ), + if (!Navigator.canPop(context)) const SizedBox(), IconButton( icon: Icon( Icons.refresh_rounded, @@ -170,120 +172,6 @@ class UpdateScreen extends StatelessWidget { ); } - Widget headerLandscape() { - return Column( - mainAxisSize: MainAxisSize.max, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - IconButton( - icon: Icon( - Icons.arrow_back, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - onPressed: () => Navigator.pop(context), - ), - IconButton( - icon: Icon( - Icons.refresh_rounded, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - tooltip: AppLocalizations.of(context)!.checkUpdates, - onPressed: () => serversProvider.checkServerUpdatesAvailable(serversProvider.selectedServer!) - ), - ], - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only( - top: 8, bottom: 16, left: 16, right: 16 - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - serversProvider.updateAvailable.loadStatus == LoadStatus.loading - ? Column( - children: const [ - CircularProgressIndicator(), - SizedBox(height: 4) - ], - ) - : Icon( - serversProvider.updateAvailable.data!.updateAvailable != null - ? serversProvider.updateAvailable.data!.updateAvailable == true - ? Icons.system_update_rounded - : Icons.system_security_update_good_rounded - : Icons.system_security_update_warning_rounded, - size: 40, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(height: 16), - Text( - serversProvider.updateAvailable.loadStatus == LoadStatus.loading - ? AppLocalizations.of(context)!.checkingUpdates - : serversProvider.updateAvailable.data!.updateAvailable != null - ? serversProvider.updateAvailable.data!.updateAvailable == true - ? AppLocalizations.of(context)!.updateAvailable - : AppLocalizations.of(context)!.serverUpdated - : AppLocalizations.of(context)!.unknownStatus, - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.w400 - ), - ), - const SizedBox(height: 40), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - if (serversProvider.updateAvailable.loadStatus == LoadStatus.loaded) Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - serversProvider.updateAvailable.data!.updateAvailable != null && serversProvider.updateAvailable.data!.updateAvailable == true - ? AppLocalizations.of(context)!.newVersion - : AppLocalizations.of(context)!.currentVersion, - style: const TextStyle( - fontSize: 16, - ), - ), - const SizedBox(height: 4), - Text( - serversProvider.updateAvailable.data!.updateAvailable != null - ? serversProvider.updateAvailable.data!.updateAvailable == true - ? serversProvider.updateAvailable.data!.newVersion ?? 'N/A' - : serversProvider.updateAvailable.data!.currentVersion - : "N/A", - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w700, - color: Theme.of(context).colorScheme.onSurfaceVariant - ), - ) - ], - ), - if (serversProvider.updateAvailable.loadStatus != LoadStatus.loaded) const SizedBox(), - FilledButton.icon( - icon: const Icon(Icons.download_rounded), - label: Text(AppLocalizations.of(context)!.updateNow), - onPressed: serversProvider.updateAvailable.data!.updateAvailable != null && serversProvider.updateAvailable.data!.updateAvailable == true - ? serversProvider.updateAvailable.data!.canAutoupdate == true - ? () => update() - : () => showAutoUpdateUnavailableModal() - : null - ) - ], - ), - ), - ], - ), - ), - ), - ], - ); - } - final changelog = serversProvider.updateAvailable.loadStatus == LoadStatus.loaded && serversProvider.updateAvailable.data!.changelog != null ? ListView( children: [ @@ -313,51 +201,20 @@ class UpdateScreen extends StatelessWidget { : null; return Scaffold( - body: MediaQuery.of(context).size.width > 700 - ? Row( - children: [ - Expanded( - flex: 2, - child: Container( - color: Theme.of(context).colorScheme.surfaceVariant, - child: Column( - mainAxisSize: MainAxisSize.max, - children: [ - Container( - height: MediaQuery.of(context).size.height, - padding: EdgeInsets.only( - top: MediaQuery.of(context).viewPadding.top - ), - child: headerLandscape(), - ) - ], - ), - ), - ), - Expanded( - flex: 3, - child: SafeArea( - child: SizedBox( - width: MediaQuery.of(context).size.width*0.6, - child: changelog ?? const SizedBox(), - ), - ), - ) - ], - ) - : Column( - children: [ - Container( - color: Theme.of(context).colorScheme.surfaceVariant, - child: SafeArea( - child: headerPortrait() - ) - ), - changelog != null - ? Expanded(child: changelog) - : const SizedBox(), - ] - ) + body: Column( + children: [ + Container( + color: Theme.of(context).colorScheme.surfaceVariant, + child: SafeArea( + child: headerPortrait() + ) + ), + const SizedBox(height: 16), + changelog != null + ? Expanded(child: changelog) + : const SizedBox(), + ] + ) ); } } \ No newline at end of file diff --git a/lib/screens/top_items/top_items_modal.dart b/lib/screens/top_items/top_items_modal.dart new file mode 100644 index 0000000..55aae09 --- /dev/null +++ b/lib/screens/top_items/top_items_modal.dart @@ -0,0 +1,276 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:percent_indicator/percent_indicator.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/screens/home/top_items_options_modal.dart'; +import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; + +import 'package:adguard_home_manager/models/applied_filters.dart'; +import 'package:adguard_home_manager/functions/copy_clipboard.dart'; +import 'package:adguard_home_manager/providers/logs_provider.dart'; +import 'package:adguard_home_manager/functions/snackbar.dart'; +import 'package:adguard_home_manager/functions/number_format.dart'; +import 'package:adguard_home_manager/functions/block_unblock_domain.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; + +class TopItemsModal extends StatefulWidget { + final String type; + final String title; + final bool? isClient; + final List> data; + + const TopItemsModal({ + Key? key, + required this.type, + required this.title, + this.isClient, + required this.data, + }) : super(key: key); + + @override + State createState() => _TopItemsModalState(); +} + +class _TopItemsModalState extends State { + bool searchActive = false; + final TextEditingController searchController = TextEditingController(); + + List> data = []; + List> screenData = []; + + void search(String value) { + List> newValues = widget.data.where((item) => item.keys.toList()[0].contains(value)).toList(); + setState(() => screenData = newValues); + } + + @override + void initState() { + data = widget.data; + screenData = widget.data; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final serversProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + final logsProvider = Provider.of(context); + + int total = 0; + for (var element in data) { + total = total + int.parse(element.values.toList()[0].toString()); + } + + bool? getIsBlocked() { + if (widget.type == 'topBlockedDomains') { + return true; + } + else if (widget.type == 'topQueriedDomains') { + return false; + } + else { + return null; + } + } + + void changeBlockStatus(String status, String domain) async { + final result = await blockUnblock(context, domain, status); + showSnacbkar( + context: context, + appConfigProvider: appConfigProvider, + label: result['message'], + color: result['success'] == true ? Colors.green : Colors.red + ); + } + + void openOptionsModal(String domain, String type) { + showDialog( + context: context, + builder: (context) => TopItemsOptionsModal( + isBlocked: getIsBlocked(), + changeStatus: (String status) => changeBlockStatus(status, domain), + copyToClipboard: () => copyToClipboard( + context: context, + value: domain, + successMessage: AppLocalizations.of(context)!.domainCopiedClipboard + ), + type: type, + ) + ); + } + + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500 + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + flex: 1, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.clear_rounded), + tooltip: AppLocalizations.of(context)!.close, + ), + ], + ), + ), + Expanded( + flex: 1, + child: TextField( + controller: searchController, + onChanged: search, + decoration: InputDecoration( + filled: true, + fillColor: Theme.of(context).colorScheme.primary.withOpacity(0.1), + hintText: AppLocalizations.of(context)!.search, + prefixIcon: const Icon(Icons.search_rounded), + contentPadding: const EdgeInsets.only(left: 14, bottom: 9, top: 11), + focusedBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.transparent), + borderRadius: BorderRadius.circular(25.7), + ), + enabledBorder: UnderlineInputBorder( + borderSide: const BorderSide(color: Colors.transparent), + borderRadius: BorderRadius.circular(25.7), + ), + ), + ), + ) + ], + ), + ), + if (screenData.isNotEmpty) Flexible( + child: ListView.builder( + padding: const EdgeInsets.only(top: 0), + itemCount: screenData.length, + itemBuilder: (context, index) { + String? name; + if (widget.isClient != null && widget.isClient == true) { + try { + name = serversProvider.serverStatus.data!.clients.firstWhere((c) => c.ids.contains(screenData[index].keys.toList()[0])).name; + } catch (e) { + // ---- // + } + } + + return CustomListTile( + onTap: () { + if (widget.type == 'topQueriedDomains' || widget.type == 'topBlockedDomains') { + logsProvider.setSearchText(screenData[index].keys.toList()[0]); + logsProvider.setSelectedClients(null); + logsProvider.setAppliedFilters( + AppliedFiters( + selectedResultStatus: 'all', + searchText: screenData[index].keys.toList()[0], + clients: null + ) + ); + appConfigProvider.setSelectedScreen(2); + Navigator.pop(context); + } + else if (widget.type == 'topClients') { + logsProvider.setSearchText(null); + logsProvider.setSelectedClients([screenData[index].keys.toList()[0]]); + logsProvider.setAppliedFilters( + AppliedFiters( + selectedResultStatus: 'all', + searchText: null, + clients: [screenData[index].keys.toList()[0]] + ) + ); + appConfigProvider.setSelectedScreen(2); + Navigator.pop(context); + } + }, + onLongPress: () => openOptionsModal( + screenData[index].keys.toList()[0], + widget.type + ), + title: screenData[index].keys.toList()[0], + trailing: Text( + screenData[index].values.toList()[0].toString(), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ), + subtitleWidget: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (name != null) ...[ + Text( + name, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurface + ), + ), + const SizedBox(height: 5), + ], + Row( + children: [ + SizedBox( + width: 50, + child: Text( + "${doubleFormat((screenData[index].values.toList()[0]/total*100), Platform.localeName)}%", + style: TextStyle( + color: Theme.of(context).listTileTheme.textColor + ), + ), + ), + const SizedBox(width: 10), + Flexible( + child: LinearPercentIndicator( + animation: true, + lineHeight: 4, + animationDuration: 500, + curve: Curves.easeOut, + percent: screenData[index].values.toList()[0]/total, + barRadius: const Radius.circular(5), + progressColor: Theme.of(context).colorScheme.primary, + backgroundColor: Theme.of(context).colorScheme.surfaceVariant, + ), + ), + const SizedBox(width: 10), + ], + ), + ], + ) + ); + } + ), + ), + if (screenData.isEmpty) Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + AppLocalizations.of(context)!.noItemsSearch, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + ) + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/services/database.dart b/lib/services/db/database.dart similarity index 100% rename from lib/services/database.dart rename to lib/services/db/database.dart diff --git a/lib/services/db/queries.dart b/lib/services/db/queries.dart new file mode 100644 index 0000000..d5b018d --- /dev/null +++ b/lib/services/db/queries.dart @@ -0,0 +1,114 @@ +import 'package:sqflite/sqflite.dart'; + +import 'package:adguard_home_manager/functions/conversions.dart'; +import 'package:adguard_home_manager/models/server.dart'; + +Future saveServerQuery(Database db, Server server) async { + try { + return await db.transaction((txn) async { + await txn.insert( + 'servers', + { + 'id': server.id, + 'name': server.name, + 'connectionMethod': server.connectionMethod, + 'domain': server.domain, + 'path': server.path, + 'port': server.port, + 'user': server.user, + 'password': server.password, + 'defaultServer': convertFromBoolToInt(server.defaultServer), + 'authToken': server.authToken, + 'runningOnHa': convertFromBoolToInt(server.runningOnHa) + } + ); + return null; + }); + } catch (e) { + return e; + } +} + +Future editServerQuery(Database db, Server server) async { + try { + return await db.transaction((txn) async { + await txn.update( + 'servers', + { + 'id': server.id, + 'name': server.name, + 'connectionMethod': server.connectionMethod, + 'domain': server.domain, + 'path': server.path, + 'port': server.port, + 'user': server.user, + 'password': server.password, + 'defaultServer': server.defaultServer, + 'authToken': server.authToken, + 'runningOnHa': convertFromBoolToInt(server.runningOnHa) + }, + where: 'id = ?', + whereArgs: [server.id] + ); + return null; + }); + } catch (e) { + return e; + } +} + + +Future removeServerQuery(Database db, String id) async { + try { + return await db.transaction((txn) async { + await txn.delete( + 'servers', + where: 'id = ?', + whereArgs: [id] + ); + return true; + }); + } catch (e) { + return false; + } +} + +Future setDefaultServerQuery(Database db, String id) async { + try { + return await db.transaction((txn) async { + await txn.update( + 'servers', + {'defaultServer': '0'}, + where: 'defaultServer = ?', + whereArgs: [1] + ); + await txn.update( + 'servers', + {'defaultServer': '1'}, + where: 'id = ?', + whereArgs: [id] + ); + return null; + }); + } catch (e) { + return e; + } +} + +Future updateConfigQuery({ + required Database db, + required String column, + required dynamic value +}) async { + try { + return await db.transaction((txn) async { + await txn.update( + 'appConfig', + { column: value }, + ); + return true; + }); + } catch (e) { + return false; + } +} \ No newline at end of file diff --git a/lib/widgets/add_server_modal.dart b/lib/widgets/add_server_modal.dart index 7fc830d..a332c58 100644 --- a/lib/widgets/add_server_modal.dart +++ b/lib/widgets/add_server_modal.dart @@ -16,10 +16,12 @@ enum ConnectionType { http, https} class AddServerModal extends StatefulWidget { final Server? server; + final bool window; const AddServerModal({ Key? key, this.server, + required this.window }) : super(key: key); @override @@ -429,15 +431,227 @@ class _AddServerModalState extends State { } } - return Stack( - children: [ - Scaffold( - appBar: AppBar( - title: Text(AppLocalizations.of(context)!.createConnection), - actions: [ + List form() { + return [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + margin: const EdgeInsets.only( + top: 24, + left: 24, + right: 24 + ), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary.withOpacity(0.05), + borderRadius: BorderRadius.circular(30), + border: Border.all( + color: Theme.of(context).colorScheme.primary + ) + ), + child: Text( + "${connectionType.name}://${ipDomainController.text}${portController.text != '' ? ':${portController.text}' : ""}${pathController.text}", + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.w500 + ), + ), + ), + sectionLabel(AppLocalizations.of(context)!.general), + textField( + label: AppLocalizations.of(context)!.name, + controller: nameController, + icon: Icons.badge_rounded, + error: nameError, + onChanged: (value) { + if (value != '') { + setState(() => nameError = null); + } + else { + setState(() => nameError = AppLocalizations.of(context)!.nameNotEmpty); + } + checkDataValid(); + } + ), + sectionLabel(AppLocalizations.of(context)!.connection), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: SegmentedButton( + segments: const [ + ButtonSegment( + value: ConnectionType.http, + label: Text("HTTP") + ), + ButtonSegment( + value: ConnectionType.https, + label: Text("HTTPS") + ), + ], + selected: {connectionType}, + onSelectionChanged: (value) => setState(() => connectionType = value.first), + ), + ), + const SizedBox(height: 30), + textField( + label: AppLocalizations.of(context)!.ipDomain, + controller: ipDomainController, + icon: Icons.link_rounded, + error: ipDomainError, + keyboardType: TextInputType.url, + onChanged: validateAddress + ), + const SizedBox(height: 20), + textField( + label: AppLocalizations.of(context)!.path, + controller: pathController, + icon: Icons.route_rounded, + error: pathError, + onChanged: validateSubroute, + hintText: AppLocalizations.of(context)!.examplePath, + helperText: AppLocalizations.of(context)!.helperPath, + ), + const SizedBox(height: 20), + textField( + label: AppLocalizations.of(context)!.port, + controller: portController, + icon: Icons.numbers_rounded, + error: portError, + keyboardType: TextInputType.number, + onChanged: validatePort + ), + sectionLabel(AppLocalizations.of(context)!.authentication), + textField( + label: AppLocalizations.of(context)!.username, + controller: userController, + icon: Icons.person_rounded, + ), + const SizedBox(height: 20), + textField( + label: AppLocalizations.of(context)!.password, + controller: passwordController, + icon: Icons.lock_rounded, + keyboardType: TextInputType.visiblePassword, + obscureText: true + ), + sectionLabel(AppLocalizations.of(context)!.other), + Material( + color: Colors.transparent, + child: InkWell( + onTap: widget.server == null + ? () => setState(() => defaultServer = !defaultServer) + : null, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + AppLocalizations.of(context)!.defaultServer, + style: const TextStyle( + fontSize: 15, + ), + ), + Switch( + value: defaultServer, + onChanged: widget.server == null + ? (value) => setState(() => defaultServer = value) + : null, + ) + ], + ), + ), + ), + ), + const SizedBox(height: 20), + Material( + color: Colors.transparent, + child: InkWell( + onTap: () => setState(() => homeAssistant = !homeAssistant), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + AppLocalizations.of(context)!.runningHomeAssistant, + style: const TextStyle( + fontSize: 15, + ), + ), + Switch( + value: homeAssistant, + onChanged: (value) => setState(() => homeAssistant = value), + ) + ], + ), + ), + ), + ), + const SizedBox(height: 20), + ]; + } + + if (widget.window == true) { + return Dialog( + child: SizedBox( + width: 400, + child: Column( + children: [ Padding( - padding: const EdgeInsets.only(right: 10), - child: IconButton( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.clear_rounded) + ), + const SizedBox(width: 8), + Text( + AppLocalizations.of(context)!.createConnection, + style: const TextStyle( + fontSize: 20 + ), + ), + ], + ), + IconButton( + tooltip: widget.server == null + ? AppLocalizations.of(context)!.connect + : AppLocalizations.of(context)!.save, + onPressed: allDataValid == true + ? widget.server == null + ? () => connect() + : () => edit() + : null, + icon: Icon( + widget.server == null + ? Icons.login_rounded + : Icons.save_rounded + ) + ), + ], + ), + ), + Expanded( + child: ListView( + children: form() + ), + ) + ], + ), + ), + ); + } + else { + return Stack( + children: [ + Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context)!.createConnection), + actions: [ + IconButton( tooltip: widget.server == null ? AppLocalizations.of(context)!.connect : AppLocalizations.of(context)!.save, @@ -452,203 +666,49 @@ class _AddServerModalState extends State { : Icons.save_rounded ) ), - ), - ], - toolbarHeight: 70, + const SizedBox(width: 10) + ], + toolbarHeight: 70, + ), + body: ListView( + children: form(), + ) ), - body: ListView( - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - margin: const EdgeInsets.only( - top: 24, - left: 24, - right: 24 - ), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary.withOpacity(0.05), - borderRadius: BorderRadius.circular(30), - border: Border.all( - color: Theme.of(context).colorScheme.primary - ) - ), - child: Text( - "${connectionType.name}://${ipDomainController.text}${portController.text != '' ? ':${portController.text}' : ""}${pathController.text}", - textAlign: TextAlign.center, - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.w500 - ), - ), - ), - sectionLabel(AppLocalizations.of(context)!.general), - textField( - label: AppLocalizations.of(context)!.name, - controller: nameController, - icon: Icons.badge_rounded, - error: nameError, - onChanged: (value) { - if (value != '') { - setState(() => nameError = null); - } - else { - setState(() => nameError = AppLocalizations.of(context)!.nameNotEmpty); - } - checkDataValid(); - } - ), - sectionLabel(AppLocalizations.of(context)!.connection), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: SegmentedButton( - segments: const [ - ButtonSegment( - value: ConnectionType.http, - label: Text("HTTP") - ), - ButtonSegment( - value: ConnectionType.https, - label: Text("HTTPS") - ), - ], - selected: {connectionType}, - onSelectionChanged: (value) => setState(() => connectionType = value.first), - ), - ), - const SizedBox(height: 30), - textField( - label: AppLocalizations.of(context)!.ipDomain, - controller: ipDomainController, - icon: Icons.link_rounded, - error: ipDomainError, - keyboardType: TextInputType.url, - onChanged: validateAddress - ), - const SizedBox(height: 20), - textField( - label: AppLocalizations.of(context)!.path, - controller: pathController, - icon: Icons.route_rounded, - error: pathError, - onChanged: validateSubroute, - hintText: AppLocalizations.of(context)!.examplePath, - helperText: AppLocalizations.of(context)!.helperPath, - ), - const SizedBox(height: 20), - textField( - label: AppLocalizations.of(context)!.port, - controller: portController, - icon: Icons.numbers_rounded, - error: portError, - keyboardType: TextInputType.number, - onChanged: validatePort - ), - sectionLabel(AppLocalizations.of(context)!.authentication), - textField( - label: AppLocalizations.of(context)!.username, - controller: userController, - icon: Icons.person_rounded, - ), - const SizedBox(height: 20), - textField( - label: AppLocalizations.of(context)!.password, - controller: passwordController, - icon: Icons.lock_rounded, - keyboardType: TextInputType.visiblePassword, - obscureText: true - ), - sectionLabel(AppLocalizations.of(context)!.other), - Material( - color: Colors.transparent, - child: InkWell( - onTap: widget.server == null - ? () => setState(() => defaultServer = !defaultServer) - : null, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - AppLocalizations.of(context)!.defaultServer, - style: const TextStyle( - fontSize: 15, - ), - ), - Switch( - value: defaultServer, - onChanged: widget.server == null - ? (value) => setState(() => defaultServer = value) - : null, - ) - ], - ), - ), - ), - ), - const SizedBox(height: 20), - Material( - color: Colors.transparent, - child: InkWell( - onTap: () => setState(() => homeAssistant = !homeAssistant), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - AppLocalizations.of(context)!.runningHomeAssistant, - style: const TextStyle( - fontSize: 15, - ), - ), - Switch( - value: homeAssistant, - onChanged: (value) => setState(() => homeAssistant = value), - ) - ], - ), - ), - ), - ), - const SizedBox(height: 20), - ], - ), - ), - AnimatedOpacity( - opacity: isConnecting == true ? 1 : 0, - duration: const Duration(milliseconds: 250), - curve: Curves.easeInOut, - child: IgnorePointer( - ignoring: isConnecting == true ? false : true, - child: Scaffold( - backgroundColor: Colors.transparent, - body: Container( - width: mediaQuery.size.width, - height: mediaQuery.size.height, - color: const Color.fromRGBO(0, 0, 0, 0.7), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const CircularProgressIndicator( - color: Colors.white, - ), - const SizedBox(height: 30), - Text( - AppLocalizations.of(context)!.connecting, - style: const TextStyle( + AnimatedOpacity( + opacity: isConnecting == true ? 1 : 0, + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + child: IgnorePointer( + ignoring: isConnecting == true ? false : true, + child: Scaffold( + backgroundColor: Colors.transparent, + body: Container( + width: mediaQuery.size.width, + height: mediaQuery.size.height, + color: const Color.fromRGBO(0, 0, 0, 0.7), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const CircularProgressIndicator( color: Colors.white, - fontWeight: FontWeight.w500, - fontSize: 26 ), - ) - ], + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.connecting, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500, + fontSize: 26 + ), + ) + ], + ), ), ), ), - ), - ) - ], - ); + ) + ], + ); + } } } \ No newline at end of file diff --git a/lib/widgets/bottom_nav_bar.dart b/lib/widgets/bottom_nav_bar.dart index 5793442..36abbbe 100644 --- a/lib/widgets/bottom_nav_bar.dart +++ b/lib/widgets/bottom_nav_bar.dart @@ -89,6 +89,10 @@ class BottomNavBar extends StatelessWidget { if (value != 2) { logsProvider.resetFilters(); } + // Reset settings selected screen + if (value != screens.length-1) { + appConfigProvider.setSelectedSettingsScreen(screen: null); + } appConfigProvider.setSelectedScreen(value); }, ); diff --git a/lib/widgets/custom_list_tile.dart b/lib/widgets/custom_list_tile.dart index 34a7c80..6f27129 100644 --- a/lib/widgets/custom_list_tile.dart +++ b/lib/widgets/custom_list_tile.dart @@ -10,6 +10,7 @@ class CustomListTile extends StatelessWidget { final EdgeInsets? padding; final void Function()? onLongPress; final bool? disabled; + final void Function(bool)? onHover; const CustomListTile({ Key? key, @@ -21,7 +22,8 @@ class CustomListTile extends StatelessWidget { this.trailing, this.padding, this.onLongPress, - this.disabled + this.disabled, + this.onHover, }) : super(key: key); @override @@ -30,6 +32,7 @@ class CustomListTile extends StatelessWidget { color: Colors.transparent, child: InkWell( onTap: onTap, + onHover: onHover, onLongPress: onLongPress, child: Padding( padding: padding ?? const EdgeInsets.symmetric(horizontal: 16, vertical: 12), diff --git a/lib/widgets/custom_radio_list_tile.dart b/lib/widgets/custom_radio_list_tile.dart index b800aa3..4d59b3c 100644 --- a/lib/widgets/custom_radio_list_tile.dart +++ b/lib/widgets/custom_radio_list_tile.dart @@ -41,33 +41,35 @@ class CustomRadioListTile extends StatelessWidget { backgroundColor: radioBackgroundColor, ), const SizedBox(width: 24), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: MediaQuery.of(context).size.width-110, - child: Text( - title, - style: TextStyle( - fontSize: 16, - color: Theme.of(context).colorScheme.onSurface - ), - ), - ), - if (subtitle != null) ...[ - const SizedBox(height: 5), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ SizedBox( width: MediaQuery.of(context).size.width-110, child: Text( - subtitle!, + title, style: TextStyle( - color: Theme.of(context).listTileTheme.textColor, - fontSize: 14 + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface ), ), ), - ] - ], + if (subtitle != null) ...[ + const SizedBox(height: 5), + SizedBox( + width: MediaQuery.of(context).size.width-110, + child: Text( + subtitle!, + style: TextStyle( + color: Theme.of(context).listTileTheme.textColor, + fontSize: 14 + ), + ), + ), + ] + ], + ), ), ], ), diff --git a/lib/widgets/custom_settings_tile.dart b/lib/widgets/custom_settings_tile.dart new file mode 100644 index 0000000..554f13b --- /dev/null +++ b/lib/widgets/custom_settings_tile.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; + +class CustomSettingsTile extends StatelessWidget { + final String title; + final String? subtitle; + final Widget? subtitleWidget; + final void Function()? onTap; + final IconData? icon; + final Widget? trailing; + final EdgeInsets? padding; + final int thisItem; + final int? selectedItem; + + const CustomSettingsTile({ + Key? key, + required this.title, + this.subtitle, + this.subtitleWidget, + this.onTap, + this.icon, + this.trailing, + this.padding, + required this.thisItem, + required this.selectedItem, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + Widget tileBody = Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (icon != null) ...[ + Icon( + icon, + size: 24, + color: Theme.of(context).listTileTheme.iconColor, + ), + const SizedBox(width: 16), + ], + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + if (subtitle != null || subtitleWidget != null) ...[ + const SizedBox(height: 5), + if (subtitle == null && subtitleWidget != null) subtitleWidget!, + if (subtitle != null && subtitleWidget == null) Text( + subtitle!, + style: TextStyle( + color: Theme.of(context).listTileTheme.textColor, + fontSize: 14, + fontWeight: FontWeight.w400 + ), + ), + ] + ], + ), + ) + ], + ), + ), + if (trailing != null) ...[ + const SizedBox(width: 10), + trailing! + ] + ], + ); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(28), + child: InkWell( + borderRadius: BorderRadius.circular(28), + onTap: onTap, + child: Container( + width: double.maxFinite, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(28), + color: thisItem == selectedItem + ? Theme.of(context).colorScheme.primaryContainer + : null + ), + child: tileBody + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/menu_bar.dart b/lib/widgets/menu_bar.dart new file mode 100644 index 0000000..0151b87 --- /dev/null +++ b/lib/widgets/menu_bar.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/config/app_screens.dart'; +import 'package:adguard_home_manager/models/app_screen.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; + +class CustomMenuBar extends StatelessWidget { + final Widget child; + + const CustomMenuBar({ + Key? key, + required this.child + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final serversProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + + List screens = serversProvider.selectedServer != null + ? screensServerConnected + : screensSelectServer; + + String translatedName(String key) { + switch (key) { + case 'connect': + return AppLocalizations.of(context)!.connect; + + case 'home': + return AppLocalizations.of(context)!.home; + + case 'settings': + return AppLocalizations.of(context)!.settings; + + case 'clients': + return AppLocalizations.of(context)!.clients; + + case 'logs': + return AppLocalizations.of(context)!.logs; + + case 'filters': + return AppLocalizations.of(context)!.filters; + + default: + return ''; + } + } + + return PlatformMenuBar( + menus: [ + PlatformMenu( + label: 'AdGuard Home Manager', + menus: [ + if ( + PlatformProvidedMenuItem.hasMenu(PlatformProvidedMenuItemType.about) + ) const PlatformMenuItemGroup( + members: [ + PlatformProvidedMenuItem( + type: PlatformProvidedMenuItemType.about, + ), + ] + ), + if ( + PlatformProvidedMenuItem.hasMenu(PlatformProvidedMenuItemType.quit) + ) const PlatformMenuItemGroup( + members: [ + PlatformProvidedMenuItem( + type: PlatformProvidedMenuItemType.quit, + ), + ] + ) + ], + ), + PlatformMenu( + label: AppLocalizations.of(context)!.screens, + menus: [ + PlatformMenuItemGroup( + members: screens.asMap().entries.map((e) => PlatformMenuItem( + label: "${appConfigProvider.selectedScreen == e.key ? '✔' : ''} ${translatedName(e.value.name)}", + onSelected: () => appConfigProvider.setSelectedScreen(e.key), + )).toList() + ), + ], + ), + ], + child: child, + ); + } +} \ No newline at end of file diff --git a/lib/widgets/navigation_rail.dart b/lib/widgets/navigation_rail.dart new file mode 100644 index 0000000..6dc028f --- /dev/null +++ b/lib/widgets/navigation_rail.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/providers/logs_provider.dart'; +import 'package:adguard_home_manager/config/app_screens.dart'; +import 'package:adguard_home_manager/models/app_screen.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; + +class SideNavigationRail extends StatelessWidget { + const SideNavigationRail({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final serversProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + final logsProvider = Provider.of(context); + + List screens = serversProvider.selectedServer != null + ? screensServerConnected + : screensSelectServer; + + String translatedName(String key) { + switch (key) { + case 'home': + return AppLocalizations.of(context)!.home; + + case 'settings': + return AppLocalizations.of(context)!.settings; + + case 'connect': + return AppLocalizations.of(context)!.connect; + + case 'clients': + return AppLocalizations.of(context)!.clients; + + case 'logs': + return AppLocalizations.of(context)!.logs; + + case 'filters': + return AppLocalizations.of(context)!.filters; + + default: + return ''; + } + } + + return NavigationRail( + selectedIndex: appConfigProvider.selectedScreen, + destinations: screens.map((screen) => NavigationRailDestination( + icon: Icon( + screen.icon, + color: screens[appConfigProvider.selectedScreen] == screen + ? Theme.of(context).colorScheme.onSecondaryContainer + : Theme.of(context).colorScheme.onSurfaceVariant, + ), + label: Text(translatedName(screen.name)) + )).toList(), + onDestinationSelected: (value) { + // Reset clients tab to 0 when changing screen + if (value != 1) { + appConfigProvider.setSelectedClientsTab(0); + } + // Reset logs filters when changing screen + if (value != 2) { + logsProvider.resetFilters(); + } + // Reset settings selected screen + if (value != screens.length-1) { + appConfigProvider.setSelectedSettingsScreen(screen: null); + } + appConfigProvider.setSelectedScreen(value); + }, + labelType: NavigationRailLabelType.all, + useIndicator: true, + groupAlignment: 0, + backgroundColor: Theme.of(context).colorScheme.primary.withOpacity(0.05), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/process_dialog.dart b/lib/widgets/process_dialog.dart index 84c1185..d8794d8 100644 --- a/lib/widgets/process_dialog.dart +++ b/lib/widgets/process_dialog.dart @@ -20,6 +20,7 @@ class ProcessDialog extends StatelessWidget { horizontal: 30 ), child: Row( + mainAxisSize: MainAxisSize.min, children: [ const CircularProgressIndicator(), const SizedBox(width: 40), diff --git a/lib/widgets/servers_list/delete_modal.dart b/lib/widgets/servers_list/delete_modal.dart index 2cc9809..9d51fe3 100644 --- a/lib/widgets/servers_list/delete_modal.dart +++ b/lib/widgets/servers_list/delete_modal.dart @@ -23,9 +23,17 @@ class DeleteModal extends StatelessWidget { final appConfigProvider = Provider.of(context); void removeServer() async { + final previouslySelectedServer = serversProvider.selectedServer; + final deleted = await serversProvider.removeServer(serverToDelete); + Navigator.pop(context); + if (deleted == true) { + if (previouslySelectedServer != null && previouslySelectedServer.id == serverToDelete.id) { + appConfigProvider.setSelectedScreen(0); + } + showSnacbkar( context: context, appConfigProvider: appConfigProvider, diff --git a/lib/widgets/servers_list/servers_list.dart b/lib/widgets/servers_list/servers_list.dart index c69f5ed..d4cd9cf 100644 --- a/lib/widgets/servers_list/servers_list.dart +++ b/lib/widgets/servers_list/servers_list.dart @@ -5,6 +5,7 @@ import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/widgets/servers_list/servers_list_item.dart'; +import 'package:adguard_home_manager/widgets/servers_list/servers_tile_item.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart'; @@ -13,6 +14,7 @@ class ServersList extends StatelessWidget { final List controllers; final Function(int) onChange; final ScrollController scrollController; + final double breakingWidth; const ServersList({ Key? key, @@ -20,23 +22,44 @@ class ServersList extends StatelessWidget { required this.controllers, required this.onChange, required this.scrollController, + required this.breakingWidth }) : super(key: key); @override Widget build(BuildContext context) { final serversProvider = Provider.of(context); + + final width = MediaQuery.of(context).size.width; if (serversProvider.serversList.isNotEmpty) { - return ListView.builder( - controller: scrollController, - itemCount: serversProvider.serversList.length, - itemBuilder: (context, index) => ServersListItem( - expandableController: controllers[index], - server: serversProvider.serversList[index], - index: index, - onChange: onChange - ) - ); + if (width > breakingWidth) { + return ListView( + children: [ + Wrap( + children: serversProvider.serversList.asMap().entries.map( + (s) => ServersTileItem( + server: serversProvider.serversList[s.key], + index: s.key, + onChange: onChange + ) + ).toList(), + ), + const SizedBox(height: 8) + ], + ); + } + else { + return ListView.builder( + controller: scrollController, + itemCount: serversProvider.serversList.length, + itemBuilder: (context, index) => ServersListItem( + expandableController: controllers[index], + server: serversProvider.serversList[index], + index: index, + onChange: onChange + ) + ); + } } else { return SizedBox( diff --git a/lib/widgets/servers_list/servers_list_item.dart b/lib/widgets/servers_list/servers_list_item.dart index 919a680..20b9551 100644 --- a/lib/widgets/servers_list/servers_list_item.dart +++ b/lib/widgets/servers_list/servers_list_item.dart @@ -71,6 +71,8 @@ class _ServersListItemState extends State with SingleTickerProv final serversProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + void showDeleteModal(Server server) async { await Future.delayed(const Duration(seconds: 0), () => { showDialog( @@ -85,10 +87,25 @@ class _ServersListItemState extends State with SingleTickerProv void openAddServerBottomSheet({Server? server}) async { await Future.delayed(const Duration(seconds: 0), (() => { - Navigator.push(context, MaterialPageRoute( - fullscreenDialog: true, - builder: (BuildContext context) => AddServerModal(server: server) - )) + if (width > 700) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AddServerModal( + server: server, + window: true, + ), + ) + } + else { + Navigator.push(context, MaterialPageRoute( + fullscreenDialog: true, + builder: (BuildContext context) => AddServerModal( + server: server, + window: false, + ) + )) + } })); } @@ -356,7 +373,6 @@ class _ServersListItemState extends State with SingleTickerProv ); } - return Container( decoration: BoxDecoration( border: Border( diff --git a/lib/widgets/servers_list/servers_tile_item.dart b/lib/widgets/servers_list/servers_tile_item.dart new file mode 100644 index 0000000..bf67b6f --- /dev/null +++ b/lib/widgets/servers_list/servers_tile_item.dart @@ -0,0 +1,381 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/widgets/add_server_modal.dart'; +import 'package:adguard_home_manager/widgets/servers_list/delete_modal.dart'; + +import 'package:adguard_home_manager/classes/process_modal.dart'; +import 'package:adguard_home_manager/functions/snackbar.dart'; +import 'package:adguard_home_manager/models/app_log.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; +import 'package:adguard_home_manager/services/http_requests.dart'; +import 'package:adguard_home_manager/models/server.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; + +class ServersTileItem extends StatefulWidget { + final Server server; + final int index; + final void Function(int) onChange; + + const ServersTileItem({ + Key? key, + required this.server, + required this.index, + required this.onChange + }) : super(key: key); + + @override + State createState() => _ServersTileItemState(); +} + +class _ServersTileItemState extends State with SingleTickerProviderStateMixin { + @override + Widget build(BuildContext context) { + final serversProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + + final width = MediaQuery.of(context).size.width; + + void showDeleteModal(Server server) async { + await Future.delayed(const Duration(seconds: 0), () => { + showDialog( + context: context, + builder: (context) => DeleteModal( + serverToDelete: server, + ), + barrierDismissible: false + ) + }); + } + + void openAddServerBottomSheet({Server? server}) async { + await Future.delayed(const Duration(seconds: 0), (() => { + if (width > 700) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AddServerModal( + server: server, + window: true, + ), + ) + } + else { + Navigator.push(context, MaterialPageRoute( + fullscreenDialog: true, + builder: (BuildContext context) => AddServerModal( + server: server, + window: false, + ) + )) + } + })); + } + + void connectToServer(Server server) async { + final ProcessModal process = ProcessModal(context: context); + process.open(AppLocalizations.of(context)!.connecting); + + final result = server.runningOnHa == true + ? await loginHA(server) + : await login(server); + + if (result['result'] == 'success') { + serversProvider.setSelectedServer(server); + + serversProvider.setServerStatusLoad(0); + final serverStatus = await getServerStatus(server); + if (serverStatus['result'] == 'success') { + serversProvider.setServerStatusData(serverStatus['data']); + serversProvider.checkServerUpdatesAvailable(server); + serversProvider.setServerStatusLoad(1); + } + else { + appConfigProvider.addLog(serverStatus['log']); + serversProvider.setServerStatusLoad(2); + } + + process.close(); + } + else { + process.close(); + appConfigProvider.addLog(result['log']); + showSnacbkar( + context: context, + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.cannotConnect, + color: Colors.red + ); + } + } + + void setDefaultServer(Server server) async { + final result = await serversProvider.setDefaultServer(server); + if (result == null) { + showSnacbkar( + context: context, + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.connectionDefaultSuccessfully, + color: Colors.green + ); + } + else { + appConfigProvider.addLog( + AppLog( + type: 'set_default_server', + dateTime: DateTime.now(), + message: result.toString() + ) + ); + showSnacbkar( + context: context, + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.connectionDefaultFailed, + color: Colors.red + ); + } + } + + Widget leadingIcon(Server server) { + if (server.defaultServer == true) { + return Stack( + alignment: Alignment.center, + children: [ + Icon( + Icons.storage_rounded, + color: serversProvider.selectedServer != null && serversProvider.selectedServer?.id == server.id + ? serversProvider.serverStatus.data != null + ? Colors.green + : Colors.orange + : null, + ), + SizedBox( + width: 25, + height: 25, + child: Stack( + alignment: Alignment.bottomRight, + children: [ + Container( + padding: const EdgeInsets.all(1), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(20) + ), + child: Icon( + Icons.star, + color: Theme.of(context).colorScheme.onPrimaryContainer, + size: 10, + ), + ), + ], + ), + ) + ], + ); + } + else { + return Icon( + Icons.storage_rounded, + color: serversProvider.selectedServer != null && serversProvider.selectedServer?.id == server.id + ? serversProvider.serverStatus.data != null + ? Colors.green + : Colors.orange + : null, + ); + } + } + + Widget topRow(Server server, int index) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + children: [ + Container( + margin: const EdgeInsets.only(right: 16), + child: leadingIcon(server), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${server.connectionMethod}://${server.domain}${server.path ?? ""}${server.port != null ? ':${server.port}' : ""}", + textAlign: TextAlign.left, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface + ), + ), + Column( + children: [ + const SizedBox(height: 3), + Text( + server.name, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ) + ], + ) + ], + ), + ), + ], + ), + ), + ], + ); + } + + Widget bottomRow(Server server, int index) { + return Column( + children: [ + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + PopupMenuButton( + // color: Theme.of(context).dialogBackgroundColor, + itemBuilder: (context) => [ + PopupMenuItem( + enabled: server.defaultServer == false + ? true + : false, + onTap: server.defaultServer == false + ? (() => setDefaultServer(server)) + : null, + child: SizedBox( + child: Row( + children: [ + const Icon(Icons.star), + const SizedBox(width: 15), + Text( + server.defaultServer == true + ? AppLocalizations.of(context)!.defaultConnection + : AppLocalizations.of(context)!.setDefault, + ) + ], + ), + ) + ), + PopupMenuItem( + onTap: (() => openAddServerBottomSheet(server: server)), + child: Row( + children: [ + const Icon(Icons.edit), + const SizedBox(width: 15), + Text(AppLocalizations.of(context)!.edit) + ], + ) + ), + PopupMenuItem( + onTap: (() => showDeleteModal(server)), + child: Row( + children: [ + const Icon(Icons.delete), + const SizedBox(width: 15), + Text(AppLocalizations.of(context)!.delete) + ], + ) + ), + ] + ), + SizedBox( + child: serversProvider.selectedServer != null && + serversProvider.selectedServer != null && serversProvider.selectedServer?.id == server.id && serversProvider.serverStatus.data != null && + serversProvider.selectedServer?.id == server.id + ? Container( + margin: const EdgeInsets.only(right: 12), + padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10), + decoration: BoxDecoration( + color: serversProvider.selectedServer != null && serversProvider.selectedServer?.id == server.id && serversProvider.serverStatus.data != null + ? Colors.green + : Colors.orange, + borderRadius: BorderRadius.circular(30) + ), + child: Row( + children: [ + Icon( + serversProvider.selectedServer != null && serversProvider.selectedServer?.id == server.id && serversProvider.serverStatus.data != null + ? Icons.check + : Icons.warning, + color: Colors.white, + ), + const SizedBox(width: 10), + Text( + serversProvider.selectedServer != null && serversProvider.selectedServer?.id == server.id && serversProvider.serverStatus.data != null + ? AppLocalizations.of(context)!.connected + : AppLocalizations.of(context)!.selectedDisconnected, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500 + ), + ) + ], + ), + ) + : Container( + margin: const EdgeInsets.only(right: 10), + child: TextButton( + onPressed: () => connectToServer(server), + child: Text(AppLocalizations.of(context)!.connect), + ), + ), + ) + ], + ) + ], + ); + } + + EdgeInsets generateMargins(int index) { + if (index == 0) { + return const EdgeInsets.only(top: 16, left: 16, right: 8, bottom: 8); + } + if (index == 1) { + return const EdgeInsets.only(top: 16, left: 8, right: 16, bottom: 8); + } + else if (index == serversProvider.serversList.length-1 && (index+1)%2 == 0) { + return const EdgeInsets.only(top: 8, left: 8, right: 16, bottom: 16); + } + else if (index == serversProvider.serversList.length-1 && (index+1)%2 == 1) { + return const EdgeInsets.only(top: 8, left: 16, right: 8, bottom: 16); + } + else { + if ((index+1)%2 == 0) { + return const EdgeInsets.only(top: 8, left: 8, right: 16, bottom: 8); + } + else { + return const EdgeInsets.only(top: 8, left: 16, right: 8, bottom: 8); + } + } + } + + return FractionallySizedBox( + widthFactor: 0.5, + child: Card( + margin: generateMargins(widget.index), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + topRow(widget.server, widget.index), + bottomRow(widget.server, widget.index) + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/tab_content_list.dart b/lib/widgets/tab_content_list.dart index 5171081..d1f2ad0 100644 --- a/lib/widgets/tab_content_list.dart +++ b/lib/widgets/tab_content_list.dart @@ -15,6 +15,8 @@ class CustomTabContentList extends StatelessWidget { final double? refreshIndicatorOffset; final Widget? fab; final bool? fabVisible; + final bool? noSliver; + final EdgeInsets? listPadding; const CustomTabContentList({ Key? key, @@ -27,7 +29,9 @@ class CustomTabContentList extends StatelessWidget { required this.onRefresh, this.refreshIndicatorOffset, this.fab, - this.fabVisible + this.fabVisible, + this.noSliver, + this.listPadding }) : super(key: key); @override @@ -36,95 +40,157 @@ class CustomTabContentList extends StatelessWidget { switch (loadStatus) { case LoadStatus.loading: - return SafeArea( - top: false, - bottom: false, - child: Builder( - builder: (BuildContext context) => CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - ), - SliverFillRemaining( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: loadingGenerator() + if (noSliver == true) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: loadingGenerator() + ); + } + else { + return SafeArea( + top: false, + bottom: false, + child: Builder( + builder: (BuildContext context) => CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), ), - ) - ], - ), - ) - ); + SliverFillRemaining( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: loadingGenerator() + ), + ) + ], + ), + ) + ); + } case LoadStatus.loaded: - return Stack( - children: [ - SafeArea( - top: false, - bottom: false, - child: Builder( - builder: (BuildContext context) { - return RefreshIndicator( - onRefresh: onRefresh, - edgeOffset: refreshIndicatorOffset ?? 95, - child: CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - ), - if (itemsCount > 0) SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) => contentWidget(index), - childCount: itemsCount + if (noSliver == true) { + if (itemsCount > 0) { + return Stack( + children: [ + ListView.builder( + padding: listPadding, + itemCount: itemsCount, + itemBuilder: (context, index) => contentWidget(index), + ), + if (fab != null) AnimatedPositioned( + duration: const Duration(milliseconds: 100), + curve: Curves.easeInOut, + bottom: fabVisible != null && fabVisible == true ? + appConfigProvider.showingSnackbar + ? 70 : 20 + : -70, + right: 20, + child: fab! + ), + ], + ); + } + else { + return Stack( + children: [ + noData, + if (fab != null) AnimatedPositioned( + duration: const Duration(milliseconds: 100), + curve: Curves.easeInOut, + bottom: fabVisible != null && fabVisible == true ? + appConfigProvider.showingSnackbar + ? 70 : 20 + : -70, + right: 20, + child: fab! + ), + ], + ); + } + } + else { + return Stack( + children: [ + SafeArea( + top: false, + bottom: false, + child: Builder( + builder: (BuildContext context) { + return RefreshIndicator( + onRefresh: onRefresh, + edgeOffset: refreshIndicatorOffset ?? 70, + child: CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), ), - ), - if (itemsCount == 0) SliverFillRemaining( - child: noData, - ) - ], - ), - ); - }, + if (itemsCount > 0) SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) => contentWidget(index), + childCount: itemsCount + ), + ), + if (itemsCount == 0) SliverFillRemaining( + child: noData, + ) + ], + ), + ); + }, + ), ), - ), - if (fab != null) AnimatedPositioned( - duration: const Duration(milliseconds: 100), - curve: Curves.easeInOut, - bottom: fabVisible != null && fabVisible == true ? - appConfigProvider.showingSnackbar - ? 70 : 20 - : -70, - right: 20, - child: fab! - ), - ], - ); + if (fab != null) AnimatedPositioned( + duration: const Duration(milliseconds: 100), + curve: Curves.easeInOut, + bottom: fabVisible != null && fabVisible == true ? + appConfigProvider.showingSnackbar + ? 70 : 20 + : -70, + right: 20, + child: fab! + ), + ], + ); + } case LoadStatus.error: - return SafeArea( - top: false, - bottom: false, - child: Builder( - builder: (BuildContext context) => CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - ), - SliverFillRemaining( - child: Padding( - padding: const EdgeInsets.only( - top: 95, - left: 16, - right: 16 - ), - child: errorGenerator() - ), - ) - ], + if (noSliver == true) { + return Padding( + padding: const EdgeInsets.only( + top: 95, + left: 16, + right: 16 ), - ) - ); + child: errorGenerator() + ); + } + else { + return SafeArea( + top: false, + bottom: false, + child: Builder( + builder: (BuildContext context) => CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + ), + SliverFillRemaining( + child: Padding( + padding: const EdgeInsets.only( + top: 95, + left: 16, + right: 16 + ), + child: errorGenerator() + ), + ) + ], + ), + ) + ); + } default: return const SizedBox(); diff --git a/lib/widgets/update_modal.dart b/lib/widgets/update_modal.dart index b3acc8d..a6f456a 100644 --- a/lib/widgets/update_modal.dart +++ b/lib/widgets/update_modal.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -22,14 +24,30 @@ class UpdateModal extends StatefulWidget { class _UpdateModalState extends State { bool doNotRemember = false; - String getDownloadLink() { - return widget.gitHubRelease.assets.firstWhere((item) => item.browserDownloadUrl.contains('apk')).browserDownloadUrl; + String? getDownloadLink() { + if (Platform.isAndroid) { + return widget.gitHubRelease.assets.firstWhere((item) => item.browserDownloadUrl.contains('apk')).browserDownloadUrl; + } + else if (Platform.isMacOS) { + return widget.gitHubRelease.assets.firstWhere((item) => item.browserDownloadUrl.contains('macOS')).browserDownloadUrl; // macOS package is a zip + } + else if (Platform.isWindows) { + return widget.gitHubRelease.assets.firstWhere((item) => item.browserDownloadUrl.contains('exe')).browserDownloadUrl; + } + else if (Platform.isLinux) { + return widget.gitHubRelease.assets.firstWhere((item) => item.browserDownloadUrl.contains('deb')).browserDownloadUrl; + } + else { + return null; + } } @override Widget build(BuildContext context) { final appConfigProvider = Provider.of(context); + final downloadLink = getDownloadLink(); + return AlertDialog( scrollable: true, title: Column( @@ -104,10 +122,10 @@ class _UpdateModalState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - TextButton( + if (downloadLink != null) TextButton( onPressed: () { Navigator.pop(context); - widget.onDownload(getDownloadLink(), widget.gitHubRelease.tagName); + widget.onDownload(downloadLink, widget.gitHubRelease.tagName); }, child: Text(AppLocalizations.of(context)!.download) ), diff --git a/linux/.gitignore b/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt new file mode 100644 index 0000000..bf11fa4 --- /dev/null +++ b/linux/CMakeLists.txt @@ -0,0 +1,138 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "adguard_home_manager") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.jgeek00.adguard_home_manager") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/linux/flutter/CMakeLists.txt b/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..cb9d874 --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,27 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) dynamic_color_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); + dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); + g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); + sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); + g_autoptr(FlPluginRegistrar) window_size_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "WindowSizePlugin"); + window_size_plugin_register_with_registrar(window_size_registrar); +} diff --git a/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..da06542 --- /dev/null +++ b/linux/flutter/generated_plugins.cmake @@ -0,0 +1,27 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + dynamic_color + sqlite3_flutter_libs + url_launcher_linux + window_size +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/linux/main.cc b/linux/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/linux/my_application.cc b/linux/my_application.cc new file mode 100644 index 0000000..1f79b20 --- /dev/null +++ b/linux/my_application.cc @@ -0,0 +1,104 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "AdGuard Home Manager"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "AdGuard Home Manager"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/linux/my_application.h b/linux/my_application.h new file mode 100644 index 0000000..72271d5 --- /dev/null +++ b/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/macos/.gitignore b/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..4b81f9b --- /dev/null +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..5caa9d1 --- /dev/null +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..1c34d3c --- /dev/null +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,24 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import device_info_plus_macos +import dynamic_color +import package_info_plus_macos +import sqflite +import sqlite3_flutter_libs +import url_launcher_macos +import window_size + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) + DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) + FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + WindowSizePlugin.register(with: registry.registrar(forPlugin: "WindowSizePlugin")) +} diff --git a/macos/Podfile b/macos/Podfile new file mode 100644 index 0000000..049abe2 --- /dev/null +++ b/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.14' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/macos/Podfile.lock b/macos/Podfile.lock new file mode 100644 index 0000000..9346410 --- /dev/null +++ b/macos/Podfile.lock @@ -0,0 +1,82 @@ +PODS: + - device_info_plus_macos (0.0.1): + - FlutterMacOS + - dynamic_color (0.0.2): + - FlutterMacOS + - FlutterMacOS (1.0.0) + - FMDB (2.7.5): + - FMDB/standard (= 2.7.5) + - FMDB/standard (2.7.5) + - package_info_plus_macos (0.0.1): + - FlutterMacOS + - sqflite (0.0.2): + - FlutterMacOS + - FMDB (>= 2.7.5) + - sqlite3 (3.41.0): + - sqlite3/common (= 3.41.0) + - sqlite3/common (3.41.0) + - sqlite3/fts5 (3.41.0): + - sqlite3/common + - sqlite3/perf-threadsafe (3.41.0): + - sqlite3/common + - sqlite3/rtree (3.41.0): + - sqlite3/common + - sqlite3_flutter_libs (0.0.1): + - FlutterMacOS + - sqlite3 (~> 3.41.0) + - sqlite3/fts5 + - sqlite3/perf-threadsafe + - sqlite3/rtree + - url_launcher_macos (0.0.1): + - FlutterMacOS + - window_size (0.0.2): + - FlutterMacOS + +DEPENDENCIES: + - device_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus_macos/macos`) + - dynamic_color (from `Flutter/ephemeral/.symlinks/plugins/dynamic_color/macos`) + - FlutterMacOS (from `Flutter/ephemeral`) + - package_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos`) + - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`) + - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + - window_size (from `Flutter/ephemeral/.symlinks/plugins/window_size/macos`) + +SPEC REPOS: + trunk: + - FMDB + - sqlite3 + +EXTERNAL SOURCES: + device_info_plus_macos: + :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus_macos/macos + dynamic_color: + :path: Flutter/ephemeral/.symlinks/plugins/dynamic_color/macos + FlutterMacOS: + :path: Flutter/ephemeral + package_info_plus_macos: + :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos + sqflite: + :path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos + sqlite3_flutter_libs: + :path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + window_size: + :path: Flutter/ephemeral/.symlinks/plugins/window_size/macos + +SPEC CHECKSUMS: + device_info_plus_macos: 1ad388a1ef433505c4038e7dd9605aadd1e2e9c7 + dynamic_color: 394d6a888650f8534e029b27d2f8bc5c64e44008 + FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a + package_info_plus_macos: f010621b07802a241d96d01876d6705f15e77c1c + sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea + sqlite3: d31b2b69d59bd1b4ab30e5c92eb18fd8e82fa392 + sqlite3_flutter_libs: f20746e4a0245afbee4f20d9afc0072ebff7cc26 + url_launcher_macos: 5335912b679c073563f29d89d33d10d459f95451 + window_size: 339dafa0b27a95a62a843042038fa6c3c48de195 + +PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7 + +COCOAPODS: 1.11.2 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..a0fa417 --- /dev/null +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,650 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + E73513DE6359E03D97EE7354 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7D05500669291AB89B123249 /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0C04B50F31022114B20541C8 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 2B20822A5E5ADA2133261EFB /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* AdGuard Home Manager.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "AdGuard Home Manager.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 7D05500669291AB89B123249 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + 985E47E8D393FC1DF77CB4F3 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E73513DE6359E03D97EE7354 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 46371A716FC111389EADDE5D /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* AdGuard Home Manager.app */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + 46371A716FC111389EADDE5D /* Pods */ = { + isa = PBXGroup; + children = ( + 2B20822A5E5ADA2133261EFB /* Pods-Runner.debug.xcconfig */, + 985E47E8D393FC1DF77CB4F3 /* Pods-Runner.release.xcconfig */, + 0C04B50F31022114B20541C8 /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 7D05500669291AB89B123249 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 7C0020C10EF81232492FC538 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + CC375DDE73754317056A739B /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* AdGuard Home Manager.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 7C0020C10EF81232492FC538 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + CC375DDE73754317056A739B /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 53; + DEVELOPMENT_TEAM = 38Z3B9TJTR; + INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "AdGuard Home Manager"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MARKETING_VERSION = 2.0.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.jgeek00.adguard-home-manager"; + PRODUCT_NAME = "AdGuard Home Manager"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 53; + DEVELOPMENT_TEAM = 38Z3B9TJTR; + INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "AdGuard Home Manager"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MARKETING_VERSION = 2.0.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.jgeek00.adguard-home-manager"; + PRODUCT_NAME = "AdGuard Home Manager"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 53; + DEVELOPMENT_TEAM = 38Z3B9TJTR; + INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "AdGuard Home Manager"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MARKETING_VERSION = 2.0.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.jgeek00.adguard-home-manager"; + PRODUCT_NAME = "AdGuard Home Manager"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..0a0b302 --- /dev/null +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..d53ef64 --- /dev/null +++ b/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..96d3fee --- /dev/null +++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "info": { + "version": 1, + "author": "xcode" + }, + "images": [ + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_16.png", + "scale": "1x" + }, + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "2x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "1x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_64.png", + "scale": "2x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_128.png", + "scale": "1x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "2x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "1x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "2x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "1x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_1024.png", + "scale": "2x" + } + ] +} \ No newline at end of file diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..8b1c839 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..2f23ec2 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..066823a Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..5f8cce7 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..7856b4a Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..e851645 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..1a2df1d Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/macos/Runner/Base.lproj/MainMenu.xib b/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..01712f7 --- /dev/null +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = adguard_home_manager + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.jgeek00.adguardHomeManager + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2023 com.jgeek00. All rights reserved. diff --git a/macos/Runner/Configs/Debug.xcconfig b/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Release.xcconfig b/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Warnings.xcconfig b/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..08c3ab1 --- /dev/null +++ b/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.network.client + + + diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist new file mode 100644 index 0000000..a7cd9c5 --- /dev/null +++ b/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + AdGuard Home Manager + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..2722837 --- /dev/null +++ b/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements new file mode 100644 index 0000000..ee95ab7 --- /dev/null +++ b/macos/Runner/Release.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/pubspec.lock b/pubspec.lock index 93d9e05..fd67c77 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -77,10 +77,10 @@ packages: dependency: transitive description: name: cli_util - sha256: "66f86e916d285c1a93d3b79587d94bd71984a66aac4ff74e524cfa7877f1395c" + sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 url: "https://pub.dev" source: hosted - version: "0.3.5" + version: "0.4.0" clock: dependency: transitive description: @@ -258,10 +258,10 @@ packages: dependency: "direct dev" description: name: flutter_launcher_icons - sha256: a9de6706cd844668beac27c0aed5910fa0534832b3c2cad61a5fd977fce82a5d + sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" url: "https://pub.dev" source: hosted - version: "0.10.0" + version: "0.13.1" flutter_lints: dependency: "direct dev" description: @@ -287,10 +287,19 @@ packages: dependency: "direct dev" description: name: flutter_native_splash - sha256: "6777a3abb974021a39b5fdd2d46a03ca390e03903b6351f21d10e7ecc969f12d" + sha256: "048bd1f1dc0e5ea25899f702815934d9a9e916fe23451c320e7dd94d5e3ad933" url: "https://pub.dev" source: hosted - version: "2.2.16" + version: "2.2.17" + flutter_split_view: + dependency: "direct main" + description: + path: "." + ref: master-alt + resolved-ref: a6aa2419243cc0abccf9474ce340790d6c0ded6f + url: "https://github.com/JGeek00/flutter_split_view" + source: git + version: "0.1.2" flutter_svg: dependency: "direct main" description: @@ -345,10 +354,10 @@ packages: dependency: transitive description: name: image - sha256: "8e9d133755c3e84c73288363e6343157c383a0c6c56fc51afcc5d4d7180306d6" + sha256: "73964e3609fb96e01e69b0924b939967c556e46c7ff05db2ea9e31019000f4ef" url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "4.0.16" intl: dependency: "direct main" description: @@ -558,18 +567,42 @@ packages: dependency: "direct main" description: name: sqflite - sha256: "067ab48dbc66bae05e18073a604443baa35957101bd3905b94f65e764c6d0688" + sha256: "8453780d1f703ead201a39673deb93decf85d543f359f750e2afc4908b55533f" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.2.8" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: b2ed22d1d62c944ec0dac5cc687ae99cb3331c3ebe146d726ed24704634b5ccd + sha256: e77abf6ff961d69dfef41daccbb66b51e9983cdd5cb35bf30733598057401555 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.5" + sqflite_common_ffi: + dependency: "direct main" + description: + name: sqflite_common_ffi + sha256: f86de82d37403af491b21920a696b19f01465b596f545d1acd4d29a0a72418ad + url: "https://pub.dev" + source: hosted + version: "2.2.5" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: a3ba4b66a7ab170ce7aa3f5ac43c19ee8d6637afbe7b7c95c94112b4f4d91566 + url: "https://pub.dev" + source: hosted + version: "1.11.0" + sqlite3_flutter_libs: + dependency: "direct main" + description: + name: sqlite3_flutter_libs + sha256: "02f80aea54a19a36b347dedf6d4181ecd9107f5831ea6139cfd0376a3de197ba" + url: "https://pub.dev" + source: hosted + version: "0.5.13" stack_trace: dependency: transitive description: @@ -642,6 +675,70 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.4" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: "75f2846facd11168d007529d6cd8fcb2b750186bea046af9711f10b907e1587e" + url: "https://pub.dev" + source: hosted + version: "6.1.10" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "22f8db4a72be26e9e3a4aa3f194b1f7afbc76d20ec141f84be1d787db2155cbd" + url: "https://pub.dev" + source: hosted + version: "6.0.31" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" + url: "https://pub.dev" + source: hosted + version: "6.1.4" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "91ee3e75ea9dadf38036200c5d3743518f4a5eb77a8d13fda1ee5764373f185e" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "81fe91b6c4f84f222d186a9d23c73157dc4c8e1c71489c4d08be1ad3b228f1aa" + url: "https://pub.dev" + source: hosted + version: "2.0.16" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "254708f17f7c20a9c8c471f67d86d76d4a3f9c1591aad1e15292008aceb82771" + url: "https://pub.dev" + source: hosted + version: "3.0.6" uuid: dependency: "direct main" description: @@ -666,6 +763,15 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.3" + window_size: + dependency: "direct main" + description: + path: "plugins/window_size" + ref: HEAD + resolved-ref: "6c66ad23ee79749f30a8eece542cf54eaf157ed8" + url: "https://github.com/google/flutter-desktop-embedding" + source: git + version: "0.1.0" xml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 78908b2..b9bc0cf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.14.1+52 +version: 2.0.0+53 environment: sdk: '>=2.18.1 <3.0.0' @@ -36,7 +36,7 @@ dependencies: sdk: flutter intl: ^0.17.0 provider: ^6.0.3 - sqflite: ^2.1.0 + sqflite: ^2.2.5 package_info_plus: ^1.4.3+1 flutter_displaymode: ^0.4.0 dynamic_color: ^1.5.4 @@ -54,6 +54,17 @@ dependencies: markdown: ^7.0.2 html: ^0.15.2 flutter_html: ^3.0.0-alpha.6 + sqlite3_flutter_libs: ^0.5.13 + sqflite_common_ffi: ^2.2.3 + window_size: + git: + url: https://github.com/google/flutter-desktop-embedding + path: plugins/window_size + flutter_split_view: + git: + url: https://github.com/JGeek00/flutter_split_view + ref: master-alt + url_launcher: ^6.1.10 dev_dependencies: flutter_test: @@ -65,7 +76,7 @@ dev_dependencies: # package. See that file for information about deactivating specific lint # rules and activating additional ones. flutter_lints: ^2.0.0 - flutter_launcher_icons: ^0.10.0 + flutter_launcher_icons: ^0.13.1 flutter_native_splash: ^2.2.10+1 # For information on the generic Dart part of this file, see the @@ -79,6 +90,12 @@ flutter_icons: image_path_android: "assets/icon/icon-android.png" adaptive_icon_foreground: "assets/icon/icon-android-adap.png" adaptive_icon_background: "#ffffff" + macos: + generate: true + image_path: "assets/icon/icon-macos.png" + windows: + generate: true + image_path: "assets/icon/icon-circle.png" flutter_native_splash: android12: true @@ -102,6 +119,8 @@ flutter: - assets/icon/icon-android-adap.png - assets/icon/icon-android.png - assets/icon/icon-splash.png + - assets/icon/icon-macos.png + - assets/icon/icon-circle.png - assets/icon/icon1024-white-center.png - assets/other/get_google_play.png - assets/resources/github.svg diff --git a/windows/.gitignore b/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt new file mode 100644 index 0000000..f992491 --- /dev/null +++ b/windows/CMakeLists.txt @@ -0,0 +1,101 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(adguard_home_manager LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "adguard_home_manager") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..930d207 --- /dev/null +++ b/windows/flutter/CMakeLists.txt @@ -0,0 +1,104 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..3d0e948 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,23 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + DynamicColorPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); + Sqlite3FlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); + WindowSizePluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowSizePlugin")); +} diff --git a/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..8ca2ff7 --- /dev/null +++ b/windows/flutter/generated_plugins.cmake @@ -0,0 +1,27 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + dynamic_color + sqlite3_flutter_libs + url_launcher_windows + window_size +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/windows/innosetup_installer_builder.iss b/windows/innosetup_installer_builder.iss new file mode 100644 index 0000000..4887491 --- /dev/null +++ b/windows/innosetup_installer_builder.iss @@ -0,0 +1,56 @@ +; Script generated by the Inno Setup Script Wizard. +; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! + +#define MyAppName "AdGuard Home Manager" +#define MyAppVersion "2.0.0" +#define MyAppPublisher "JGeek00" +#define MyAppURL "https://github.com/JGeek00/adguard-home-manager" +#define MyAppExeName "adguard_home_manager.exe" + +[Setup] +; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. +; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) +AppId={{41521D70-0D21-4A9B-8CD4-2760753054C9} +AppName={#MyAppName} +AppVersion={#MyAppVersion} +;AppVerName={#MyAppName} {#MyAppVersion} +AppPublisher={#MyAppPublisher} +AppPublisherURL={#MyAppURL} +AppSupportURL={#MyAppURL} +AppUpdatesURL={#MyAppURL} +DefaultDirName={autopf}\{#MyAppName} +DisableProgramGroupPage=yes +; Remove the following line to run in administrative install mode (install for all users.) +PrivilegesRequired=lowest +OutputDir=C:\Users\Juan\adguard_home_manager\build\windows +OutputBaseFilename=aghm_installer +SetupIconFile=C:\Users\Juan\adguard_home_manager\assets\icon\icon-circle.ico +Compression=lzma +SolidCompression=yes +WizardStyle=modern + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" +Name: "spanish"; MessagesFile: "compiler:Languages\Spanish.isl" + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked + +[Files] +Source: "C:\Users\Juan\adguard_home_manager\build\windows\runner\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion +Source: "C:\Users\Juan\adguard_home_manager\build\windows\runner\Release\dynamic_color_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "C:\Users\Juan\adguard_home_manager\build\windows\runner\Release\flutter_windows.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "C:\Users\Juan\adguard_home_manager\build\windows\runner\Release\sqlite3.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "C:\Users\Juan\adguard_home_manager\build\windows\runner\Release\sqlite3_flutter_libs_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "C:\Users\Juan\adguard_home_manager\build\windows\runner\Release\url_launcher_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "C:\Users\Juan\adguard_home_manager\build\windows\runner\Release\window_size_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "C:\Users\Juan\adguard_home_manager\build\windows\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs +; NOTE: Don't use "Flags: ignoreversion" on any shared system files + +[Icons] +Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" +Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon + +[Run] +Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent + diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc new file mode 100644 index 0000000..a56b12b --- /dev/null +++ b/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "JGeek00" "\0" + VALUE "FileDescription", "AdGuard Home Manager" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "AdGuard Home Manager" "\0" + VALUE "LegalCopyright", "Copyright (C) 2023 JGeek00. All rights reserved." "\0" + VALUE "OriginalFilename", "adguard_home_manager.exe" "\0" + VALUE "ProductName", "AdGuard Home Manager" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..b25e363 --- /dev/null +++ b/windows/runner/flutter_window.cpp @@ -0,0 +1,66 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/windows/runner/flutter_window.h b/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp new file mode 100644 index 0000000..1c992e7 --- /dev/null +++ b/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"AdGuard Home Manager", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/windows/runner/resource.h b/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..6d25264 Binary files /dev/null and b/windows/runner/resources/app_icon.ico differ diff --git a/windows/runner/runner.exe.manifest b/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..a42ea76 --- /dev/null +++ b/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp new file mode 100644 index 0000000..f5bf9fa --- /dev/null +++ b/windows/runner/utils.cpp @@ -0,0 +1,64 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/windows/runner/utils.h b/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp new file mode 100644 index 0000000..041a385 --- /dev/null +++ b/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h new file mode 100644 index 0000000..c86632d --- /dev/null +++ b/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_