Merge branch 'desktop-ui'

This commit is contained in:
Juan Gilsanz Polo 2023-05-04 03:30:22 +02:00
commit b4344ffb96
154 changed files with 11280 additions and 4128 deletions

View file

@ -4,7 +4,7 @@
# This file should be version controlled. # This file should be version controlled.
version: version:
revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 revision: f72efea43c3013323d1b95cff571f3c1caa37583
channel: stable channel: stable
project_type: app project_type: app
@ -13,26 +13,11 @@ project_type: app
migration: migration:
platforms: platforms:
- platform: root - platform: root
create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 create_revision: f72efea43c3013323d1b95cff571f3c1caa37583
base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 base_revision: f72efea43c3013323d1b95cff571f3c1caa37583
- platform: android
create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
- platform: ios
create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
- platform: linux - platform: linux
create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 create_revision: f72efea43c3013323d1b95cff571f3c1caa37583
base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 base_revision: f72efea43c3013323d1b95cff571f3c1caa37583
- platform: macos
create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
- platform: web
create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
- platform: windows
create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
# User provided section # User provided section

BIN
assets/icon/icon-circle.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
assets/icon/icon-circle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
assets/icon/icon-macos.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 KiB

1
debian/compile_deb.txt vendored Normal file
View file

@ -0,0 +1 @@
https://pub.dev/packages/flutter_to_debian

14
debian/debian.yaml vendored Normal file
View file

@ -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

View file

@ -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;

BIN
debian/gui/adguard-home-manager.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View file

@ -365,7 +365,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.example.adguardHomeManager; PRODUCT_BUNDLE_IDENTIFIER = com.jgeek00.adguard_home_manager;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -494,7 +494,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.example.adguardHomeManager; PRODUCT_BUNDLE_IDENTIFIER = com.jgeek00.adguard_home_manager;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -517,7 +517,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.example.adguardHomeManager; PRODUCT_BUNDLE_IDENTIFIER = com.jgeek00.adguard_home_manager;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;

View file

@ -1,6 +1,7 @@
// ignore_for_file: use_build_context_synchronously, depend_on_referenced_packages // ignore_for_file: use_build_context_synchronously, depend_on_referenced_packages
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:animations/animations.dart'; import 'package:animations/animations.dart';
@ -10,7 +11,9 @@ import 'package:store_checker/store_checker.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:adguard_home_manager/widgets/bottom_nav_bar.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/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/providers/app_config_provider.dart';
import 'package:adguard_home_manager/models/github_release.dart'; import 'package:adguard_home_manager/models/github_release.dart';
@ -53,11 +56,23 @@ class _BaseState extends State<Base> with WidgetsBindingObserver {
} }
Future<GitHubRelease?> checkInstallationSource() async { Future<GitHubRelease?> checkInstallationSource() async {
Source installationSource = await StoreChecker.getSource; final result = await checkAppUpdatesGitHub();
if (installationSource != Source.IS_INSTALLED_FROM_PLAY_STORE) { if (result['result'] == 'success') {
final result = await checkAppUpdatesGitHub(); final update = updateExists(widget.appConfigProvider.getAppInfo!.version, result['body'].tagName);
if (result['result'] == 'success') { if (update == true) {
if (updateExists(widget.appConfigProvider.getAppInfo!.version, result['body'].tagName)) { 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']; return result['body'];
} }
} }
@ -107,38 +122,51 @@ class _BaseState extends State<Base> with WidgetsBindingObserver {
final serversProvider = Provider.of<ServersProvider>(context); final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context); final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
List<AppScreen> screens = serversProvider.selectedServer != null List<AppScreen> screens = serversProvider.selectedServer != null
? screensServerConnected ? screensServerConnected
: screensSelectServer; : screensSelectServer;
return AnnotatedRegion<SystemUiOverlayStyle>( return CustomMenuBar(
value: SystemUiOverlayStyle( child: AnnotatedRegion<SystemUiOverlayStyle>(
statusBarColor: Colors.transparent, value: SystemUiOverlayStyle(
statusBarBrightness: Theme.of(context).brightness == Brightness.light statusBarColor: Colors.transparent,
? Brightness.light statusBarBrightness: Theme.of(context).brightness == Brightness.light
: Brightness.dark, ? Brightness.light
statusBarIconBrightness: Theme.of(context).brightness == Brightness.light : Brightness.dark,
? Brightness.dark statusBarIconBrightness: Theme.of(context).brightness == Brightness.light
: Brightness.light, ? Brightness.dark
systemNavigationBarColor: Theme.of(context).scaffoldBackgroundColor, : Brightness.light,
systemNavigationBarIconBrightness: Theme.of(context).brightness == Brightness.light systemNavigationBarColor: Theme.of(context).scaffoldBackgroundColor,
? Brightness.dark systemNavigationBarIconBrightness: Theme.of(context).brightness == Brightness.light
: 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,
), ),
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,
)
),
); );
} }
} }

View file

@ -1,17 +1,30 @@
import 'package:flutter_web_browser/flutter_web_browser.dart'; import 'dart:io';
void openUrl(String url) { import 'package:flutter_web_browser/flutter_web_browser.dart';
FlutterWebBrowser.openWebPage( import 'package:url_launcher/url_launcher.dart';
url: url,
customTabsOptions: const CustomTabsOptions( void openUrl(String url) async {
instantAppsEnabled: true, if (Platform.isAndroid || Platform.isIOS) {
showTitle: true, FlutterWebBrowser.openWebPage(
urlBarHidingEnabled: false, url: url,
), customTabsOptions: const CustomTabsOptions(
safariVCOptions: const SafariViewControllerOptions( instantAppsEnabled: true,
barCollapsingEnabled: true, showTitle: true,
dismissButtonStyle: SafariViewControllerDismissButtonStyle.close, urlBarHidingEnabled: false,
modalPresentationCapturesStatusBarAppearance: true, ),
) 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';
}
}
}

View file

@ -45,10 +45,10 @@
"save": "Save", "save": "Save",
"serverStatus": "Server status", "serverStatus": "Server status",
"connectionNotUpdated": "Connection not updated", "connectionNotUpdated": "Connection not updated",
"ruleFilteringWidget": "Rule\nfiltering", "ruleFilteringWidget": "Rule filtering",
"safeBrowsingWidget": "Safe\nbrowsing", "safeBrowsingWidget": "Safe browsing",
"parentalFilteringWidget": "Parental\nfiltering", "parentalFilteringWidget": "Parental filtering",
"safeSearchWidget": "Safe\nsearch", "safeSearchWidget": "Safe search",
"ruleFiltering": "Rule filtering", "ruleFiltering": "Rule filtering",
"safeBrowsing": "Safe browsing", "safeBrowsing": "Safe browsing",
"parentalFiltering": "Parental filtering", "parentalFiltering": "Parental filtering",
@ -606,5 +606,11 @@
"remainingTime": "Remaining time", "remainingTime": "Remaining time",
"safeSearchSettings": "Safe search settings", "safeSearchSettings": "Safe search settings",
"loadingSafeSearchSettings": "Loading 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"
} }

View file

@ -45,10 +45,10 @@
"save": "Guardar", "save": "Guardar",
"connectionNotUpdated": "Conexión no actualizada", "connectionNotUpdated": "Conexión no actualizada",
"serverStatus": "Estado del servidor", "serverStatus": "Estado del servidor",
"ruleFilteringWidget": "Bloqueo por\nfiltros", "ruleFilteringWidget": "Bloqueo por filtros",
"safeBrowsingWidget": "Navegación\nsegura", "safeBrowsingWidget": "Navegación segura",
"parentalFilteringWidget": "Control\nparental", "parentalFilteringWidget": "Control parental",
"safeSearchWidget": "Búsqueda\nsegura", "safeSearchWidget": "Búsqueda segura",
"ruleFiltering": "Bloqueo por filtros", "ruleFiltering": "Bloqueo por filtros",
"safeBrowsing": "Navegación segura", "safeBrowsing": "Navegación segura",
"parentalFiltering": "Control parental", "parentalFiltering": "Control parental",
@ -606,5 +606,11 @@
"remainingTime": "Tiempo restante", "remainingTime": "Tiempo restante",
"safeSearchSettings": "Configuración de búsqueda segura", "safeSearchSettings": "Configuración de búsqueda segura",
"loadingSafeSearchSettings": "Cargando 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"
} }

View file

@ -7,25 +7,33 @@ import 'package:provider/provider.dart';
import 'package:dynamic_color/dynamic_color.dart'; import 'package:dynamic_color/dynamic_color.dart';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:package_info_plus/package_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_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/base.dart'; import 'package:adguard_home_manager/base.dart';
import 'package:adguard_home_manager/classes/http_override.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/constants/colors.dart';
import 'package:adguard_home_manager/providers/logs_provider.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/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/config/theme.dart';
import 'package:adguard_home_manager/providers/servers_provider.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); 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(); AppConfigProvider appConfigProvider = AppConfigProvider();
ServersProvider serversProvider = ServersProvider(); ServersProvider serversProvider = ServersProvider();

View file

@ -4,6 +4,7 @@ import 'package:flutter/scheduler.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:sqflite/sqlite_api.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/functions/conversions.dart';
import 'package:adguard_home_manager/models/app_log.dart'; import 'package:adguard_home_manager/models/app_log.dart';
@ -16,6 +17,8 @@ class AppConfigProvider with ChangeNotifier {
int _selectedScreen = 0; int _selectedScreen = 0;
int? _selectedSettingsScreen;
bool _showingSnackbar = false; bool _showingSnackbar = false;
int _selectedTheme = 0; int _selectedTheme = 0;
@ -118,6 +121,10 @@ class AppConfigProvider with ChangeNotifier {
return _doNotRememberVersion; return _doNotRememberVersion;
} }
int? get selectedSettingsScreen {
return _selectedSettingsScreen;
}
void setDbInstance(Database db) { void setDbInstance(Database db) {
_dbInstance = db; _dbInstance = db;
} }
@ -159,8 +166,19 @@ class AppConfigProvider with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
void setSelectedSettingsScreen({required int? screen, bool? notify}) {
_selectedSettingsScreen = screen;
if (notify == true) {
notifyListeners();
}
}
Future<bool> setOverrideSslCheck(bool status) async { Future<bool> 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) { if (updated == true) {
_overrideSslCheck = status == true ? 1 : 0; _overrideSslCheck = status == true ? 1 : 0;
notifyListeners(); notifyListeners();
@ -172,7 +190,11 @@ class AppConfigProvider with ChangeNotifier {
} }
Future<bool> setHideZeroValues(bool status) async { Future<bool> 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) { if (updated == true) {
_hideZeroValues = status == true ? 1 : 0; _hideZeroValues = status == true ? 1 : 0;
notifyListeners(); notifyListeners();
@ -184,7 +206,11 @@ class AppConfigProvider with ChangeNotifier {
} }
Future<bool> setShowNameTimeLogs(bool status) async { Future<bool> 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) { if (updated == true) {
_showNameTimeLogs = status == true ? 1 : 0; _showNameTimeLogs = status == true ? 1 : 0;
notifyListeners(); notifyListeners();
@ -196,7 +222,11 @@ class AppConfigProvider with ChangeNotifier {
} }
Future<bool> setSelectedTheme(int value) async { Future<bool> setSelectedTheme(int value) async {
final updated = await _updateThemeDb(value); final updated = await updateConfigQuery(
db: _dbInstance!,
column: 'theme',
value: value
);
if (updated == true) { if (updated == true) {
_selectedTheme = value; _selectedTheme = value;
notifyListeners(); notifyListeners();
@ -208,7 +238,11 @@ class AppConfigProvider with ChangeNotifier {
} }
Future<bool> setUseDynamicColor(bool value) async { Future<bool> 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) { if (updated == true) {
_useDynamicColor = value; _useDynamicColor = value;
notifyListeners(); notifyListeners();
@ -220,7 +254,11 @@ class AppConfigProvider with ChangeNotifier {
} }
Future<bool> setUseThemeColorForStatus(bool value) async { Future<bool> 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) { if (updated == true) {
_useThemeColorForStatus = value; _useThemeColorForStatus = value;
notifyListeners(); notifyListeners();
@ -232,7 +270,11 @@ class AppConfigProvider with ChangeNotifier {
} }
Future<bool> setStaticColor(int value) async { Future<bool> setStaticColor(int value) async {
final updated = await _updateStaticColorDb(value); final updated = await updateConfigQuery(
db: _dbInstance!,
column: 'staticColor',
value: value
);
if (updated == true) { if (updated == true) {
_staticColor = value; _staticColor = value;
notifyListeners(); notifyListeners();
@ -244,109 +286,12 @@ class AppConfigProvider with ChangeNotifier {
} }
Future<bool> setDoNotRememberVersion(String value) async { Future<bool> setDoNotRememberVersion(String value) async {
try { final updated = await updateConfigQuery(
return await _dbInstance!.transaction((txn) async { db: _dbInstance!,
await txn.rawUpdate( column: 'doNotRememberVersion',
'UPDATE appConfig SET doNotRememberVersion = "$value"', value: value
); );
_doNotRememberVersion = value; return updated;
notifyListeners();
return true;
});
} catch (e) {
return false;
}
}
Future<bool> _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<bool> _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<bool> _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<bool> _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<bool> _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<bool> _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<bool> _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;
}
} }
void saveFromDb(Database dbInstance, Map<String, dynamic> dbData) { void saveFromDb(Database dbInstance, Map<String, dynamic> dbData) {

View file

@ -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/rewrite_rules.dart';
import 'package:adguard_home_manager/models/filtering_status.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/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/blocked_services.dart';
import 'package:adguard_home_manager/models/clients.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_status.dart';
import 'package:adguard_home_manager/models/server.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/services/http_requests.dart';
import 'package:adguard_home_manager/functions/time_server_disabled.dart'; import 'package:adguard_home_manager/functions/time_server_disabled.dart';
import 'package:adguard_home_manager/functions/conversions.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/functions/compare_versions.dart';
import 'package:adguard_home_manager/constants/enums.dart'; import 'package:adguard_home_manager/constants/enums.dart';
@ -33,6 +34,9 @@ class ServersProvider with ChangeNotifier {
loadStatus: LoadStatus.loading, loadStatus: LoadStatus.loading,
data: null data: null
); );
String? _searchTermClients;
List<AutoClient> _filteredActiveClients = [];
List<Client> _filteredAddedClients = [];
final Filtering _filtering = Filtering( final Filtering _filtering = Filtering(
loadStatus: LoadStatus.loading, loadStatus: LoadStatus.loading,
@ -86,6 +90,18 @@ class ServersProvider with ChangeNotifier {
return _clients; return _clients;
} }
String? get searchTermClients {
return _searchTermClients;
}
List<AutoClient> get filteredActiveClients {
return _filteredActiveClients;
}
List<Client> get filteredAddedClients {
return _filteredAddedClients;
}
FilteringStatus? get filteringStatus { FilteringStatus? get filteringStatus {
return _filteringStatus; return _filteringStatus;
} }
@ -147,6 +163,43 @@ class ServersProvider with ChangeNotifier {
void setClientsData(ClientsData data) { void setClientsData(ClientsData data) {
_clients.data = 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(); notifyListeners();
} }
@ -249,7 +302,7 @@ class ServersProvider with ChangeNotifier {
} }
Future<dynamic> createServer(Server server) async { Future<dynamic> createServer(Server server) async {
final saved = await saveServerIntoDb(server); final saved = await saveServerQuery(_dbInstance!, server);
if (saved == null) { if (saved == null) {
if (server.defaultServer == true) { if (server.defaultServer == true) {
final defaultServer = await setDefaultServer(server); final defaultServer = await setDefaultServer(server);
@ -274,7 +327,7 @@ class ServersProvider with ChangeNotifier {
} }
Future<dynamic> setDefaultServer(Server server) async { Future<dynamic> setDefaultServer(Server server) async {
final updated = await setDefaultServerDb(server.id); final updated = await setDefaultServerQuery(_dbInstance!, server.id);
if (updated == null) { if (updated == null) {
List<Server> newServers = _serversList.map((s) { List<Server> newServers = _serversList.map((s) {
if (s.id == server.id) { if (s.id == server.id) {
@ -296,7 +349,7 @@ class ServersProvider with ChangeNotifier {
} }
Future<dynamic> editServer(Server server) async { Future<dynamic> editServer(Server server) async {
final result = await editServerDb(server); final result = await editServerQuery(_dbInstance!, server);
if (result == null) { if (result == null) {
List<Server> newServers = _serversList.map((s) { List<Server> newServers = _serversList.map((s) {
if (s.id == server.id) { if (s.id == server.id) {
@ -316,7 +369,7 @@ class ServersProvider with ChangeNotifier {
} }
Future<bool> removeServer(Server server) async { Future<bool> removeServer(Server server) async {
final result = await removeFromDb(server.id); final result = await removeServerQuery(_dbInstance!, server.id);
if (result == true) { if (result == true) {
_selectedServer = null; _selectedServer = null;
List<Server> newServers = _serversList.where((s) => s.id != server.id).toList(); List<Server> newServers = _serversList.where((s) => s.id != server.id).toList();
@ -472,63 +525,6 @@ class ServersProvider with ChangeNotifier {
} }
} }
Future<dynamic> 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<dynamic> 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<bool> 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<dynamic> 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 { void checkServerUpdatesAvailable(Server server) async {
setUpdateAvailableLoadStatus(LoadStatus.loading, true); setUpdateAvailableLoadStatus(LoadStatus.loading, true);
final result = await checkServerUpdates(server: server); final result = await checkServerUpdates(server: server);

View file

@ -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),
);
}
}
}

View file

@ -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<AddedClientTile> createState() => _AddedClientTileState();
}
class _AddedClientTileState extends State<AddedClientTile> {
bool hover = false;
@override
Widget build(BuildContext context) {
final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(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,
);
}
}
}

View file

@ -1,12 +1,16 @@
// ignore_for_file: use_build_context_synchronously // ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:animations/animations.dart'; import 'package:animations/animations.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_split_view/flutter_split_view.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.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/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/remove_client_modal.dart';
import 'package:adguard_home_manager/screens/clients/fab.dart'; import 'package:adguard_home_manager/screens/clients/fab.dart';
import 'package:adguard_home_manager/screens/clients/options_modal.dart'; import 'package:adguard_home_manager/screens/clients/options_modal.dart';
@ -27,13 +31,19 @@ class AddedList extends StatefulWidget {
final LoadStatus loadStatus; final LoadStatus loadStatus;
final List<Client> data; final List<Client> data;
final Future Function() fetchClients; final Future Function() fetchClients;
final void Function(Client) onClientSelected;
final Client? selectedClient;
final bool splitView;
const AddedList({ const AddedList({
Key? key, Key? key,
required this.scrollController, required this.scrollController,
required this.loadStatus, required this.loadStatus,
required this.data, required this.data,
required this.fetchClients required this.fetchClients,
required this.onClientSelected,
this.selectedClient,
required this.splitView
}) : super(key: key); }) : super(key: key);
@override @override
@ -69,6 +79,8 @@ class _AddedListState extends State<AddedList> {
final serversProvider = Provider.of<ServersProvider>(context); final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context); final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
void confirmEditClient(Client client) async { void confirmEditClient(Client client) async {
ProcessModal processModal = ProcessModal(context: context); ProcessModal processModal = ProcessModal(context: context);
processModal.open(AppLocalizations.of(context)!.addingClient); processModal.open(AppLocalizations.of(context)!.addingClient);
@ -130,6 +142,10 @@ class _AddedListState extends State<AddedList> {
clientsData.clients = clientsData.clients.where((c) => c.name != client.name).toList(); clientsData.clients = clientsData.clients.where((c) => c.name != client.name).toList();
serversProvider.setClientsData(clientsData); serversProvider.setClientsData(clientsData);
if (widget.splitView == true) {
SplitView.of(context).popUntil(0);
}
showSnacbkar( showSnacbkar(
context: context, context: context,
appConfigProvider: appConfigProvider, appConfigProvider: appConfigProvider,
@ -150,15 +166,31 @@ class _AddedListState extends State<AddedList> {
} }
void openClientModal(Client client) { void openClientModal(Client client) {
Navigator.push(context, MaterialPageRoute( if (width > 900 || !(Platform.isAndroid | Platform.isIOS)) {
fullscreenDialog: true, showDialog(
builder: (BuildContext context) => ClientScreen( barrierDismissible: false,
onConfirm: confirmEditClient, context: context,
serverVersion: serversProvider.serverStatus.data!.serverVersion, builder: (BuildContext context) => ClientScreen(
onDelete: deleteClient, onConfirm: confirmEditClient,
client: client, 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) { void openDeleteModal(Client client) {
@ -181,9 +213,12 @@ class _AddedListState extends State<AddedList> {
} }
return CustomTabContentList( return CustomTabContentList(
noSliver: !(Platform.isAndroid || Platform.isIOS),
listPadding: widget.splitView == true
? const EdgeInsets.only(top: 8)
: null,
loadingGenerator: () => SizedBox( loadingGenerator: () => SizedBox(
width: double.maxFinite, width: double.maxFinite,
height: MediaQuery.of(context).size.height-171,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
@ -202,109 +237,28 @@ class _AddedListState extends State<AddedList> {
), ),
), ),
itemsCount: widget.data.length, itemsCount: widget.data.length,
contentWidget: (index) => ListTile( contentWidget: (index) => AddedClientTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), selectedClient: widget.selectedClient,
isThreeLine: true, client: widget.data[index],
onLongPress: () => openOptionsModal(widget.data[index]), onTap: widget.onClientSelected,
onTap: () => openClientModal(widget.data[index]), onLongPress: openOptionsModal,
title: Padding( onEdit: openClientModal,
padding: const EdgeInsets.only(bottom: 5), splitView: widget.splitView,
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,
)
],
)
],
),
),
noData: SizedBox( noData: SizedBox(
width: double.maxFinite, width: double.maxFinite,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Padding(
AppLocalizations.of(context)!.noClientsList, padding: const EdgeInsets.symmetric(horizontal: 16),
textAlign: TextAlign.center, child: Text(
style: TextStyle( AppLocalizations.of(context)!.noClientsList,
fontSize: 24, textAlign: TextAlign.center,
color: Theme.of(context).colorScheme.onSurfaceVariant, style: TextStyle(
fontSize: 24,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
), ),
), ),
const SizedBox(height: 30), const SizedBox(height: 30),
@ -318,7 +272,6 @@ class _AddedListState extends State<AddedList> {
), ),
errorGenerator: () => SizedBox( errorGenerator: () => SizedBox(
width: double.maxFinite, width: double.maxFinite,
height: MediaQuery.of(context).size.height-171,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,

View file

@ -19,6 +19,7 @@ class ClientScreen extends StatefulWidget {
final String serverVersion; final String serverVersion;
final void Function(Client) onConfirm; final void Function(Client) onConfirm;
final void Function(Client)? onDelete; final void Function(Client)? onDelete;
final bool dialog;
const ClientScreen({ const ClientScreen({
Key? key, Key? key,
@ -26,6 +27,7 @@ class ClientScreen extends StatefulWidget {
required this.serverVersion, required this.serverVersion,
required this.onConfirm, required this.onConfirm,
this.onDelete, this.onDelete,
required this.dialog
}) : super(key: key); }) : super(key: key);
@override @override
@ -300,51 +302,13 @@ class _ClientScreenState extends State<ClientScreen> {
), ),
); );
} }
return Scaffold( Widget content(bool withPaddingTop) {
appBar: AppBar( return ListView(
leading: IconButton( padding: const EdgeInsets.only(top: 0),
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(
children: [ children: [
const SizedBox(height: 24), if (withPaddingTop == true) const SizedBox(height: 24),
if (withPaddingTop == false) const SizedBox(height: 6),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 24), padding: const EdgeInsets.symmetric(horizontal: 24),
child: TextFormField( child: TextFormField(
@ -693,10 +657,7 @@ class _ClientScreenState extends State<ClientScreen> {
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
SizedBox( Expanded(
width: editMode == true
? MediaQuery.of(context).size.width - 108
: MediaQuery.of(context).size.width - 40,
child: TextFormField( child: TextFormField(
enabled: editMode, enabled: editMode,
controller: controller['controller'], controller: controller['controller'],
@ -751,7 +712,125 @@ class _ClientScreenState extends State<ClientScreen> {
), ),
const SizedBox(height: 20) 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)
);
}
} }
} }

View file

@ -1,9 +1,14 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_split_view/flutter_split_view.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.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/clients_list.dart';
import 'package:adguard_home_manager/screens/clients/search_clients.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/screens/clients/added_list.dart';
import 'package:adguard_home_manager/models/app_log.dart'; import 'package:adguard_home_manager/models/app_log.dart';
@ -56,6 +61,9 @@ class _ClientsWidgetState extends State<ClientsWidget> with TickerProviderStateM
late TabController tabController; late TabController tabController;
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
bool searchMode = false;
final TextEditingController searchController = TextEditingController();
Future fetchClients() async { Future fetchClients() async {
widget.setLoadingStatus(LoadStatus.loading, false); widget.setLoadingStatus(LoadStatus.loading, false);
final result = await getClients(widget.server); final result = await getClients(widget.server);
@ -90,83 +98,207 @@ class _ClientsWidgetState extends State<ClientsWidget> with TickerProviderStateM
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final serversProvider = Provider.of<ServersProvider>(context); final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
return DefaultTabController( PreferredSizeWidget tabBar() {
length: 2, return TabBar(
child: NestedScrollView( controller: tabController,
controller: scrollController, unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant,
headerSliverBuilder: ((context, innerBoxIsScrolled) { tabs: [
return [ Tab(
SliverOverlapAbsorber( child: Row(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), mainAxisSize: MainAxisSize.min,
sliver: SliverAppBar( children: [
title: Text(AppLocalizations.of(context)!.clients), const Icon(Icons.devices),
pinned: true, const SizedBox(width: 8),
floating: true, Text(AppLocalizations.of(context)!.activeClients)
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)
)
)
), ),
child: TabBarView( Tab(
controller: tabController, child: Row(
children: [ mainAxisSize: MainAxisSize.min,
ClientsList( children: [
scrollController: scrollController, const Icon(Icons.add_rounded),
loadStatus: serversProvider.clients.loadStatus, const SizedBox(width: 8),
data: serversProvider.clients.loadStatus == LoadStatus.loaded Text(AppLocalizations.of(context)!.added)
? serversProvider.clients.data!.autoClientsData : [], ],
fetchClients: fetchClients, ),
),
]
);
}
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)
)
);
}
}
} }
} }

View file

@ -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<ClientsDesktopView> createState() => _ClientsDesktopViewState();
}
class _ClientsDesktopViewState extends State<ClientsDesktopView> 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<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(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)
)
);
}
}
}

View file

@ -1,36 +1,44 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.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/widgets/tab_content_list.dart';
import 'package:adguard_home_manager/models/clients.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/constants/enums.dart';
import 'package:adguard_home_manager/providers/logs_provider.dart';
class ClientsList extends StatelessWidget { class ClientsList extends StatelessWidget {
final ScrollController scrollController; final ScrollController scrollController;
final LoadStatus loadStatus; final LoadStatus loadStatus;
final List<AutoClient> data; final List<AutoClient> data;
final Future Function() fetchClients; final Future Function() fetchClients;
final void Function(AutoClient) onClientSelected;
final AutoClient? selectedClient;
final bool splitView;
final bool sliver;
const ClientsList({ const ClientsList({
Key? key, Key? key,
required this.scrollController, required this.scrollController,
required this.loadStatus, required this.loadStatus,
required this.data, required this.data,
required this.fetchClients required this.fetchClients,
required this.onClientSelected,
this.selectedClient,
required this.splitView,
required this.sliver
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final logsProvider = Provider.of<LogsProvider>(context);
return CustomTabContentList( return CustomTabContentList(
listPadding: splitView == true
? const EdgeInsets.only(top: 8)
: null,
noSliver: !sliver,
loadingGenerator: () => SizedBox( loadingGenerator: () => SizedBox(
width: double.maxFinite, width: double.maxFinite,
height: MediaQuery.of(context).size.height-171, height: MediaQuery.of(context).size.height-171,
@ -52,32 +60,12 @@ class ClientsList extends StatelessWidget {
), ),
), ),
itemsCount: data.length, itemsCount: data.length,
contentWidget: (index) => CustomListTile( contentWidget: (index) => ActiveClientTile(
title: data[index].name != '' client: data[index],
? data[index].name! onTap: onClientSelected,
: data[index].ip, splitView: splitView,
subtitle: data[index].name != '' selectedClient: selectedClient,
? 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);
},
),
noData: SizedBox( noData: SizedBox(
width: double.maxFinite, width: double.maxFinite,
child: Column( child: Column(

View file

@ -1,5 +1,7 @@
// ignore_for_file: use_build_context_synchronously // ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -23,6 +25,8 @@ class ClientsFab extends StatelessWidget {
final serversProvider = Provider.of<ServersProvider>(context); final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context); final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
void confirmAddClient(Client client) async { void confirmAddClient(Client client) async {
ProcessModal processModal = ProcessModal(context: context); ProcessModal processModal = ProcessModal(context: context);
processModal.open(AppLocalizations.of(context)!.addingClient); processModal.open(AppLocalizations.of(context)!.addingClient);
@ -65,13 +69,27 @@ class ClientsFab extends StatelessWidget {
} }
void openAddClient() { void openAddClient() {
Navigator.push(context, MaterialPageRoute( if (width > 900 || !(Platform.isAndroid | Platform.isIOS)) {
fullscreenDialog: true, showDialog(
builder: (BuildContext context) => ClientScreen( barrierDismissible: false,
onConfirm: confirmAddClient, context: context,
serverVersion: serversProvider.serverStatus.data!.serverVersion, 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( return FloatingActionButton(

View file

@ -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<LogsListClient> createState() => _LogsListClientState();
}
class _LogsListClientState extends State<LogsListClient> {
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(),
);
}
}

View file

@ -64,109 +64,114 @@ class _SafeSearchModalState extends State<SafeSearchModal> {
) )
], ],
), ),
content: Wrap( content: ConstrainedBox(
children: [ constraints: const BoxConstraints(
Padding( maxWidth: 400
padding: const EdgeInsets.symmetric(horizontal: 16), ),
child: Material( child: Wrap(
color: Theme.of(context).colorScheme.primary.withOpacity(0.1), children: [
borderRadius: BorderRadius.circular(28), Padding(
child: InkWell( padding: const EdgeInsets.symmetric(horizontal: 16),
onTap: widget.disabled == true child: Material(
? null color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
: () => setState(() => generalEnabled = !generalEnabled),
borderRadius: BorderRadius.circular(28), borderRadius: BorderRadius.circular(28),
child: Padding( child: InkWell(
padding: const EdgeInsets.symmetric( onTap: widget.disabled == true
horizontal: 20, ? null
vertical: 5 : () => setState(() => generalEnabled = !generalEnabled),
), borderRadius: BorderRadius.circular(28),
child: Row( child: Padding(
mainAxisAlignment: MainAxisAlignment.spaceBetween, padding: const EdgeInsets.symmetric(
children: [ horizontal: 20,
Text( vertical: 5
AppLocalizations.of(context)!.enable, ),
style: TextStyle( child: Row(
fontSize: 16, mainAxisAlignment: MainAxisAlignment.spaceBetween,
color: widget.disabled == true children: [
? Colors.grey Text(
: Theme.of(context).colorScheme.onSurface AppLocalizations.of(context)!.enable,
style: TextStyle(
fontSize: 16,
color: widget.disabled == true
? Colors.grey
: Theme.of(context).colorScheme.onSurface
),
), ),
), Switch(
Switch( value: generalEnabled,
value: generalEnabled, onChanged: widget.disabled == true
onChanged: widget.disabled == true ? null
? null : (value) => setState(() => generalEnabled = value),
: (value) => setState(() => generalEnabled = value), )
) ],
], ),
), ),
), ),
), ),
), ),
), const SizedBox(height: 4, width: double.maxFinite),
const SizedBox(height: 4, width: double.maxFinite), CustomCheckboxListTile(
CustomCheckboxListTile( value: bingEnabled,
value: bingEnabled, onChanged: (value) => setState(() => bingEnabled = value),
onChanged: (value) => setState(() => bingEnabled = value), title: "Bing",
title: "Bing", disabled: widget.disabled || !generalEnabled,
disabled: widget.disabled || !generalEnabled, padding: const EdgeInsets.symmetric(
padding: const EdgeInsets.symmetric( horizontal: 36,
horizontal: 36, vertical: 4
vertical: 4 ),
), ),
), CustomCheckboxListTile(
CustomCheckboxListTile( value: duckduckgoEnabled,
value: duckduckgoEnabled, onChanged: (value) => setState(() => duckduckgoEnabled = value),
onChanged: (value) => setState(() => duckduckgoEnabled = value), title: "DuckDuckGo",
title: "DuckDuckGo", disabled: widget.disabled || !generalEnabled,
disabled: widget.disabled || !generalEnabled, padding: const EdgeInsets.symmetric(
padding: const EdgeInsets.symmetric( horizontal: 36,
horizontal: 36, vertical: 4
vertical: 4 ),
), ),
), CustomCheckboxListTile(
CustomCheckboxListTile( value: googleEnabled,
value: googleEnabled, onChanged: (value) => setState(() => googleEnabled = value),
onChanged: (value) => setState(() => googleEnabled = value), title: "Google",
title: "Google", disabled: widget.disabled || !generalEnabled,
disabled: widget.disabled || !generalEnabled, padding: const EdgeInsets.symmetric(
padding: const EdgeInsets.symmetric( horizontal: 36,
horizontal: 36, vertical: 4
vertical: 4 ),
), ),
), CustomCheckboxListTile(
CustomCheckboxListTile( value: pixabayEnabled,
value: pixabayEnabled, onChanged: (value) => setState(() => pixabayEnabled = value),
onChanged: (value) => setState(() => pixabayEnabled = value), title: "Pixabay",
title: "Pixabay", disabled: widget.disabled || !generalEnabled,
disabled: widget.disabled || !generalEnabled, padding: const EdgeInsets.symmetric(
padding: const EdgeInsets.symmetric( horizontal: 36,
horizontal: 36, vertical: 4
vertical: 4 ),
), ),
), CustomCheckboxListTile(
CustomCheckboxListTile( value: yandexEnabled,
value: yandexEnabled, onChanged: (value) => setState(() => yandexEnabled = value),
onChanged: (value) => setState(() => yandexEnabled = value), title: "Yandex",
title: "Yandex", disabled: widget.disabled || !generalEnabled,
disabled: widget.disabled || !generalEnabled, padding: const EdgeInsets.symmetric(
padding: const EdgeInsets.symmetric( horizontal: 36,
horizontal: 36, vertical: 4
vertical: 4 ),
), ),
), CustomCheckboxListTile(
CustomCheckboxListTile( value: youtubeEnabled,
value: youtubeEnabled, onChanged: (value) => setState(() => youtubeEnabled = value),
onChanged: (value) => setState(() => youtubeEnabled = value), title: "YouTube",
title: "YouTube", disabled: widget.disabled || !generalEnabled,
disabled: widget.disabled || !generalEnabled, padding: const EdgeInsets.symmetric(
padding: const EdgeInsets.symmetric( horizontal: 36,
horizontal: 36, vertical: 4
vertical: 4 ),
), ),
), ],
], ),
), ),
actions: [ actions: [
TextButton( TextButton(

View file

@ -1,5 +1,7 @@
// ignore_for_file: use_build_context_synchronously // ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:animations/animations.dart'; import 'package:animations/animations.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -99,6 +101,8 @@ class _SearchClientsWidgetState extends State<SearchClientsWidget> {
final serversProvider = Provider.of<ServersProvider>(context); final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context); final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
void deleteClient(Client client) async { void deleteClient(Client client) async {
ProcessModal processModal = ProcessModal(context: context); ProcessModal processModal = ProcessModal(context: context);
processModal.open(AppLocalizations.of(context)!.removingClient); processModal.open(AppLocalizations.of(context)!.removingClient);
@ -183,15 +187,31 @@ class _SearchClientsWidgetState extends State<SearchClientsWidget> {
} }
void openClientModal(Client client) { void openClientModal(Client client) {
Navigator.push(context, MaterialPageRoute( if (width > 900 || !(Platform.isAndroid | Platform.isIOS)) {
fullscreenDialog: true, showDialog(
builder: (BuildContext context) => ClientScreen( barrierDismissible: false,
onConfirm: confirmEditClient, context: context,
onDelete: deleteClient, builder: (BuildContext context) => ClientScreen(
client: client, onConfirm: confirmEditClient,
serverVersion: serversProvider.serverStatus.data!.serverVersion, 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) { void openDeleteModal(Client client) {

View file

@ -66,6 +66,7 @@ class _ConnectState extends State<Connect> {
controllers: expandableControllerList, controllers: expandableControllerList,
onChange: expandOrContract, onChange: expandOrContract,
scrollController: scrollController, scrollController: scrollController,
breakingWidth: 700,
), ),
AnimatedPositioned( AnimatedPositioned(
duration: const Duration(milliseconds: 100), duration: const Duration(milliseconds: 100),

View file

@ -6,12 +6,27 @@ class FabConnect extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
void openAddServerModal() async { void openAddServerModal() async {
await Future.delayed(const Duration(seconds: 0), (() => { await Future.delayed(const Duration(seconds: 0), (() => {
Navigator.push(context, MaterialPageRoute( if (width > 700) {
fullscreenDialog: true, showDialog(
builder: (BuildContext context) => const AddServerModal() context: context,
)) barrierDismissible: false,
builder: (context) => const AddServerModal(
window: true,
),
)
}
else {
Navigator.push(context, MaterialPageRoute(
fullscreenDialog: true,
builder: (BuildContext context) => const AddServerModal(
window: false,
)
))
}
})); }));
} }

View file

@ -1,5 +1,7 @@
// ignore_for_file: use_build_context_synchronously // ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.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/models/filtering.dart';
import 'package:adguard_home_manager/providers/servers_provider.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart';
class FiltersFab extends StatelessWidget { class AddFiltersButton extends StatelessWidget {
final String type; final String type;
final Widget Function(void Function()) widget;
const FiltersFab({ const AddFiltersButton({
Key? key, Key? key,
required this.type, required this.type,
required this.widget
}) : super(key: key); }) : super(key: key);
@override @override
@ -28,6 +32,8 @@ class FiltersFab extends StatelessWidget {
final serversProvider = Provider.of<ServersProvider>(context); final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context); final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
void confirmAddRule(String rule) async { void confirmAddRule(String rule) async {
ProcessModal processModal = ProcessModal(context: context); ProcessModal processModal = ProcessModal(context: context);
processModal.open(AppLocalizations.of(context)!.addingRule); processModal.open(AppLocalizations.of(context)!.addingRule);
@ -64,14 +70,27 @@ class FiltersFab extends StatelessWidget {
} }
void openAddCustomRule() { void openAddCustomRule() {
Navigator.of(context).push( if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) {
MaterialPageRoute( showDialog(
fullscreenDialog: true, context: context,
builder: (context) => AddCustomRule( 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 { void confirmAddList({required String name, required String url, required String type}) async {
@ -154,22 +173,34 @@ class FiltersFab extends StatelessWidget {
} }
void openAddWhitelistBlacklist() { void openAddWhitelistBlacklist() {
showModalBottomSheet( if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) {
context: context, showDialog(
builder: (ctx) => AddListModal( context: context,
type: type, builder: (ctx) => AddListModal(
onConfirm: confirmAddList, type: type,
), onConfirm: confirmAddList,
isScrollControlled: true, dialog: true,
backgroundColor: Colors.transparent ),
); );
}
else {
showModalBottomSheet(
context: context,
builder: (ctx) => AddListModal(
type: type,
onConfirm: confirmAddList,
dialog: false,
),
isScrollControlled: true,
backgroundColor: Colors.transparent
);
}
} }
return FloatingActionButton( return widget(
onPressed: type == 'blacklist' || type == 'whitelist' type == 'blacklist' || type == 'whitelist'
? () => openAddWhitelistBlacklist() ? () => openAddWhitelistBlacklist()
: () => openAddCustomRule(), : () => openAddCustomRule(),
child: const Icon(Icons.add),
); );
} }
} }

View file

@ -1,17 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.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/constants/urls.dart';
import 'package:adguard_home_manager/providers/app_config_provider.dart';
class AddCustomRule extends StatefulWidget { class AddCustomRule extends StatefulWidget {
final void Function(String) onConfirm; final void Function(String) onConfirm;
final bool dialog;
const AddCustomRule({ const AddCustomRule({
Key? key, Key? key,
required this.onConfirm required this.onConfirm,
required this.dialog
}) : super(key: key); }) : super(key: key);
@override @override
@ -72,293 +72,338 @@ class _AddCustomRuleState extends State<AddCustomRule> {
return rule; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar( List<Widget> content() {
title: Text(AppLocalizations.of(context)!.addCustomRule), return [
actions: [ const SizedBox(height: 24),
IconButton( Row(
onPressed: checkValidValues() == true mainAxisAlignment: MainAxisAlignment.center,
? () { children: [
Navigator.pop(context); Container(
widget.onConfirm(buildRule()); padding: const EdgeInsets.symmetric(
} horizontal: 10,
: null, vertical: 5
icon: const Icon(Icons.check) ),
), decoration: BoxDecoration(
const SizedBox(width: 10) color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
], borderRadius: BorderRadius.circular(30),
), border: Border.all(
body: ListView( color: Theme.of(context).colorScheme.primary
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
),
) )
), ),
], child: Text(
), buildRule(),
const SizedBox(height: 30), textAlign: TextAlign.center,
Padding( style: TextStyle(
padding: const EdgeInsets.symmetric(horizontal: 24), color: Theme.of(context).colorScheme.primary,
child: TextFormField( fontWeight: FontWeight.w500
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, ),
],
),
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( Container(height: 30),
padding: const EdgeInsets.symmetric(horizontal: 24), Padding(
child: SegmentedButton( padding: const EdgeInsets.symmetric(horizontal: 24),
segments: [ child: SegmentedButton(
ButtonSegment( segments: [
value: BlockingPresets.block, ButtonSegment(
label: Text(AppLocalizations.of(context)!.block) value: BlockingPresets.block,
), label: Text(AppLocalizations.of(context)!.block)
ButtonSegment( ),
value: BlockingPresets.unblock, ButtonSegment(
label: Text(AppLocalizations.of(context)!.unblock) value: BlockingPresets.unblock,
), label: Text(AppLocalizations.of(context)!.unblock)
ButtonSegment( ),
value: BlockingPresets.custom, ButtonSegment(
label: Text(AppLocalizations.of(context)!.custom) value: BlockingPresets.custom,
), label: Text(AppLocalizations.of(context)!.custom)
], ),
selected: <BlockingPresets>{preset}, ],
onSelectionChanged: (value) => setState(() => preset = value.first), selected: <BlockingPresets>{preset},
), onSelectionChanged: (value) => setState(() => preset = value.first),
), ),
const SizedBox(height: 20), ),
Material( Container(height: 20),
color: Colors.transparent, Material(
child: InkWell( color: Colors.transparent,
onTap: () => setState(() => addImportant = !addImportant), child: InkWell(
child: Padding( onTap: () => setState(() => addImportant = !addImportant),
padding: const EdgeInsets.symmetric(horizontal: 28), child: Padding(
child: Row( padding: const EdgeInsets.symmetric(horizontal: 28),
mainAxisAlignment: MainAxisAlignment.spaceBetween, child: Row(
children: [ mainAxisAlignment: MainAxisAlignment.spaceBetween,
Padding( children: [
padding: const EdgeInsets.only(left: 10), Padding(
child: Text( padding: const EdgeInsets.only(left: 10),
AppLocalizations.of(context)!.addImportant, child: Text(
style: TextStyle( AppLocalizations.of(context)!.addImportant,
fontSize: 16, style: TextStyle(
color: Theme.of(context).colorScheme.onSurface fontSize: 16,
), color: Theme.of(context).colorScheme.onSurface
), ),
), ),
Switch( ),
value: addImportant, Switch(
onChanged: (value) => setState(() => addImportant = value), value: addImportant,
) onChanged: (value) => setState(() => addImportant = value),
], )
), ],
), ),
), ),
), ),
const SizedBox(height: 20), ),
Padding( Container(height: 20),
padding: const EdgeInsets.symmetric(horizontal: 24), Padding(
child: Card( padding: const EdgeInsets.symmetric(horizontal: 24),
child: Padding( child: Card(
padding: const EdgeInsets.all(20), child: Padding(
child: Column( 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: [ children: [
Row( Row(
children: [ children: [
Icon( IconButton(
Icons.info, onPressed: () => Navigator.pop(context),
color: Theme.of(context).colorScheme.onSurface icon: const Icon(Icons.clear_rounded),
tooltip: AppLocalizations.of(context)!.close,
), ),
const SizedBox(width: 20), const SizedBox(width: 8),
Text( Text(
AppLocalizations.of(context)!.examples, AppLocalizations.of(context)!.addCustomRule,
style: TextStyle( style: const TextStyle(
fontSize: 18, fontSize: 22
color: Theme.of(context).colorScheme.onSurface
), ),
) ),
], ],
), ),
const SizedBox(height: 20), IconButton(
SizedBox( onPressed: checkValidValues() == true
width: double.maxFinite, ? () {
child: Column( Navigator.pop(context);
crossAxisAlignment: CrossAxisAlignment.start, widget.onConfirm(buildRule());
children: [ }
Text( : null,
"||example.org^", icon: const Icon(Icons.check)
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
),
),
],
),
) )
], ],
), ),
), ),
), Flexible(
), child: SingleChildScrollView(
const SizedBox(height: 20), child: Wrap(
Material( alignment: WrapAlignment.center,
color: Colors.transparent, children: content(),
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
),
)
],
), ),
), )
), ],
), ),
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(),
)
);
}
} }
} }

View file

@ -10,6 +10,7 @@ class AddListModal extends StatefulWidget {
final Filter? list; final Filter? list;
final void Function({required String name, required String url, required String type})? onConfirm; final void Function({required String name, required String url, required String type})? onConfirm;
final void Function({required Filter list, required String type})? onEdit; final void Function({required Filter list, required String type})? onEdit;
final bool dialog;
const AddListModal({ const AddListModal({
Key? key, Key? key,
@ -17,6 +18,7 @@ class AddListModal extends StatefulWidget {
this.list, this.list,
this.onConfirm, this.onConfirm,
this.onEdit, this.onEdit,
required this.dialog
}) : super(key: key); }) : super(key: key);
@override @override
@ -68,51 +70,49 @@ class _AddListModalState extends State<AddListModal> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( Widget content() {
padding: MediaQuery.of(context).viewInsets, return Column(
child: Container( mainAxisSize: MainAxisSize.min,
height: Platform.isIOS ? 386 : 370, children: [
decoration: BoxDecoration( Flexible(
borderRadius: const BorderRadius.only( child: SingleChildScrollView(
topLeft: Radius.circular(28), child: Wrap(
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,
children: [ children: [
Padding( Row(
padding: const EdgeInsets.only(top: 24), mainAxisAlignment: MainAxisAlignment.center,
child: Icon( children: [
widget.type == 'whitelist' Column(
? Icons.verified_user_rounded children: [
: Icons.gpp_bad_rounded, Padding(
size: 24, padding: const EdgeInsets.only(top: 24),
color: Theme.of(context).listTileTheme.iconColor 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(
padding: const EdgeInsets.symmetric(horizontal: 24), padding: const EdgeInsets.symmetric(horizontal: 24),
child: TextFormField( child: TextFormField(
@ -129,7 +129,7 @@ class _AddListModalState extends State<AddListModal> {
), ),
), ),
), ),
const SizedBox(height: 30), Container(height: 30),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 24), padding: const EdgeInsets.symmetric(horizontal: 24),
child: TextFormField( child: TextFormField(
@ -151,54 +151,80 @@ class _AddListModalState extends State<AddListModal> {
], ],
), ),
), ),
Padding( ),
padding: const EdgeInsets.all(24), Padding(
child: Row( padding: const EdgeInsets.all(24),
mainAxisAlignment: MainAxisAlignment.end, child: Row(
children: [ mainAxisAlignment: MainAxisAlignment.end,
TextButton( children: [
onPressed: () => Navigator.pop(context), TextButton(
child: Text(AppLocalizations.of(context)!.cancel) onPressed: () => Navigator.pop(context),
), child: Text(AppLocalizations.of(context)!.cancel)
const SizedBox(width: 20), ),
TextButton( const SizedBox(width: 20),
onPressed: () { TextButton(
Navigator.pop(context); onPressed: () {
if (widget.list != null) { Navigator.pop(context);
final Filter newList = Filter( if (widget.list != null) {
url: urlController.text, final Filter newList = Filter(
name: nameController.text, url: urlController.text,
lastUpdated: widget.list!.lastUpdated, name: nameController.text,
id: widget.list!.id, lastUpdated: widget.list!.lastUpdated,
rulesCount: widget.list!.rulesCount, id: widget.list!.id,
enabled: widget.list!.enabled rulesCount: widget.list!.rulesCount,
); enabled: widget.list!.enabled
widget.onEdit!( );
list: newList, widget.onEdit!(
type: widget.type list: newList,
); type: widget.type
} );
else { }
widget.onConfirm!( else {
name: nameController.text, widget.onConfirm!(
url: urlController.text, name: nameController.text,
type: widget.type url: urlController.text,
); type: widget.type
} );
}, }
child: Text( },
widget.list != null child: Text(
? AppLocalizations.of(context)!.save widget.list != null
: AppLocalizations.of(context)!.confirm ? 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()
),
);
}
} }
} }

View file

@ -12,7 +12,12 @@ import 'package:adguard_home_manager/providers/app_config_provider.dart';
import 'package:adguard_home_manager/providers/servers_provider.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart';
class BlockedServicesScreen extends StatelessWidget { 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -21,7 +26,8 @@ class BlockedServicesScreen extends StatelessWidget {
return BlockedServicesScreenWidget( return BlockedServicesScreenWidget(
serversProvider: serversProvider, serversProvider: serversProvider,
appConfigProvider: appConfigProvider appConfigProvider: appConfigProvider,
dialog: dialog,
); );
} }
} }
@ -29,11 +35,13 @@ class BlockedServicesScreen extends StatelessWidget {
class BlockedServicesScreenWidget extends StatefulWidget { class BlockedServicesScreenWidget extends StatefulWidget {
final ServersProvider serversProvider; final ServersProvider serversProvider;
final AppConfigProvider appConfigProvider; final AppConfigProvider appConfigProvider;
final bool dialog;
const BlockedServicesScreenWidget({ const BlockedServicesScreenWidget({
Key? key, Key? key,
required this.serversProvider, required this.serversProvider,
required this.appConfigProvider, required this.appConfigProvider,
required this.dialog
}) : super(key: key); }) : super(key: key);
@override @override
@ -209,24 +217,74 @@ class _BlockedServicesScreenStateWidget extends State<BlockedServicesScreenWidge
} }
} }
return Scaffold( if (widget.dialog == true) {
appBar: AppBar( return Dialog(
title: Text(AppLocalizations.of(context)!.blockedServices), child: ConstrainedBox(
actions: [ constraints: const BoxConstraints(
IconButton( maxWidth: 400
onPressed: updateBlockedServices,
icon: const Icon(
Icons.save_rounded
),
tooltip: AppLocalizations.of(context)!.save,
), ),
const SizedBox(width: 10) child: Column(
], children: [
), Padding(
body: RefreshIndicator( padding: const EdgeInsets.all(16),
onRefresh: loadBlockedServices, child: Row(
child: body() mainAxisAlignment: MainAxisAlignment.spaceBetween,
), children: [
); Row(
crossAxisAlignment: CrossAxisAlignment.center,
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)!.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()
),
);
}
} }
} }

View file

@ -10,7 +10,12 @@ import 'package:adguard_home_manager/services/http_requests.dart';
import 'package:adguard_home_manager/providers/servers_provider.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart';
class CheckHostModal extends StatefulWidget { 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 @override
State<CheckHostModal> createState() => _CheckHostModalState(); State<CheckHostModal> createState() => _CheckHostModalState();
@ -117,126 +122,141 @@ class _CheckHostModalState extends State<CheckHostModal> {
} }
} }
return Padding( Widget content() {
padding: MediaQuery.of(context).viewInsets, return Column(
child: Container( mainAxisSize: MainAxisSize.min,
height: 330, children: [
width: double.maxFinite, Flexible(
decoration: BoxDecoration( child: SingleChildScrollView(
borderRadius: const BorderRadius.only( child: Wrap(
topLeft: Radius.circular(28), children: [
topRight: Radius.circular(28), Row(
), mainAxisAlignment: MainAxisAlignment.center,
color: Theme.of(context).dialogBackgroundColor children: [
), Column(
child: Center( children: [
child: Column( Padding(
children: [ padding: const EdgeInsets.only(top: 24),
Expanded( child: Icon(
child: ListView( Icons.shield_rounded,
physics: 350 < MediaQuery.of(context).size.height size: 24,
? const NeverScrollableScrollPhysics() color: Theme.of(context).listTileTheme.iconColor
: 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)
)
), ),
errorText: domainError, const SizedBox(height: 16),
labelText: AppLocalizations.of(context)!.domain, 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 ),
), Column(
child: resultWidget, 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( const SizedBox(width: 20),
padding: const EdgeInsets.only( TextButton(
top: 20, onPressed: domainController.text != '' && domainError == null
left: 20, ? () => checkHost()
right: 20 : null,
), child: Text(
child: Center( AppLocalizations.of(context)!.check,
child: Text( style: TextStyle(
AppLocalizations.of(context)!.insertDomain, color: domainController.text != '' && domainError == null
style: const TextStyle( ? Theme.of(context).colorScheme.primary
fontSize: 16, : 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()
),
);
}
} }
} }

View file

@ -2,33 +2,27 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.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/add_button.dart';
import 'package:adguard_home_manager/screens/filters/remove_custom_rule_modal.dart';
import 'package:adguard_home_manager/widgets/tab_content_list.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/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 { class CustomRulesList extends StatefulWidget {
final LoadStatus loadStatus; final LoadStatus loadStatus;
final ScrollController scrollController; final ScrollController scrollController;
final List<String> data; final List<String> data;
final Future<void> Function() fetchData; final Future<void> Function() fetchData;
final void Function(String) onRemoveCustomRule;
const CustomRulesList({ const CustomRulesList({
Key? key, Key? key,
required this.loadStatus, required this.loadStatus,
required this.scrollController, required this.scrollController,
required this.data, required this.data,
required this.fetchData required this.fetchData,
required this.onRemoveCustomRule
}) : super(key: key); }) : super(key: key);
@override @override
@ -61,52 +55,6 @@ class _CustomRulesListState extends State<CustomRulesList> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
void removeCustomRule(String rule) async {
ProcessModal processModal = ProcessModal(context: context);
processModal.open(AppLocalizations.of(context)!.deletingRule);
final List<String> 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) { bool checkIfComment(String value) {
final regex = RegExp(r'^(!|#).*$'); final regex = RegExp(r'^(!|#).*$');
if (regex.hasMatch(value)) { if (regex.hasMatch(value)) {
@ -184,7 +132,7 @@ class _CustomRulesListState extends State<CustomRulesList> {
), ),
subtitle: generateSubtitle(widget.data[index]), subtitle: generateSubtitle(widget.data[index]),
trailing: IconButton( trailing: IconButton(
onPressed: () => openRemoveCustomRuleModal(widget.data[index]), onPressed: () => widget.onRemoveCustomRule(widget.data[index]),
icon: const Icon(Icons.delete) icon: const Icon(Icons.delete)
), ),
), ),
@ -239,8 +187,12 @@ class _CustomRulesListState extends State<CustomRulesList> {
), ),
loadStatus: widget.loadStatus, loadStatus: widget.loadStatus,
onRefresh: widget.fetchData, onRefresh: widget.fetchData,
fab: const FiltersFab( fab: AddFiltersButton(
type: 'custom_rule', type: 'custom_rule',
widget: (fn) => FloatingActionButton(
onPressed: fn,
child: const Icon(Icons.add),
),
), ),
fabVisible: isVisible, fabVisible: isVisible,
); );

View file

@ -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
),
),
],
),
)
],
),
);
}
}

View file

@ -1,18 +1,23 @@
// ignore_for_file: use_build_context_synchronously // ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.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/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/blocked_services_screen.dart';
import 'package:adguard_home_manager/screens/filters/update_interval_lists_modal.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/functions/snackbar.dart';
import 'package:adguard_home_manager/classes/process_modal.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/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/constants/enums.dart';
import 'package:adguard_home_manager/services/http_requests.dart'; import 'package:adguard_home_manager/services/http_requests.dart';
import 'package:adguard_home_manager/models/clients.dart'; import 'package:adguard_home_manager/models/clients.dart';
@ -47,10 +52,7 @@ class FiltersWidget extends StatefulWidget {
State<FiltersWidget> createState() => _FiltersWidgetState(); State<FiltersWidget> createState() => _FiltersWidgetState();
} }
class _FiltersWidgetState extends State<FiltersWidget> with TickerProviderStateMixin { class _FiltersWidgetState extends State<FiltersWidget> {
late TabController tabController;
final ScrollController scrollController = ScrollController();
Future fetchFilters() async { Future fetchFilters() async {
widget.serversProvider.setFilteringLoadStatus(LoadStatus.loading, false); widget.serversProvider.setFilteringLoadStatus(LoadStatus.loading, false);
@ -68,20 +70,14 @@ class _FiltersWidgetState extends State<FiltersWidget> with TickerProviderStateM
} }
} }
List<AutoClient> generateClientsList(List<AutoClient> clients, List<String> ips) {
return clients.where((client) => ips.contains(client.ip)).toList();
}
@override @override
void initState() { void initState() {
fetchFilters(); fetchFilters();
super.initState(); super.initState();
tabController = TabController(
initialIndex: 0,
length: 3,
vsync: this,
);
tabController.addListener(() => widget.appConfigProvider.setSelectedFiltersTab(tabController.index));
}
List<AutoClient> generateClientsList(List<AutoClient> clients, List<String> ips) {
return clients.where((client) => ips.contains(client.ip)).toList();
} }
@override @override
@ -89,6 +85,8 @@ class _FiltersWidgetState extends State<FiltersWidget> with TickerProviderStateM
final serversProvider = Provider.of<ServersProvider>(context); final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context); final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
void fetchUpdateLists() async { void fetchUpdateLists() async {
ProcessModal processModal = ProcessModal(context: context); ProcessModal processModal = ProcessModal(context: context);
processModal.open(AppLocalizations.of(context)!.updatingLists); processModal.open(AppLocalizations.of(context)!.updatingLists);
@ -139,12 +137,24 @@ class _FiltersWidgetState extends State<FiltersWidget> with TickerProviderStateM
void showCheckHostModal() { void showCheckHostModal() {
Future.delayed(const Duration(seconds: 0), () { Future.delayed(const Duration(seconds: 0), () {
showModalBottomSheet( if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) {
context: context, showDialog(
builder: (context) => const CheckHostModal(), context: context,
backgroundColor: Colors.transparent, builder: (context) => const CheckHostModal(
isScrollControlled: true, 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<FiltersWidget> with TickerProviderStateM
void openBlockedServicesModal() { void openBlockedServicesModal() {
Future.delayed(const Duration(seconds: 0), () { Future.delayed(const Duration(seconds: 0), () {
Navigator.of(context).push( if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) {
MaterialPageRoute( showDialog(
builder: (context) => const BlockedServicesScreen(), context: context,
) builder: (context) => const BlockedServicesScreen(
); dialog: true,
),
barrierDismissible: false
);
}
else {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const BlockedServicesScreen(
dialog: false,
),
)
);
}
}); });
} }
return DefaultTabController( void removeCustomRule(String rule) async {
length: 3, ProcessModal processModal = ProcessModal(context: context);
child: NestedScrollView( processModal.open(AppLocalizations.of(context)!.deletingRule);
controller: scrollController,
headerSliverBuilder: ((context, innerBoxIsScrolled) { final List<String> newRules = serversProvider.filtering.data!.userRules.where((r) => r != rule).toList();
return [
SliverOverlapAbsorber( final result = await setCustomRules(server: serversProvider.selectedServer!, rules: newRules);
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar( processModal.close();
title: Text(AppLocalizations.of(context)!.filters),
pinned: true, if (result['result'] == 'success') {
floating: true, FilteringData filteringData = serversProvider.filtering.data!;
forceElevated: innerBoxIsScrolled, filteringData.userRules = newRules;
centerTitle: false, serversProvider.setFilteringData(filteringData);
actions: serversProvider.filtering.loadStatus == LoadStatus.loaded ? [
IconButton( showSnacbkar(
onPressed: enableDisableFiltering, context: context,
tooltip: serversProvider.filtering.data!.enabled == true appConfigProvider: appConfigProvider,
? AppLocalizations.of(context)!.disableFiltering label: AppLocalizations.of(context)!.ruleRemovedSuccessfully,
: AppLocalizations.of(context)!.enableFiltering, color: Colors.green
icon: Stack( );
children: [ }
const Icon(Icons.power_settings_new_rounded), else {
Positioned( appConfigProvider.addLog(result['log']);
bottom: 0,
right: 0, showSnacbkar(
child: Stack( context: context,
children: [ appConfigProvider: appConfigProvider,
Container( label: AppLocalizations.of(context)!.ruleNotRemoved,
decoration: BoxDecoration( color: Colors.red
borderRadius: BorderRadius.circular(30), );
color: Colors.white }
), }
child: Icon(
serversProvider.filtering.data!.enabled == true void openRemoveCustomRuleModal(String rule) {
? Icons.check_circle_rounded showDialog(
: Icons.cancel, context: context,
size: 12, builder: (context) => RemoveCustomRule(
color: serversProvider.filtering.data!.enabled == true onConfirm: () => removeCustomRule(rule),
? appConfigProvider.useThemeColorForStatus == true )
? Theme.of(context).colorScheme.primary );
: Colors.green }
: appConfigProvider.useThemeColorForStatus == true
? Colors.grey void openListDetails(Filter filter, String type) {
: Colors.red if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) {
), showDialog(
), context: context,
], builder: (context) => ListDetailsScreen(
), list: filter,
) type: type,
], dialog: true,
) ),
), barrierDismissible: false
IconButton( );
onPressed: () { }
showModalBottomSheet( else {
context: context, Navigator.of(context).push(
builder: (context) => UpdateIntervalListsModal( MaterialPageRoute(
interval: serversProvider.filtering.data!.interval, builder: (context) => ListDetailsScreen(
onChange: setUpdateFrequency list: filter,
type: type,
dialog: false,
)
)
);
}
}
List<Widget> 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, child: Icon(
isScrollControlled: true serversProvider.filtering.data!.enabled == true
); ? Icons.check_circle_rounded
}, : Icons.cancel,
icon: const Icon(Icons.update_rounded) 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( IconButton(
children: [ onPressed: () {
const Icon(Icons.sync_rounded), if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) {
const SizedBox(width: 10), showDialog(
Text(AppLocalizations.of(context)!.updateLists) context: context,
], builder: (context) => UpdateIntervalListsModal(
) interval: serversProvider.filtering.data!.interval,
), onChange: setUpdateFrequency,
PopupMenuItem( dialog: true,
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), );
] : [], }
bottom: TabBar( else {
controller: tabController, showModalBottomSheet(
isScrollable: false, context: context,
unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant, builder: (context) => UpdateIntervalListsModal(
tabs: [ interval: serversProvider.filtering.data!.interval,
Tab( onChange: setUpdateFrequency,
icon: const Icon(Icons.verified_user_rounded), dialog: false,
text: AppLocalizations.of(context)!.whitelists, ),
), backgroundColor: Colors.transparent,
Tab( isScrollControlled: true
icon: const Icon(Icons.gpp_bad_rounded), );
text: AppLocalizations.of(context)!.blacklist, }
), },
Tab( icon: const Icon(Icons.update_rounded),
icon: const Icon(Icons.shield_rounded), tooltip: AppLocalizations.of(context)!.updateFrequency,
text: AppLocalizations.of(context)!.customRules, ),
), 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(
body: TabBarView( children: [
controller: tabController, const Icon(Icons.block),
children: [ const SizedBox(width: 10),
FiltersList( Text(AppLocalizations.of(context)!.blockedServices)
loadStatus: serversProvider.filtering.loadStatus, ],
scrollController: scrollController, )
type: 'whitelist', ),
data: serversProvider.filtering.loadStatus == LoadStatus.loaded PopupMenuItem(
? serversProvider.filtering.data!.whitelistFilters : [], onTap: showCheckHostModal,
fetchData: fetchFilters, child: Row(
), children: [
FiltersList( const Icon(Icons.shield_rounded),
loadStatus: serversProvider.filtering.loadStatus, const SizedBox(width: 10),
scrollController: scrollController, Text(AppLocalizations.of(context)!.checkHostFiltered)
type: 'blacklist', ],
data: serversProvider.filtering.loadStatus == LoadStatus.loaded )
? serversProvider.filtering.data!.filters : [], ),
fetchData: fetchFilters, ]
), ),
CustomRulesList( const SizedBox(width: 5),
loadStatus: serversProvider.filtering.loadStatus, ];
scrollController: scrollController, }
data: serversProvider.filtering.loadStatus == LoadStatus.loaded else {
? serversProvider.filtering.data!.userRules : [], return [];
fetchData: fetchFilters, }
), }
]
) 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,
);
}
} }
} }

View file

@ -7,8 +7,7 @@ import 'package:provider/provider.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.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/add_button.dart';
import 'package:adguard_home_manager/screens/filters/list_details_screen.dart';
import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; import 'package:adguard_home_manager/widgets/custom_list_tile.dart';
import 'package:adguard_home_manager/widgets/tab_content_list.dart'; import 'package:adguard_home_manager/widgets/tab_content_list.dart';
@ -23,6 +22,7 @@ class FiltersList extends StatefulWidget {
final List<Filter> data; final List<Filter> data;
final Future<void> Function() fetchData; final Future<void> Function() fetchData;
final String type; final String type;
final void Function(Filter, String) onOpenDetailsScreen;
const FiltersList({ const FiltersList({
Key? key, Key? key,
@ -31,6 +31,7 @@ class FiltersList extends StatefulWidget {
required this.data, required this.data,
required this.fetchData, required this.fetchData,
required this.type, required this.type,
required this.onOpenDetailsScreen
}) : super(key: key); }) : super(key: key);
@override @override
@ -64,17 +65,6 @@ class _FiltersListState extends State<FiltersList> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final appConfigProvider = Provider.of<AppConfigProvider>(context); final appConfigProvider = Provider.of<AppConfigProvider>(context);
void openDetailsModal(Filter filter) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ListDetailsScreen(
list: filter,
type: widget.type,
)
)
);
}
return CustomTabContentList( return CustomTabContentList(
loadingGenerator: () => SizedBox( loadingGenerator: () => SizedBox(
@ -112,7 +102,7 @@ class _FiltersListState extends State<FiltersList> {
? Colors.grey ? Colors.grey
: Colors.red : Colors.red
), ),
onTap: () => openDetailsModal(widget.data[index]), onTap: () => widget.onOpenDetailsScreen(widget.data[index], widget.type),
), ),
noData: Container( noData: Container(
width: double.maxFinite, width: double.maxFinite,
@ -166,8 +156,12 @@ class _FiltersListState extends State<FiltersList> {
), ),
loadStatus: widget.loadStatus, loadStatus: widget.loadStatus,
onRefresh: widget.fetchData, onRefresh: widget.fetchData,
fab: FiltersFab( fab: AddFiltersButton(
type: widget.type, type: widget.type,
widget: (fn) => FloatingActionButton(
onPressed: fn,
child: const Icon(Icons.add),
),
), ),
fabVisible: isVisible, fabVisible: isVisible,
); );

View file

@ -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<Widget> 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<FiltersTabsView> createState() => _FiltersTabsViewState();
}
class _FiltersTabsViewState extends State<FiltersTabsView> 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<ServersProvider>(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,
),
]
)
)
);
}
}

View file

@ -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<Widget> 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<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(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(),
);
}
}

View file

@ -7,9 +7,9 @@ import 'package:flutter/rendering.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.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/add_list_modal.dart';
import 'package:adguard_home_manager/screens/filters/delete_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/functions/format_time.dart';
import 'package:adguard_home_manager/providers/app_config_provider.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 { class ListDetailsScreen extends StatefulWidget {
final Filter list; final Filter list;
final String type; final String type;
final bool dialog;
const ListDetailsScreen({ const ListDetailsScreen({
Key? key, Key? key,
required this.list, required this.list,
required this.type, required this.type,
required this.dialog
}) : super(key: key); }) : super(key: key);
@override @override
@ -68,6 +70,8 @@ class _ListDetailsScreenState extends State<ListDetailsScreen> {
final serversProvider = Provider.of<ServersProvider>(context); final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context); final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
void enableDisableList(Filter list, bool newStatus) async { void enableDisableList(Filter list, bool newStatus) async {
ProcessModal processModal = ProcessModal(context: context); ProcessModal processModal = ProcessModal(context: context);
processModal.open( processModal.open(
@ -216,108 +220,234 @@ class _ListDetailsScreenState extends State<ListDetailsScreen> {
} }
} }
return Scaffold( List<Widget> content() {
appBar: AppBar( return [
title: Text(AppLocalizations.of(context)!.listDetails), CustomListTile(
actions: [ icon: Icons.shield_rounded,
IconButton( title: AppLocalizations.of(context)!.currentStatus,
onPressed: () => { 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<Widget> 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( showModalBottomSheet(
context: context, context: context,
builder: (ctx) => AddListModal( builder: (ctx) => AddListModal(
list: widget.list, list: widget.list,
type: widget.type, type: widget.type,
onEdit: confirmEditList onEdit: confirmEditList,
dialog: false,
), ),
isScrollControlled: true, isScrollControlled: true,
backgroundColor: Colors.transparent 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( child: Column(
onPressed: () { mainAxisSize: MainAxisSize.min,
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(
children: [ children: [
FilterListTile( Padding(
icon: Icons.shield_rounded, padding: const EdgeInsets.all(16),
title: AppLocalizations.of(context)!.currentStatus, child: Row(
subtitle: enabled == true mainAxisAlignment: MainAxisAlignment.spaceBetween,
? AppLocalizations.of(context)!.enabled children: [
: AppLocalizations.of(context)!.disabled, Row(
color: enabled == true children: [
? appConfigProvider.useThemeColorForStatus == true IconButton(
? Theme.of(context).colorScheme.primary onPressed: () => Navigator.pop(context),
: Colors.green icon: const Icon(Icons.clear_rounded),
: appConfigProvider.useThemeColorForStatus == true tooltip: AppLocalizations.of(context)!.close,
? Colors.grey ),
: Colors.red, const SizedBox(width: 8),
bold: true, Text(
), AppLocalizations.of(context)!.listDetails,
FilterListTile( style: const TextStyle(
icon: Icons.badge_rounded, fontSize: 22
title: AppLocalizations.of(context)!.name, ),
subtitle: name )
), ],
FilterListTile( ),
icon: Icons.link_rounded, Row(
title: "URL", children: [
subtitle: widget.list.url IconButton(
), onPressed: () => enableDisableList(widget.list, !enabled),
FilterListTile( icon: Icon(
icon: Icons.list_rounded, enabled == true
title: AppLocalizations.of(context)!.rules, ? Icons.gpp_bad_rounded
subtitle: widget.list.rulesCount.toString() : Icons.verified_user_rounded,
), ),
FilterListTile( tooltip: enabled == true
icon: Icons.shield_rounded, ? AppLocalizations.of(context)!.disableList
title: AppLocalizations.of(context)!.listType, : AppLocalizations.of(context)!.enableList,
subtitle: widget.type == 'whitelist' ),
? AppLocalizations.of(context)!.whitelist ...actions()
: 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'),
), ),
Flexible(
child: SingleChildScrollView(
child: Wrap(
children: content(),
),
)
)
], ],
), ),
AnimatedPositioned( )
duration: const Duration(milliseconds: 100), );
curve: Curves.easeInOut, }
bottom: fabVisible ? else {
appConfigProvider.showingSnackbar return Scaffold(
? 70 : (Platform.isIOS ? 40 : 20) appBar: AppBar(
: -70, title: Text(AppLocalizations.of(context)!.listDetails),
right: 20, actions: actions(),
child: FloatingActionButton( ),
onPressed: () => enableDisableList(widget.list, !enabled), body: Stack(
child: Icon( children: [
enabled == true ListView(
? Icons.gpp_bad_rounded children: content(),
: Icons.verified_user_rounded,
),
), ),
) 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,
),
),
)
],
),
);
}
} }
} }

View file

@ -9,11 +9,13 @@ import 'package:adguard_home_manager/widgets/option_box.dart';
class UpdateIntervalListsModal extends StatefulWidget { class UpdateIntervalListsModal extends StatefulWidget {
final int interval; final int interval;
final void Function(int) onChange; final void Function(int) onChange;
final bool dialog;
const UpdateIntervalListsModal({ const UpdateIntervalListsModal({
Key? key, Key? key,
required this.interval, required this.interval,
required this.onChange, required this.onChange,
required this.dialog
}) : super(key: key); }) : super(key: key);
@override @override
@ -37,272 +39,266 @@ class _UpdateIntervalListsModalState extends State<UpdateIntervalListsModal> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final MediaQueryData mediaQueryData = MediaQuery.of(context); final MediaQueryData mediaQueryData = MediaQuery.of(context);
return Padding( Widget content() {
padding: mediaQueryData.viewInsets, return Column(
child: Container( mainAxisSize: MainAxisSize.min,
height: Platform.isIOS ? 406 : 390, children: [
decoration: BoxDecoration( Flexible(
color: Theme.of(context).dialogBackgroundColor, child: SingleChildScrollView(
borderRadius: const BorderRadius.only( child: Wrap(
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,
children: [ children: [
Padding( Row(
padding: const EdgeInsets.only(top: 24), mainAxisAlignment: MainAxisAlignment.center,
child: Icon( children: [
Icons.update_rounded, Column(
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(
children: [ children: [
Row( Padding(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, padding: const EdgeInsets.only(top: 24),
children: [ child: Icon(
Container( Icons.update_rounded,
width: (mediaQueryData.size.width-70)/2, size: 24,
margin: const EdgeInsets.only( color: Theme.of(context).listTileTheme.iconColor
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),
),
),
),
),
],
), ),
Row( Container(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, padding: const EdgeInsets.symmetric(
children: [ horizontal: 24,
Container( vertical: 16
width: (mediaQueryData.size.width-70)/2, ),
margin: const EdgeInsets.only( child: Text(
top: 5, AppLocalizations.of(context)!.updateFrequency,
right: 5, textAlign: TextAlign.center,
bottom: 5 overflow: TextOverflow.ellipsis,
), style: TextStyle(
child: OptionBox( fontSize: 24,
optionsValue: selectedOption, color: Theme.of(context).colorScheme.onSurface
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( ),
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), Padding(
child: Row( padding: const EdgeInsets.all(24),
mainAxisAlignment: MainAxisAlignment.end, child: Row(
children: [ mainAxisAlignment: MainAxisAlignment.end,
TextButton( children: [
onPressed: () => Navigator.pop(context), TextButton(
child: Text(AppLocalizations.of(context)!.cancel), onPressed: () => Navigator.pop(context),
), child: Text(AppLocalizations.of(context)!.cancel),
const SizedBox(width: 20), ),
TextButton( const SizedBox(width: 20),
onPressed: selectedOption != null TextButton(
? () { onPressed: selectedOption != null
Navigator.pop(context); ? () {
widget.onChange(selectedOption!); Navigator.pop(context);
} widget.onChange(selectedOption!);
: null, }
style: ButtonStyle( : null,
overlayColor: MaterialStateProperty.all( style: ButtonStyle(
Theme.of(context).colorScheme.primary.withOpacity(0.1) overlayColor: MaterialStateProperty.all(
), Theme.of(context).colorScheme.primary.withOpacity(0.1)
foregroundColor: MaterialStateProperty.all( ),
selectedOption != null foregroundColor: MaterialStateProperty.all(
? Theme.of(context).colorScheme.primary selectedOption != null
: Colors.grey, ? Theme.of(context).colorScheme.primary
), : Colors.grey,
), ),
child: Text(AppLocalizations.of(context)!.confirm), ),
), 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()
),
);
}
} }
} }

View file

@ -48,12 +48,15 @@ class HomeChart extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Flexible(
label, child: Text(
style: TextStyle( label,
fontSize: 18, overflow: TextOverflow.ellipsis,
fontWeight: FontWeight.w500, style: TextStyle(
color: Theme.of(context).colorScheme.onSurface fontSize: 18,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurface
),
), ),
), ),
!isEmpty !isEmpty

View file

@ -12,13 +12,27 @@ class HomeFab extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final serversProvider = Provider.of<ServersProvider>(context); final serversProvider = Provider.of<ServersProvider>(context);
final width = MediaQuery.of(context).size.width;
void openManagementBottomSheet() { void openManagementBottomSheet() {
showModalBottomSheet( if (width > 700) {
context: context, showDialog(
isScrollControlled: true, context: context,
builder: (context) => const ManagementModal(), builder: (context) => const ManagementModal(
backgroundColor: Colors.transparent, dialog: true,
); ),
);
}
else {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => const ManagementModal(
dialog: false,
),
backgroundColor: Colors.transparent,
);
}
} }
return serversProvider.serverStatus.loadStatus == 1 return serversProvider.serverStatus.loadStatus == 1

View file

@ -55,6 +55,8 @@ class _HomeState extends State<Home> {
final serversProvider = Provider.of<ServersProvider>(context); final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context); final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
Widget status() { Widget status() {
switch (serversProvider.serverStatus.loadStatus) { switch (serversProvider.serverStatus.loadStatus) {
case 0: case 0:
@ -92,72 +94,138 @@ class _HomeState extends State<Home> {
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
HomeChart( Wrap(
data: serversProvider.serverStatus.data!.stats.dnsQueries, children: [
label: AppLocalizations.of(context)!.dnsQueries, FractionallySizedBox(
primaryValue: intFormat(serversProvider.serverStatus.data!.stats.numDnsQueries, Platform.localeName), widthFactor: width > 700 ? 0.5 : 1,
secondaryValue: "${doubleFormat(serversProvider.serverStatus.data!.stats.avgProcessingTime*1000, Platform.localeName)} ms", child: HomeChart(
color: Colors.blue, data: serversProvider.serverStatus.data!.stats.dnsQueries,
), label: AppLocalizations.of(context)!.dnsQueries,
primaryValue: intFormat(serversProvider.serverStatus.data!.stats.numDnsQueries, Platform.localeName),
HomeChart( secondaryValue: "${doubleFormat(serversProvider.serverStatus.data!.stats.avgProcessingTime*1000, Platform.localeName)} ms",
data: serversProvider.serverStatus.data!.stats.blockedFiltering, color: Colors.blue,
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}%", FractionallySizedBox(
color: Colors.red, 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( if (width <= 700) ...[
data: serversProvider.serverStatus.data!.stats.replacedSafebrowsing, TopItems(
label: AppLocalizations.of(context)!.malwarePhisingBlocked, label: AppLocalizations.of(context)!.topQueriedDomains,
primaryValue: intFormat(serversProvider.serverStatus.data!.stats.numReplacedSafebrowsing, Platform.localeName), data: serversProvider.serverStatus.data!.stats.topQueriedDomains,
secondaryValue: "${serversProvider.serverStatus.data!.stats.numDnsQueries > 0 ? doubleFormat((serversProvider.serverStatus.data!.stats.numReplacedSafebrowsing/serversProvider.serverStatus.data!.stats.numDnsQueries)*100, Platform.localeName) : 0}%", type: 'topQueriedDomains',
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),
), ),
), Padding(
const SizedBox(height: 20), padding: const EdgeInsets.symmetric(horizontal: 20),
child: Divider(
TopItems( thickness: 1,
label: AppLocalizations.of(context)!.topBlockedDomains, color: Theme.of(context).colorScheme.onSurface.withOpacity(0.2),
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( const SizedBox(height: 20),
label: AppLocalizations.of(context)!.topClients,
data: serversProvider.serverStatus.data!.stats.topClients, TopItems(
type: 'topClients', label: AppLocalizations.of(context)!.topBlockedDomains,
clients: true, 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',
),
),
),
],
),
],
)
], ],
); );

View file

@ -15,7 +15,12 @@ import 'package:adguard_home_manager/services/http_requests.dart';
import 'package:adguard_home_manager/providers/servers_provider.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart';
class ManagementModal extends StatefulWidget { 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 @override
State<ManagementModal> createState() => _ManagementModalState(); State<ManagementModal> createState() => _ManagementModalState();
@ -364,8 +369,112 @@ class _ManagementModalState extends State<ManagementModal> with SingleTickerProv
); );
} }
return SafeArea( Widget header() {
child: Container( 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<Widget> 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( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
@ -373,66 +482,18 @@ class _ManagementModalState extends State<ManagementModal> with SingleTickerProv
topRight: Radius.circular(28) topRight: Radius.circular(28)
) )
), ),
child: Wrap( child: Column(
mainAxisSize: MainAxisSize.min,
children: [ children: [
Row( Flexible(
mainAxisAlignment: MainAxisAlignment.center, child: SingleChildScrollView(
children: [ child: Wrap(
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Padding( header(),
padding: const EdgeInsets.only(top: 24), ...toggles()
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,
),
),
),
], ],
), ),
], ),
),
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(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
@ -449,7 +510,7 @@ class _ManagementModalState extends State<ManagementModal> with SingleTickerProv
if (Platform.isIOS) const SizedBox(height: 16) if (Platform.isIOS) const SizedBox(height: 16)
], ],
), ),
), );
); }
} }
} }

View file

@ -15,8 +15,12 @@ class ServerStatus extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
return Container( 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( child: Column(
children: [ children: [
Text( Text(
@ -29,11 +33,11 @@ class ServerStatus extends StatelessWidget {
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
SizedBox( SizedBox(
height: 140, height: width > 700 ? 70 : 140,
child: GridView( child: GridView(
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, crossAxisCount: width > 700 ? 4 : 2,
crossAxisSpacing: 10, crossAxisSpacing: 10,
mainAxisSpacing: 10, mainAxisSpacing: 10,
mainAxisExtent: 65 mainAxisExtent: 65

View file

@ -44,13 +44,15 @@ class StatusBox extends StatelessWidget {
: Colors.grey.computeLuminance() > 0.5 ? Colors.black : Colors.white, : Colors.grey.computeLuminance() > 0.5 ? Colors.black : Colors.white,
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Text( Flexible(
label, child: Text(
style: TextStyle( label,
color: appConfigProvider.useThemeColorForStatus == true style: TextStyle(
? Theme.of(context).colorScheme.primary.computeLuminance() > 0.5 ? Colors.black : Colors.white color: appConfigProvider.useThemeColorForStatus == true
: Colors.grey.computeLuminance() > 0.5 ? Colors.black : Colors.white, ? Theme.of(context).colorScheme.primary.computeLuminance() > 0.5 ? Colors.black : Colors.white
fontWeight: FontWeight.w500 : Colors.grey.computeLuminance() > 0.5 ? Colors.black : Colors.white,
fontWeight: FontWeight.w500
),
), ),
) )
], ],

View file

@ -1,5 +1,9 @@
// ignore_for_file: use_build_context_synchronously // 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/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -35,6 +39,8 @@ class TopItems extends StatelessWidget {
final appConfigProvider = Provider.of<AppConfigProvider>(context); final appConfigProvider = Provider.of<AppConfigProvider>(context);
final logsProvider = Provider.of<LogsProvider>(context); final logsProvider = Provider.of<LogsProvider>(context);
final width = MediaQuery.of(context).size.width;
bool? getIsBlocked() { bool? getIsBlocked() {
if (type == 'topBlockedDomains') { if (type == 'topBlockedDomains') {
return true; return true;
@ -266,16 +272,32 @@ class TopItems extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
TextButton( TextButton(
onPressed: () => Navigator.of(context).push( onPressed: () => {
MaterialPageRoute( if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) {
builder: (context) => TopItemsScreen( showDialog(
type: type, context: context,
title: label, barrierDismissible: false,
isClient: clients, builder: (context) => TopItemsModal(
data: generateData(), 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( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [

View file

@ -8,10 +8,12 @@ import 'package:adguard_home_manager/providers/logs_provider.dart';
class ClientsModal extends StatefulWidget { class ClientsModal extends StatefulWidget {
final List<String>? value; final List<String>? value;
final bool dialog;
const ClientsModal({ const ClientsModal({
Key? key, Key? key,
required this.value required this.value,
required this.dialog
}) : super(key: key); }) : super(key: key);
@override @override
@ -94,44 +96,36 @@ class _ClientsModalState extends State<ClientsModal> {
}); });
} }
return Container( Widget content() {
height: height >= (logsProvider.clients!.length*64) == true return Column(
? logsProvider.clients!.length*64 mainAxisSize: MainAxisSize.min,
: height-50,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28),
topRight: Radius.circular(28)
),
color: Theme.of(context).dialogBackgroundColor
),
child: Column(
children: [ children: [
Padding( Column(
padding: const EdgeInsets.only( children: [
top: 24, Padding(
bottom: 16, padding: const EdgeInsets.only(
), top: 24,
child: Icon( bottom: 16,
Icons.smartphone_rounded, ),
size: 24, child: Icon(
color: Theme.of(context).listTileTheme.iconColor 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( Flexible(
AppLocalizations.of(context)!.clients,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurface
),
),
const SizedBox(height: 16),
Expanded(
child: ListView.builder( child: ListView.builder(
physics: height >= (logsProvider.clients!.length*64) == true
? const NeverScrollableScrollPhysics()
: null,
itemCount: logsProvider.clients!.length, itemCount: logsProvider.clients!.length,
itemBuilder: (context, index) => listItem( itemBuilder: (context, index) => listItem(
label: logsProvider.clients![index].ip, label: logsProvider.clients![index].ip,
@ -150,7 +144,7 @@ class _ClientsModalState extends State<ClientsModal> {
} }
} }
) )
), )
), ),
Padding( Padding(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
@ -176,7 +170,35 @@ class _ClientsModalState extends State<ClientsModal> {
), ),
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 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()
),
);
}
} }
} }

View file

@ -8,10 +8,12 @@ import 'package:adguard_home_manager/providers/logs_provider.dart';
class FilterStatusModal extends StatefulWidget { class FilterStatusModal extends StatefulWidget {
final String value; final String value;
final bool dialog;
const FilterStatusModal({ const FilterStatusModal({
Key? key, Key? key,
required this.value required this.value,
required this.dialog
}) : super(key: key); }) : super(key: key);
@override @override
@ -31,8 +33,6 @@ class _FilterStatusModalState extends State<FilterStatusModal> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final logsProvider = Provider.of<LogsProvider>(context); final logsProvider = Provider.of<LogsProvider>(context);
final height = MediaQuery.of(context).size.height;
void apply() async { void apply() async {
logsProvider.setSelectedResultStatus(selectedResultStatus); logsProvider.setSelectedResultStatus(selectedResultStatus);
@ -83,95 +83,94 @@ class _FilterStatusModalState extends State<FilterStatusModal> {
); );
} }
return Container( Widget content() {
height: height >= (Platform.isIOS ? 736 : 720) == true return Column(
? (Platform.isIOS ? 736 : 720) mainAxisSize: MainAxisSize.min,
: height-25,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28),
topRight: Radius.circular(28)
),
color: Theme.of(context).dialogBackgroundColor
),
child: Column(
children: [ children: [
Padding( Flexible(
padding: const EdgeInsets.only( child: SingleChildScrollView(
top: 24, child: Wrap(
bottom: 16, children: [
), Row(
child: Icon( mainAxisAlignment: MainAxisAlignment.center,
Icons.shield_rounded, children: [
size: 24, Column(
color: Theme.of(context).listTileTheme.iconColor children: [
), Padding(
), padding: const EdgeInsets.only(
Text( top: 24,
AppLocalizations.of(context)!.responseStatus, bottom: 16,
style: TextStyle( ),
fontSize: 24, child: Icon(
fontWeight: FontWeight.w400, Icons.shield_rounded,
color: Theme.of(context).colorScheme.onSurface size: 24,
), color: Theme.of(context).listTileTheme.iconColor
), ),
const SizedBox(height: 16), ),
Expanded( Text(
child: ListView( AppLocalizations.of(context)!.responseStatus,
physics: height >= 720 == true style: TextStyle(
? const NeverScrollableScrollPhysics() fontSize: 24,
: null, fontWeight: FontWeight.w400,
children: [ color: Theme.of(context).colorScheme.onSurface
filterStatusListItem( ),
id: "all", ),
icon: Icons.shield_rounded, ],
label: AppLocalizations.of(context)!.all, )
onChanged: (value) => setState(() => selectedResultStatus = value!) ],
), ),
filterStatusListItem( Container(height: 16),
id: "filtered", filterStatusListItem(
icon: Icons.shield_rounded, id: "all",
label: AppLocalizations.of(context)!.filtered, icon: Icons.shield_rounded,
onChanged: (value) => setState(() => selectedResultStatus = value!) label: AppLocalizations.of(context)!.all,
), onChanged: (value) => setState(() => selectedResultStatus = value!)
filterStatusListItem( ),
id: "processed", filterStatusListItem(
icon: Icons.verified_user_rounded, id: "filtered",
label: AppLocalizations.of(context)!.processedRow, icon: Icons.shield_rounded,
onChanged: (value) => setState(() => selectedResultStatus = value!) label: AppLocalizations.of(context)!.filtered,
), onChanged: (value) => setState(() => selectedResultStatus = value!)
filterStatusListItem( ),
id: "whitelisted", filterStatusListItem(
icon: Icons.verified_user_rounded, id: "processed",
label: AppLocalizations.of(context)!.processedWhitelistRow, icon: Icons.verified_user_rounded,
onChanged: (value) => setState(() => selectedResultStatus = value!) label: AppLocalizations.of(context)!.processedRow,
), onChanged: (value) => setState(() => selectedResultStatus = value!)
filterStatusListItem( ),
id: "blocked", filterStatusListItem(
icon: Icons.gpp_bad_rounded, id: "whitelisted",
label: AppLocalizations.of(context)!.blocked, icon: Icons.verified_user_rounded,
onChanged: (value) => setState(() => selectedResultStatus = value!) label: AppLocalizations.of(context)!.processedWhitelistRow,
), onChanged: (value) => setState(() => selectedResultStatus = value!)
filterStatusListItem( ),
id: "blocked_safebrowsing", filterStatusListItem(
icon: Icons.gpp_bad_rounded, id: "blocked",
label: AppLocalizations.of(context)!.blockedSafeBrowsingRow, icon: Icons.gpp_bad_rounded,
onChanged: (value) => setState(() => selectedResultStatus = value!) label: AppLocalizations.of(context)!.blocked,
), onChanged: (value) => setState(() => selectedResultStatus = value!)
filterStatusListItem( ),
id: "blocked_parental", filterStatusListItem(
icon: Icons.gpp_bad_rounded, id: "blocked_safebrowsing",
label: AppLocalizations.of(context)!.blockedParentalRow, icon: Icons.gpp_bad_rounded,
onChanged: (value) => setState(() => selectedResultStatus = value!) label: AppLocalizations.of(context)!.blockedSafeBrowsingRow,
), onChanged: (value) => setState(() => selectedResultStatus = value!)
filterStatusListItem( ),
id: "safe_search", filterStatusListItem(
icon: Icons.gpp_bad_rounded, id: "blocked_parental",
label: AppLocalizations.of(context)!.blockedSafeSearchRow, icon: Icons.gpp_bad_rounded,
onChanged: (value) => setState(() => selectedResultStatus = value!) 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( Padding(
@ -188,7 +187,30 @@ class _FilterStatusModalState extends State<FilterStatusModal> {
), ),
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 Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28),
topRight: Radius.circular(28)
),
color: Theme.of(context).dialogBackgroundColor
),
child: content()
);
}
} }
} }

View file

@ -19,10 +19,12 @@ import 'package:adguard_home_manager/providers/app_config_provider.dart';
class LogDetailsScreen extends StatelessWidget { class LogDetailsScreen extends StatelessWidget {
final Log log; final Log log;
final bool dialog;
const LogDetailsScreen({ const LogDetailsScreen({
Key? key, Key? key,
required this.log required this.log,
required this.dialog
}) : super(key: key); }) : super(key: key);
@override @override
@ -105,25 +107,8 @@ class LogDetailsScreen extends StatelessWidget {
} }
} }
return Scaffold( Widget content() {
appBar: AppBar( return ListView(
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(
children: [ children: [
SectionLabel(label: AppLocalizations.of(context)!.status), SectionLabel(label: AppLocalizations.of(context)!.status),
LogListTile( LogListTile(
@ -247,7 +232,87 @@ class LogDetailsScreen extends StatelessWidget {
)).toList() )).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()
);
}
} }
} }

View file

@ -1,10 +1,10 @@
// ignore_for_file: use_build_context_synchronously // ignore_for_file: use_build_context_synchronously
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.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/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/copy_clipboard.dart';
import 'package:adguard_home_manager/functions/block_unblock_domain.dart'; import 'package:adguard_home_manager/functions/block_unblock_domain.dart';
@ -18,12 +18,18 @@ class LogTile extends StatelessWidget {
final Log log; final Log log;
final int length; final int length;
final int index; final int index;
final bool? isLogSelected;
final void Function(Log) onLogTap;
final bool? useAlwaysNormalTile;
const LogTile({ const LogTile({
Key? key, Key? key,
required this.log, required this.log,
required this.length, required this.length,
required this.index required this.index,
this.isLogSelected,
required this.onLogTap,
this.useAlwaysNormalTile
}) : super(key: key); }) : super(key: key);
@override @override
@ -38,7 +44,7 @@ class LogTile extends StatelessWidget {
required String text required String text
}) { }) {
return SizedBox( return SizedBox(
width: 70, width: 80,
child: Column( child: Column(
children: [ children: [
Icon( Icon(
@ -95,133 +101,250 @@ class LogTile extends StatelessWidget {
) )
); );
} }
return Material( if (width > 1100 && !(useAlwaysNormalTile == true)) {
color: Colors.transparent, return Padding(
child: InkWell( padding: const EdgeInsets.symmetric(horizontal: 12),
onTap: () => Navigator.push(context, MaterialPageRoute( child: Material(
builder: (context) => LogDetailsScreen(log: log) color: Colors.transparent,
)), borderRadius: BorderRadius.circular(28),
onLongPress: () => openOptionsModal(log), child: InkWell(
child: Container( borderRadius: BorderRadius.circular(28),
width: double.maxFinite, onTap: () => onLogTap(log),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Container(
child: Row( width: double.maxFinite,
mainAxisAlignment: MainAxisAlignment.spaceBetween, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
children: [ decoration: BoxDecoration(
SizedBox( borderRadius: BorderRadius.circular(28),
width: width-130, color: isLogSelected == true
child: Column( ? Theme.of(context).colorScheme.primaryContainer
crossAxisAlignment: CrossAxisAlignment.start, : null
children: [ ),
Text( child: Row(
log.question.name, mainAxisAlignment: MainAxisAlignment.spaceBetween,
style: TextStyle( children: [
fontSize: 16, Flexible(
height: 1.5, child: Row(
fontWeight: FontWeight.w400, mainAxisSize: MainAxisSize.min,
color: Theme.of(context).colorScheme.onSurface
),
),
const SizedBox(height: 5),
if (log.client.length <= 15 && appConfigProvider.showNameTimeLogs == false) Row(
children: [ children: [
...[ Flexible(
Icon( child: Column(
Icons.smartphone_rounded, crossAxisAlignment: CrossAxisAlignment.start,
size: 16, children: [
color: Theme.of(context).listTileTheme.textColor, Text(
), log.question.name,
const SizedBox(width: 5), style: TextStyle(
Flexible( fontSize: 16,
child: Text( fontWeight: FontWeight.w400,
log.client, color: Theme.of(context).colorScheme.onSurface,
overflow: TextOverflow.ellipsis, ),
style: TextStyle(
color: Theme.of(context).listTileTheme.textColor,
fontSize: 14,
height: 1.4,
fontWeight: FontWeight.w400,
), ),
), const SizedBox(height: 5),
) if (log.client.length <= 15 && appConfigProvider.showNameTimeLogs == false) Row(
], children: [
const SizedBox(width: 15), ...[
...[ Icon(
Icon( Icons.smartphone_rounded,
Icons.schedule_rounded, size: 16,
size: 16, color: Theme.of(context).listTileTheme.textColor,
color: Theme.of(context).listTileTheme.textColor, ),
), const SizedBox(width: 5),
const SizedBox(width: 5), Flexible(
Flexible( child: Text(
child: Text( log.client,
convertTimestampLocalTimezone(log.time, 'HH:mm:ss'), overflow: TextOverflow.ellipsis,
overflow: TextOverflow.ellipsis, style: TextStyle(
style: TextStyle( color: Theme.of(context).listTileTheme.textColor,
color: Theme.of(context).listTileTheme.textColor, fontSize: 14,
fontSize: 13 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: [ generateLogStatus()
Row( ],
children: [ )
),
),
),
);
}
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( Icon(
Icons.smartphone_rounded, Icons.smartphone_rounded,
size: 16, size: 16,
color: Theme.of(context).listTileTheme.textColor, color: Theme.of(context).listTileTheme.textColor,
), ),
const SizedBox(width: 15), const SizedBox(width: 5),
Flexible( Flexible(
child: Text( child: Text(
log.client, log.client,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
color: Theme.of(context).listTileTheme.textColor, color: Theme.of(context).listTileTheme.textColor,
fontSize: 13 fontSize: 14,
height: 1.4,
fontWeight: FontWeight.w400,
), ),
), ),
) )
], ],
), const SizedBox(width: 15),
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( Icon(
Icons.schedule_rounded, Icons.schedule_rounded,
size: 16, size: 16,
color: Theme.of(context).listTileTheme.textColor, color: Theme.of(context).listTileTheme.textColor,
), ),
const SizedBox(width: 15), const SizedBox(width: 5),
SizedBox( Flexible(
child: Text( child: Text(
convertTimestampLocalTimezone(log.time, 'HH:mm:ss'), convertTimestampLocalTimezone(log.time, 'HH:mm:ss'),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
@ -230,22 +353,23 @@ class LogTile extends StatelessWidget {
fontSize: 13 fontSize: 13
), ),
), ),
) ),
], ],
), ],
if (appConfigProvider.showNameTimeLogs == true && log.elapsedMs != '') ...[ ),
const SizedBox(height: 10), if (log.client.length > 15 || appConfigProvider.showNameTimeLogs == true) Column(
children: [
Row( Row(
children: [ children: [
Icon( Icon(
Icons.timer, Icons.smartphone_rounded,
size: 16, size: 16,
color: Theme.of(context).listTileTheme.textColor, color: Theme.of(context).listTileTheme.textColor,
), ),
const SizedBox(width: 15), const SizedBox(width: 15),
SizedBox( Flexible(
child: Text( child: Text(
"${double.parse(log.elapsedMs).toStringAsFixed(2)} ms", log.client,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
color: Theme.of(context).listTileTheme.textColor, 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),
const SizedBox(width: 10), generateLogStatus()
generateLogStatus() ],
], ),
), ),
), ),
), );
); }
} }
} }

View file

@ -1,5 +1,7 @@
// ignore_for_file: use_build_context_synchronously // ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.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_filters_modal.dart';
import 'package:adguard_home_manager/screens/logs/logs_config_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_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/functions/snackbar.dart';
import 'package:adguard_home_manager/classes/process_modal.dart'; import 'package:adguard_home_manager/classes/process_modal.dart';
@ -64,6 +67,8 @@ class _LogsWidgetState extends State<LogsWidget> {
bool showDivider = true; bool showDivider = true;
Log? selectedLog;
Future fetchLogs({ Future fetchLogs({
int? inOffset, int? inOffset,
bool? loadingMore, bool? loadingMore,
@ -188,6 +193,8 @@ class _LogsWidgetState extends State<LogsWidget> {
final appConfigProvider = Provider.of<AppConfigProvider>(context); final appConfigProvider = Provider.of<AppConfigProvider>(context);
final logsProvider = Provider.of<LogsProvider>(context); final logsProvider = Provider.of<LogsProvider>(context);
final width = MediaQuery.of(context).size.width;
void updateConfig(Map<String, dynamic> data) async { void updateConfig(Map<String, dynamic> data) async {
ProcessModal processModal = ProcessModal(context: context); ProcessModal processModal = ProcessModal(context: context);
processModal.open(AppLocalizations.of(context)!.updatingSettings); processModal.open(AppLocalizations.of(context)!.updatingSettings);
@ -252,12 +259,25 @@ class _LogsWidgetState extends State<LogsWidget> {
void openFilersModal() { void openFilersModal() {
showModalBottomSheet( if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) {
context: context, showDialog(
builder: (context) => const LogsFiltersModal(), context: context,
backgroundColor: Colors.transparent, builder: (context) => const LogsFiltersModal(
isScrollControlled: true dialog: true,
); ),
barrierDismissible: false
);
}
else {
showModalBottomSheet(
context: context,
builder: (context) => const LogsFiltersModal(
dialog: false,
),
backgroundColor: Colors.transparent,
isScrollControlled: true
);
}
} }
final Map<String, String> translatedString = { final Map<String, String> translatedString = {
@ -319,6 +339,18 @@ class _LogsWidgetState extends State<LogsWidget> {
log: logsProvider.logsData!.data[index], log: logsProvider.logsData!.data[index],
index: index, index: index,
length: logsProvider.logsData!.data.length, 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<LogsWidget> {
} }
} }
return Scaffold( Widget logsScreen() {
appBar: AppBar( return Scaffold(
title: Text(AppLocalizations.of(context)!.logs), appBar: AppBar(
centerTitle: false, title: Text(AppLocalizations.of(context)!.logs),
actions: [ centerTitle: false,
logsProvider.loadStatus == 1 actions: [
? IconButton( if (!(Platform.isAndroid || Platform.isIOS)) IconButton(
onPressed: openFilersModal, onPressed: () => fetchLogs(inOffset: 0),
icon: const Icon(Icons.filter_list_rounded) icon: const Icon(Icons.refresh_rounded),
) tooltip: AppLocalizations.of(context)!.refresh,
: const SizedBox(), ),
IconButton( logsProvider.loadStatus == 1
onPressed: () => { ? IconButton(
showModalBottomSheet( onPressed: openFilersModal,
context: context, icon: const Icon(Icons.filter_list_rounded),
builder: (context) => LogsConfigModal( tooltip: AppLocalizations.of(context)!.filters,
onConfirm: updateConfig, )
onClear: clearQueries, : const SizedBox(),
), IconButton(
backgroundColor: Colors.transparent, tooltip: AppLocalizations.of(context)!.settings,
isScrollControlled: true onPressed: () => {
) if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) {
}, showDialog(
icon: const Icon(Icons.settings) context: context,
), builder: (context) => LogsConfigModal(
const SizedBox(width: 5), onConfirm: updateConfig,
], onClear: clearQueries,
bottom: logsProvider.appliedFilters.searchText != null || logsProvider.appliedFilters.selectedResultStatus != 'all' || logsProvider.appliedFilters.clients != null dialog: true,
? PreferredSize( ),
preferredSize: const Size(double.maxFinite, 50), barrierDismissible: false
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( else {
scrollDirection: Axis.horizontal, showModalBottomSheet(
children: [ context: context,
if (logsProvider.appliedFilters.searchText != null) ...[ 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), 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();
}
} }
} }

View file

@ -12,11 +12,13 @@ import 'package:adguard_home_manager/providers/servers_provider.dart';
class LogsConfigModal extends StatelessWidget { class LogsConfigModal extends StatelessWidget {
final void Function(Map<String, dynamic>) onConfirm; final void Function(Map<String, dynamic>) onConfirm;
final void Function() onClear; final void Function() onClear;
final bool dialog;
const LogsConfigModal({ const LogsConfigModal({
Key? key, Key? key,
required this.onConfirm, required this.onConfirm,
required this.onClear, required this.onClear,
required this.dialog
}) : super(key: key); }) : super(key: key);
@override @override
@ -30,6 +32,7 @@ class LogsConfigModal extends StatelessWidget {
context: context, context: context,
onConfirm: onConfirm, onConfirm: onConfirm,
onClear: onClear, onClear: onClear,
dialog: dialog,
); );
} }
} }
@ -40,6 +43,7 @@ class LogsConfigModalWidget extends StatefulWidget {
final BuildContext context; final BuildContext context;
final void Function(Map<String, dynamic>) onConfirm; final void Function(Map<String, dynamic>) onConfirm;
final void Function() onClear; final void Function() onClear;
final bool dialog;
const LogsConfigModalWidget({ const LogsConfigModalWidget({
Key? key, Key? key,
@ -48,6 +52,7 @@ class LogsConfigModalWidget extends StatefulWidget {
required this.context, required this.context,
required this.onConfirm, required this.onConfirm,
required this.onClear, required this.onClear,
required this.dialog
}) : super(key: key); }) : super(key: key);
@override @override
@ -146,125 +151,154 @@ class _LogsConfigModalWidgetState extends State<LogsConfigModalWidget> {
Widget generateBody() { Widget generateBody() {
switch (loadStatus) { switch (loadStatus) {
case 0: case 0:
return const Center( return Padding(
child: CircularProgressIndicator(), 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: case 1:
return Column( return Column(
mainAxisSize: MainAxisSize.min,
children: [ children: [
Expanded( Flexible(
child: ListView( child: SingleChildScrollView(
physics: (Platform.isIOS ? 436 : 420) < MediaQuery.of(context).size.height child: Wrap(
? const NeverScrollableScrollPhysics() children: [
: null, Row(
children: [ mainAxisAlignment: MainAxisAlignment.center,
Padding( children: [
padding: const EdgeInsets.only(top: 24), Column(
child: Icon( children: [
Icons.settings, Padding(
size: 24, padding: const EdgeInsets.only(top: 24),
color: Theme.of(context).listTileTheme.iconColor child: Icon(
), Icons.settings,
), size: 24,
const SizedBox(height: 16), color: Theme.of(context).listTileTheme.iconColor
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, const SizedBox(height: 16),
onChanged: (value) => setState(() => generalSwitch = value), 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),
)
],
),
), ),
), ),
), ),
), ),
), Container(height: 16),
const SizedBox(height: 16), Padding(
Padding( padding: const EdgeInsets.symmetric(horizontal: 14),
padding: const EdgeInsets.symmetric(horizontal: 14), child: Column(
child: Column( children: [
children: [ Material(
Material( color: Colors.transparent,
color: Colors.transparent, child: InkWell(
child: InkWell( onTap: () => setState(() => anonymizeClientIp = !anonymizeClientIp),
onTap: () => setState(() => anonymizeClientIp = !anonymizeClientIp), child: Padding(
child: Padding( padding: const EdgeInsets.symmetric(horizontal: 30),
padding: const EdgeInsets.symmetric(horizontal: 30), child: Row(
child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
children: [ Text(
Text( AppLocalizations.of(context)!.anonymizeClientIp,
AppLocalizations.of(context)!.anonymizeClientIp, style: const TextStyle(
style: const TextStyle( fontSize: 16
fontSize: 16 ),
), ),
), Switch(
Switch( value: anonymizeClientIp,
value: anonymizeClientIp, onChanged: (value) => setState(() => anonymizeClientIp = value),
onChanged: (value) => setState(() => anonymizeClientIp = value), )
],
),
),
),
),
Container(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: DropdownButtonFormField(
items: retentionItems.map<DropdownMenuItem<String>>((Map<String, dynamic> item) {
return DropdownMenuItem<String>(
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<DropdownMenuItem<String>>((Map<String, dynamic> item) {
return DropdownMenuItem<String>(
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( Padding(
@ -316,31 +350,29 @@ class _LogsConfigModalWidgetState extends State<LogsConfigModalWidget> {
); );
case 2: case 2:
return SizedBox( return Column(
width: double.maxFinite, mainAxisAlignment: MainAxisAlignment.center,
child: Column( crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center, children: [
children: [ const Icon(
const Icon( Icons.error,
Icons.error, color: Colors.red,
color: Colors.red, size: 50,
size: 50, ),
), const SizedBox(height: 30),
const SizedBox(height: 30), Padding(
Padding( padding: const EdgeInsets.symmetric(horizontal: 20),
padding: const EdgeInsets.symmetric(horizontal: 20), child: Text(
child: Text( AppLocalizations.of(context)!.logSettingsNotLoaded,
AppLocalizations.of(context)!.logSettingsNotLoaded, textAlign: TextAlign.center,
textAlign: TextAlign.center, style: TextStyle(
style: const TextStyle( fontSize: 22,
fontSize: 22, color: Theme.of(context).colorScheme.onSurfaceVariant
color: Colors.grey,
),
), ),
) ),
], )
), ],
); );
default: default:
@ -348,16 +380,28 @@ class _LogsConfigModalWidgetState extends State<LogsConfigModalWidget> {
} }
} }
return Container( if (widget.dialog == true) {
height: Platform.isIOS ? 436 : 420, return Dialog(
decoration: BoxDecoration( child: ConstrainedBox(
borderRadius: const BorderRadius.only( constraints: const BoxConstraints(
topLeft: Radius.circular(28), maxWidth: 500
topRight: Radius.circular(28) ),
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()
);
}
} }
} }

View file

@ -18,24 +18,32 @@ import 'package:adguard_home_manager/models/applied_filters.dart';
import 'package:adguard_home_manager/providers/logs_provider.dart'; import 'package:adguard_home_manager/providers/logs_provider.dart';
class LogsFiltersModal extends StatelessWidget { 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final logsProvider = Provider.of<LogsProvider>(context); final logsProvider = Provider.of<LogsProvider>(context);
return LogsFiltersModalWidget( return LogsFiltersModalWidget(
logsProvider: logsProvider logsProvider: logsProvider,
dialog: dialog,
); );
} }
} }
class LogsFiltersModalWidget extends StatefulWidget { class LogsFiltersModalWidget extends StatefulWidget {
final LogsProvider logsProvider; final LogsProvider logsProvider;
final bool dialog;
const LogsFiltersModalWidget({ const LogsFiltersModalWidget({
Key? key, Key? key,
required this.logsProvider required this.logsProvider,
required this.dialog
}) : super(key: key); }) : super(key: key);
@override @override
@ -57,6 +65,8 @@ class _LogsFiltersModalWidgetState extends State<LogsFiltersModalWidget> {
final serversProvider = Provider.of<ServersProvider>(context); final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context); final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
final Map<String, String> translatedString = { final Map<String, String> translatedString = {
"all": AppLocalizations.of(context)!.all, "all": AppLocalizations.of(context)!.all,
"filtered": AppLocalizations.of(context)!.filtered, "filtered": AppLocalizations.of(context)!.filtered,
@ -101,25 +111,51 @@ class _LogsFiltersModalWidgetState extends State<LogsFiltersModalWidget> {
} }
void openSelectFilterStatus() { void openSelectFilterStatus() {
showModalBottomSheet( if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) {
context: context, showDialog(
builder: (context) => FilterStatusModal( barrierDismissible: false,
value: logsProvider.selectedResultStatus, context: context,
), builder: (context) => FilterStatusModal(
isScrollControlled: true, value: logsProvider.selectedResultStatus,
backgroundColor: Colors.transparent dialog: true,
); ),
);
}
else {
showModalBottomSheet(
context: context,
builder: (context) => FilterStatusModal(
value: logsProvider.selectedResultStatus,
dialog: false,
),
isScrollControlled: true,
backgroundColor: Colors.transparent
);
}
} }
void openSelectClients() { void openSelectClients() {
showModalBottomSheet( if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) {
context: context, showDialog(
builder: (context) => ClientsModal( context: context,
value: logsProvider.selectedClients, builder: (context) => ClientsModal(
), value: logsProvider.selectedClients,
isScrollControlled: true, dialog: true,
backgroundColor: Colors.transparent ),
); barrierDismissible: false
);
}
else {
showModalBottomSheet(
context: context,
builder: (context) => ClientsModal(
value: logsProvider.selectedClients,
dialog: false,
),
isScrollControlled: true,
backgroundColor: Colors.transparent
);
}
} }
void filterLogs() async { void filterLogs() async {
@ -161,47 +197,45 @@ class _LogsFiltersModalWidgetState extends State<LogsFiltersModalWidget> {
} }
} }
return Padding( Widget content() {
padding: MediaQuery.of(context).viewInsets, return Column(
child: Container( mainAxisSize: MainAxisSize.min,
height: Platform.isIOS ? 446 : 430, children: [
decoration: BoxDecoration( Flexible(
color: Theme.of(context).dialogBackgroundColor, child: SingleChildScrollView(
borderRadius: const BorderRadius.only( child: Wrap(
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,
children: [ children: [
Padding( Row(
padding: const EdgeInsets.only( mainAxisAlignment: MainAxisAlignment.center,
top: 24, children: [
bottom: 16, Column(
), children: [
child: Icon( Padding(
Icons.filter_list_rounded, padding: const EdgeInsets.only(
size: 24, top: 24,
color: Theme.of(context).listTileTheme.iconColor 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(
padding: const EdgeInsets.symmetric(horizontal: 24), padding: const EdgeInsets.symmetric(horizontal: 24),
child: Row( child: Row(
@ -233,7 +267,7 @@ class _LogsFiltersModalWidgetState extends State<LogsFiltersModalWidget> {
], ],
), ),
), ),
const SizedBox(height: 16), Container(height: 16),
CustomListTile( CustomListTile(
title: AppLocalizations.of(context)!.client, title: AppLocalizations.of(context)!.client,
subtitle: logsProvider.selectedClients != null subtitle: logsProvider.selectedClients != null
@ -270,26 +304,55 @@ class _LogsFiltersModalWidgetState extends State<LogsFiltersModalWidget> {
], ],
), ),
), ),
Padding( ),
padding: const EdgeInsets.all(24), Padding(
child: Row( padding: const EdgeInsets.all(24),
mainAxisAlignment: MainAxisAlignment.spaceBetween, child: Row(
children: [ mainAxisAlignment: MainAxisAlignment.spaceBetween,
TextButton( children: [
onPressed: resetFilters, TextButton(
child: Text(AppLocalizations.of(context)!.resetFilters) onPressed: resetFilters,
), child: Text(AppLocalizations.of(context)!.resetFilters)
TextButton( ),
onPressed: () => filterLogs(), TextButton(
child: Text(AppLocalizations.of(context)!.apply) 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()
),
);
}
} }
} }

View file

@ -13,7 +13,12 @@ import 'package:adguard_home_manager/providers/servers_provider.dart';
import 'package:adguard_home_manager/providers/app_config_provider.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart';
class Servers extends StatefulWidget { class Servers extends StatefulWidget {
const Servers({Key? key}) : super(key: key); final double? breakingWidth;
const Servers({
Key? key,
this.breakingWidth
}) : super(key: key);
@override @override
State<Servers> createState() => _ServersState(); State<Servers> createState() => _ServersState();
@ -55,16 +60,31 @@ class _ServersState extends State<Servers> {
final serversProvider = Provider.of<ServersProvider>(context); final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context); final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
for (var i = 0; i < serversProvider.serversList.length; i++) { for (var i = 0; i < serversProvider.serversList.length; i++) {
expandableControllerList.add(ExpandableController()); expandableControllerList.add(ExpandableController());
} }
void openAddServerModal() async { void openAddServerModal() async {
await Future.delayed(const Duration(seconds: 0), (() => { await Future.delayed(const Duration(seconds: 0), (() => {
Navigator.push(context, MaterialPageRoute( if (width > 700) {
fullscreenDialog: true, showDialog(
builder: (BuildContext context) => const AddServerModal() 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<Servers> {
context: context, context: context,
controllers: expandableControllerList, controllers: expandableControllerList,
onChange: expandOrContract, onChange: expandOrContract,
scrollController: scrollController scrollController: scrollController,
breakingWidth: widget.breakingWidth ?? 700,
), ),
AnimatedPositioned( AnimatedPositioned(
duration: const Duration(milliseconds: 100), duration: const Duration(milliseconds: 100),

View file

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -72,78 +74,114 @@ class _AccessSettingsWidgetState extends State<AccessSettingsWidget> with Ticker
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final serversProvider = Provider.of<ServersProvider>(context); final serversProvider = Provider.of<ServersProvider>(context);
return Scaffold(
body: DefaultTabController( Widget body() {
length: 3, return TabBarView(
child: NestedScrollView( controller: tabController,
controller: scrollController, children: [
headerSliverBuilder: ((context, innerBoxIsScrolled) { ClientsList(
return [ type: 'allowed',
SliverOverlapAbsorber( scrollController: scrollController,
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), loadStatus: serversProvider.clients.loadStatus,
sliver: SliverSafeArea( data: serversProvider.clients.loadStatus == LoadStatus.loaded
top: false, ? serversProvider.clients.data!.clientsAllowedBlocked!.allowedClients : [],
sliver: SliverAppBar( fetchClients: fetchClients
title: Text(AppLocalizations.of(context)!.accessSettings), ),
pinned: true, ClientsList(
floating: true, type: 'disallowed',
centerTitle: false, scrollController: scrollController,
forceElevated: innerBoxIsScrolled, loadStatus: serversProvider.clients.loadStatus,
bottom: TabBar( data: serversProvider.clients.loadStatus == LoadStatus.loaded
controller: tabController, ? serversProvider.clients.data!.clientsAllowedBlocked!.disallowedClients : [],
isScrollable: true, fetchClients: fetchClients
unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant, ),
tabs: [ ClientsList(
Tab( type: 'domains',
icon: const Icon(Icons.check), scrollController: scrollController,
text: AppLocalizations.of(context)!.allowedClients, loadStatus: serversProvider.clients.loadStatus,
), data: serversProvider.clients.loadStatus == LoadStatus.loaded
Tab( ? serversProvider.clients.data!.clientsAllowedBlocked!.blockedHosts : [],
icon: const Icon(Icons.block), fetchClients: fetchClients
text: AppLocalizations.of(context)!.disallowedClients, ),
), ]
Tab( );
icon: const Icon(Icons.link_rounded), }
text: AppLocalizations.of(context)!.disallowedDomains,
), 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: body()
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
),
]
) )
) ),
), );
); }
else {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.accessSettings),
centerTitle: false,
bottom: tabBar()
),
body: body(),
);
}
} }
} }

View file

@ -6,11 +6,13 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class AddClientModal extends StatefulWidget { class AddClientModal extends StatefulWidget {
final String type; final String type;
final void Function(String, String) onConfirm; final void Function(String, String) onConfirm;
final bool dialog;
const AddClientModal({ const AddClientModal({
Key? key, Key? key,
required this.type, required this.type,
required this.onConfirm required this.onConfirm,
required this.dialog,
}) : super(key: key); }) : super(key: key);
@override @override
@ -65,59 +67,61 @@ class _AddClientModalState extends State<AddClientModal> {
} }
} }
return Padding( Widget content() {
padding: MediaQuery.of(context).viewInsets, return Padding(
child: Container(
height: Platform.isIOS ? 321 : 305,
padding: const EdgeInsets.all(24), 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( child: Column(
mainAxisSize: MainAxisSize.min,
children: [ children: [
Expanded( Flexible(
child: ListView( child: SingleChildScrollView(
physics: (Platform.isIOS ? 338 : 322) < MediaQuery.of(context).size.height child: Wrap(
? const NeverScrollableScrollPhysics() children: [
: null, Row(
children: [ mainAxisAlignment: MainAxisAlignment.center,
Icon( children: [
icon(), Icon(
size: 24, icon(),
color: Theme.of(context).listTileTheme.iconColor 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
), ),
), Padding(
const SizedBox(height: 16), padding: const EdgeInsets.symmetric(vertical: 16),
TextFormField( child: Row(
controller: fieldController, mainAxisAlignment: MainAxisAlignment.center,
onChanged: (_) => checkValidValues(), children: [
decoration: InputDecoration( Text(
prefixIcon: const Icon(Icons.link_rounded), title(),
border: const OutlineInputBorder( textAlign: TextAlign.center,
borderRadius: BorderRadius.all( style: TextStyle(
Radius.circular(10) 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( Padding(
@ -129,7 +133,7 @@ class _AddClientModalState extends State<AddClientModal> {
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: Text(AppLocalizations.of(context)!.cancel) child: Text(AppLocalizations.of(context)!.cancel)
), ),
const SizedBox(width: 20), const SizedBox(width: 16),
TextButton( TextButton(
onPressed: validData == true onPressed: validData == true
? () { ? () {
@ -149,10 +153,38 @@ class _AddClientModalState extends State<AddClientModal> {
], ],
), ),
), ),
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()
),
);
}
} }
} }

View file

@ -1,5 +1,7 @@
// ignore_for_file: use_build_context_synchronously // ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -68,6 +70,8 @@ class _ClientsListState extends State<ClientsList> {
final serversProvider = Provider.of<ServersProvider>(context); final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context); final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
void confirmRemoveItem(String client, String type) async { void confirmRemoveItem(String client, String type) async {
Map<String, List<String>> body = { Map<String, List<String>> body = {
"allowed_clients": serversProvider.clients.data!.clientsAllowedBlocked?.allowedClients ?? [], "allowed_clients": serversProvider.clients.data!.clientsAllowedBlocked?.allowedClients ?? [],
@ -209,6 +213,7 @@ class _ClientsListState extends State<ClientsList> {
} }
return CustomTabContentList( return CustomTabContentList(
noSliver: !(Platform.isAndroid || Platform.isIOS) ? true : false,
loadingGenerator: () => SizedBox( loadingGenerator: () => SizedBox(
width: double.maxFinite, width: double.maxFinite,
height: MediaQuery.of(context).size.height-171, height: MediaQuery.of(context).size.height-171,
@ -362,15 +367,28 @@ class _ClientsListState extends State<ClientsList> {
refreshIndicatorOffset: 0, refreshIndicatorOffset: 0,
fab: FloatingActionButton( fab: FloatingActionButton(
onPressed: () { onPressed: () {
showModalBottomSheet( if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) {
context: context, showDialog(
builder: (context) => AddClientModal( context: context,
type: widget.type, builder: (context) => AddClientModal(
onConfirm: confirmAddItem type: widget.type,
), onConfirm: confirmAddItem,
backgroundColor: Colors.transparent, dialog: true,
isScrollControlled: 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), child: const Icon(Icons.add),
), ),

View file

@ -1,6 +1,9 @@
// ignore_for_file: use_build_context_synchronously // ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_split_view/flutter_split_view.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -15,6 +18,8 @@ class AdvancedSettings extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final appConfigProvider = Provider.of<AppConfigProvider>(context); final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
Future updateSslCheck(bool newStatus) async { Future updateSslCheck(bool newStatus) async {
final result = await appConfigProvider.setOverrideSslCheck(newStatus); final result = await appConfigProvider.setOverrideSslCheck(newStatus);
@ -64,11 +69,16 @@ class AdvancedSettings extends StatelessWidget {
title: AppLocalizations.of(context)!.logs, title: AppLocalizations.of(context)!.logs,
subtitle: AppLocalizations.of(context)!.checkAppLogs, subtitle: AppLocalizations.of(context)!.checkAppLogs,
onTap: () => { onTap: () => {
Navigator.of(context).push( if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) {
MaterialPageRoute( SplitView.of(context).push(const AppLogs())
builder: (context) => const AppLogs() }
else {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const AppLogs()
)
) )
) }
}, },
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
top: 10, top: 10,

View file

@ -5,10 +5,12 @@ import 'package:adguard_home_manager/models/dhcp.dart';
class AddStaticLeaseModal extends StatefulWidget { class AddStaticLeaseModal extends StatefulWidget {
final void Function(Lease) onConfirm; final void Function(Lease) onConfirm;
final bool dialog;
const AddStaticLeaseModal({ const AddStaticLeaseModal({
Key? key, Key? key,
required this.onConfirm, required this.onConfirm,
required this.dialog
}) : super(key: key); }) : super(key: key);
@override @override
@ -65,45 +67,47 @@ class _AddStaticLeaseModalState extends State<AddStaticLeaseModal> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( Widget content() {
padding: MediaQuery.of(context).viewInsets, return Column(
child: Container( mainAxisSize: MainAxisSize.min,
height: 510, children: [
decoration: BoxDecoration( Flexible(
color: Theme.of(context).dialogBackgroundColor, child: SingleChildScrollView(
borderRadius: const BorderRadius.only( child: Wrap(
topLeft: Radius.circular(28),
topRight: Radius.circular(28)
)
),
child: Column(
children: [
Expanded(
child: ListView(
physics: 550 < MediaQuery.of(context).size.height
? const NeverScrollableScrollPhysics()
: null,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.only(top: 24), padding: const EdgeInsets.only(bottom: 16),
child: Icon( child: Row(
Icons.add, mainAxisAlignment: MainAxisAlignment.center,
size: 24, children: [
color: Theme.of(context).listTileTheme.iconColor 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(
padding: const EdgeInsets.symmetric(horizontal: 28), padding: const EdgeInsets.only(
left: 24, right: 24, bottom: 12
),
child: TextFormField( child: TextFormField(
controller: macController, controller: macController,
onChanged: validateMac, onChanged: validateMac,
@ -119,9 +123,8 @@ class _AddStaticLeaseModalState extends State<AddStaticLeaseModal> {
), ),
), ),
), ),
const SizedBox(height: 30),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 28), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
child: TextFormField( child: TextFormField(
controller: ipController, controller: ipController,
onChanged: validateIp, onChanged: validateIp,
@ -137,9 +140,10 @@ class _AddStaticLeaseModalState extends State<AddStaticLeaseModal> {
), ),
), ),
), ),
const SizedBox(height: 30),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 28), padding: const EdgeInsets.only(
left: 24, right: 24, top: 12
),
child: TextFormField( child: TextFormField(
controller: hostNameController, controller: hostNameController,
onChanged: (value) { onChanged: (value) {
@ -166,44 +170,70 @@ class _AddStaticLeaseModalState extends State<AddStaticLeaseModal> {
], ],
), ),
), ),
Padding( ),
padding: const EdgeInsets.all(20), Padding(
child: Row( padding: const EdgeInsets.all(24),
mainAxisAlignment: MainAxisAlignment.end, child: Row(
children: [ mainAxisAlignment: MainAxisAlignment.end,
TextButton( children: [
onPressed: () => Navigator.pop(context), TextButton(
child: Text(AppLocalizations.of(context)!.cancel), onPressed: () => Navigator.pop(context),
), child: Text(AppLocalizations.of(context)!.cancel),
const SizedBox(width: 20), ),
TextButton( const SizedBox(width: 20),
onPressed: validData == true TextButton(
? () { onPressed: validData == true
Navigator.pop(context); ? () {
widget.onConfirm( Navigator.pop(context);
Lease( widget.onConfirm(
mac: macController.text, Lease(
hostname: hostNameController.text, mac: macController.text,
ip: ipController.text hostname: hostNameController.text,
) ip: ipController.text
); )
} );
: null, }
child: Text( : null,
AppLocalizations.of(context)!.confirm, child: Text(
style: TextStyle( AppLocalizations.of(context)!.confirm,
color: validData == true style: TextStyle(
? Theme.of(context).colorScheme.primary color: validData == true
: Colors.grey ? 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()
),
);
}
} }
} }

View file

@ -1,8 +1,10 @@
// ignore_for_file: use_build_context_synchronously // ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_split_view/flutter_split_view.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:bottom_sheet/bottom_sheet.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/widgets/section_label.dart'; import 'package:adguard_home_manager/widgets/section_label.dart';
@ -204,7 +206,7 @@ class _DhcpWidgetState extends State<DhcpWidget> {
@override @override
void initState() { void initState() {
loadDhcpStatus(); if (mounted) loadDhcpStatus();
super.initState(); super.initState();
} }
@ -213,6 +215,8 @@ class _DhcpWidgetState extends State<DhcpWidget> {
final serversProvider = Provider.of<ServersProvider>(context); final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context); final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
void saveSettings() async { void saveSettings() async {
ProcessModal processModal = ProcessModal(context: context); ProcessModal processModal = ProcessModal(context: context);
processModal.open(AppLocalizations.of(context)!.savingSettings); processModal.open(AppLocalizations.of(context)!.savingSettings);
@ -354,24 +358,33 @@ class _DhcpWidgetState extends State<DhcpWidget> {
void selectInterface() { void selectInterface() {
ScaffoldMessenger.of(context).clearSnackBars(); ScaffoldMessenger.of(context).clearSnackBars();
Future.delayed(const Duration(seconds: 0), () { Future.delayed(const Duration(seconds: 0), () {
showFlexibleBottomSheet( if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) {
minHeight: 0.6, showDialog(
initHeight: 0.6, context: context,
maxHeight: 0.95, builder: (context) => SelectInterfaceModal(
isCollapsible: true, interfaces: serversProvider.dhcp.data!.networkInterfaces,
duration: const Duration(milliseconds: 250), onSelect: (interface) => setState(() {
anchors: [0.95], clearAll();
context: context, selectedInterface = interface;
builder: (ctx, controller, offset) => SelectInterfaceModal( }),
interfaces: serversProvider.dhcp.data!.networkInterfaces, dialog: true,
scrollController: controller, )
onSelect: (interface) => setState(() { );
clearAll(); }
selectedInterface = interface; else {
}) showModalBottomSheet(
), context: context,
bottomSheetColor: Colors.transparent 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<DhcpWidget> {
case 1: case 1:
if (selectedInterface != null) { if (selectedInterface != null) {
return ListView( return SingleChildScrollView(
children: [ child: Wrap(
Padding( children: [
padding: const EdgeInsets.only( Padding(
top: 10, padding: const EdgeInsets.only(
left: 16, top: 10,
right: 16 left: 16,
), right: 16
child: Material( ),
color: Theme.of(context).colorScheme.primary.withOpacity(0.1), child: Material(
borderRadius: BorderRadius.circular(28), color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
child: InkWell(
onTap: selectedInterface != null
? () => setState(() => enabled = !enabled)
: null,
borderRadius: BorderRadius.circular(28), borderRadius: BorderRadius.circular(28),
child: Padding( child: InkWell(
padding: const EdgeInsets.symmetric( onTap: selectedInterface != null
horizontal: 20, ? () => setState(() => enabled = !enabled)
vertical: 12 : 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( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Column( Text(
crossAxisAlignment: CrossAxisAlignment.start, AppLocalizations.of(context)!.dhcpLeases,
children: [ textAlign: TextAlign.center,
Text( style: TextStyle(
AppLocalizations.of(context)!.enableDhcpServer, fontSize: 16,
style: TextStyle( color: Theme.of(context).colorScheme.onSurface,
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,
), ),
Icon(
Icons.arrow_forward_rounded,
color: Theme.of(context).colorScheme.onSurface,
)
], ],
), ),
), ),
), ),
), ),
), if (width <= 900) Material(
if (selectedInterface!.ipv4Addresses.isNotEmpty) ...[ color: Colors.transparent,
SectionLabel( child: InkWell(
label: AppLocalizations.of(context)!.ipv4settings, onTap: () {
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), Navigator.push(context, MaterialPageRoute(
), builder: (context) => DhcpLeases(
Padding( items: serversProvider.dhcp.data!.dhcpStatus.staticLeases,
padding: const EdgeInsets.symmetric(horizontal: 16), staticLeases: true,
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,
),
),
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( child: Container(
prefixIcon: const Icon(Icons.timer), padding: const EdgeInsets.all(16),
border: const OutlineInputBorder( child: Row(
borderRadius: BorderRadius.all( mainAxisAlignment: MainAxisAlignment.spaceBetween,
Radius.circular(10) children: [
) Text(
), AppLocalizations.of(context)!.dhcpStatic,
errorText: ipv4LeaseTimeError, textAlign: TextAlign.center,
labelText: AppLocalizations.of(context)!.leaseTime, style: TextStyle(
), fontSize: 16,
keyboardType: TextInputType.number, color: Theme.of(context).colorScheme.onSurface,
), ),
),
],
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,
), ),
), Icon(
Icon( Icons.arrow_forward_rounded,
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,
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
), )
), ],
Icon( ),
Icons.arrow_forward_rounded,
color: Theme.of(context).colorScheme.onSurface,
)
],
), ),
), ),
), ),
), if (width > 900) Row(
const SizedBox(height: 10) 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 { else {
return Column( return Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Padding( Flexible(
padding: const EdgeInsets.symmetric(horizontal: 20), child: Column(
child: Text( mainAxisSize: MainAxisSize.max,
AppLocalizations.of(context)!.neededSelectInterface, mainAxisAlignment: MainAxisAlignment.center,
textAlign: TextAlign.center, crossAxisAlignment: CrossAxisAlignment.center,
style: TextStyle( children: [
fontSize: 22, Padding(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5) 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)
),
], ],
); );
} }

View file

@ -1,5 +1,7 @@
// ignore_for_file: use_build_context_synchronously // ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:animations/animations.dart'; import 'package:animations/animations.dart';
@ -30,6 +32,8 @@ class DhcpLeases extends StatelessWidget {
final serversProvider = Provider.of<ServersProvider>(context); final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context); final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
void deleteLease(Lease lease) async { void deleteLease(Lease lease) async {
ProcessModal processModal = ProcessModal(context: context); ProcessModal processModal = ProcessModal(context: context);
processModal.open(AppLocalizations.of(context)!.deleting); processModal.open(AppLocalizations.of(context)!.deleting);
@ -119,14 +123,26 @@ class DhcpLeases extends StatelessWidget {
} }
void openAddStaticLease() { void openAddStaticLease() {
showModalBottomSheet( if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) {
context: context, showDialog(
builder: (context) => AddStaticLeaseModal( context: context,
onConfirm: createLease builder: (context) => AddStaticLeaseModal(
), onConfirm: createLease,
backgroundColor: Colors.transparent, dialog: true,
isScrollControlled: true ),
); );
}
else {
showModalBottomSheet(
context: context,
builder: (context) => AddStaticLeaseModal(
onConfirm: createLease,
dialog: false,
),
backgroundColor: Colors.transparent,
isScrollControlled: true
);
}
} }
return Scaffold( return Scaffold(

View file

@ -7,173 +7,177 @@ import 'package:adguard_home_manager/models/dhcp.dart';
class SelectInterfaceModal extends StatelessWidget { class SelectInterfaceModal extends StatelessWidget {
final List<NetworkInterface> interfaces; final List<NetworkInterface> interfaces;
final ScrollController scrollController;
final void Function(NetworkInterface) onSelect; final void Function(NetworkInterface) onSelect;
final bool dialog;
const SelectInterfaceModal({ const SelectInterfaceModal({
Key? key, Key? key,
required this.interfaces, required this.interfaces,
required this.scrollController,
required this.onSelect, required this.onSelect,
required this.dialog
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( Widget content() {
decoration: BoxDecoration( return Column(
color: Theme.of(context).dialogBackgroundColor, mainAxisSize: MainAxisSize.min,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28),
topRight: Radius.circular(28)
)
),
child: Column(
children: [ children: [
Expanded( Flexible(
child: ListView( child: SingleChildScrollView(
controller: scrollController, child: Wrap(
children: [ children: [
Padding( Row(
padding: const EdgeInsets.only(top: 24), mainAxisAlignment: MainAxisAlignment.center,
child: Icon( children: [
Icons.settings_ethernet_rounded, Column(
size: 24, children: [
color: Theme.of(context).listTileTheme.iconColor Padding(
), padding: const EdgeInsets.only(top: 24),
), child: Icon(
const SizedBox(height: 16), Icons.settings_ethernet_rounded,
Text( size: 24,
AppLocalizations.of(context)!.selectInterface, color: Theme.of(context).listTileTheme.iconColor
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
),
), ),
Row( ),
children: [ const SizedBox(height: 16),
Text( Text(
"${AppLocalizations.of(context)!.hardwareAddress}: ", AppLocalizations.of(context)!.selectInterface,
style: TextStyle( textAlign: TextAlign.center,
fontSize: 14, style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant 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, Row(
style: TextStyle( children: [
fontSize: 14, Text(
color: Theme.of(context).colorScheme.onSurfaceVariant "${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),
], ],
), if (interfaces[index].gatewayIp != '') ...[
const SizedBox(height: 5), Row(
if (interfaces[index].flags.isNotEmpty) ...[ children: [
Row( Text(
children: [ "${AppLocalizations.of(context)!.gatewayIp}: ",
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(', ')}",
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant color: Theme.of(context).colorScheme.onSurfaceVariant
), ),
), ),
) Text(
], interfaces[index].gatewayIp,
),
const SizedBox(height: 5),
],
if (interfaces[index].ipv6Addresses.isNotEmpty) ...[
Row(
children: [
Flexible(
child: Text(
"${AppLocalizations.of(context)!.ipv6addresses}: ${interfaces[index].ipv6Addresses.join(', ')}",
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant 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( Padding(
@ -190,7 +194,30 @@ class SelectInterfaceModal extends StatelessWidget {
), ),
if (Platform.isIOS) const SizedBox(height: 16) 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()
);
}
} }
} }

View file

@ -182,8 +182,7 @@ class _BootstrapDnsScreenState extends State<BootstrapDnsScreen> {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
SizedBox( Expanded(
width: MediaQuery.of(context).size.width-74,
child: TextFormField( child: TextFormField(
controller: c['controller'], controller: c['controller'],
onChanged: (value) => validateIp(c, value), onChanged: (value) => validateIp(c, value),
@ -199,6 +198,7 @@ class _BootstrapDnsScreenState extends State<BootstrapDnsScreen> {
) )
), ),
), ),
const SizedBox(width: 8),
IconButton( IconButton(
onPressed: () { onPressed: () {
setState(() => bootstrapControllers = bootstrapControllers.where((con) => con != c).toList()); setState(() => bootstrapControllers = bootstrapControllers.where((con) => con != c).toList());

View file

@ -257,7 +257,8 @@ class _CacheConfigDnsScreenState extends State<CacheConfigDnsScreen> {
label: Text(AppLocalizations.of(context)!.clearDnsCache), label: Text(AppLocalizations.of(context)!.clearDnsCache),
), ),
], ],
) ),
const SizedBox(height: 16)
], ],
), ),
); );

View file

@ -4,11 +4,13 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class CommentModal extends StatefulWidget { class CommentModal extends StatefulWidget {
final String? comment; final String? comment;
final void Function(String) onConfirm; final void Function(String) onConfirm;
final bool dialog;
const CommentModal({ const CommentModal({
Key? key, Key? key,
this.comment, this.comment,
required this.onConfirm required this.onConfirm,
required this.dialog
}) : super(key: key); }) : super(key: key);
@override @override
@ -30,43 +32,41 @@ class _CommentModalState extends State<CommentModal> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( Widget content() {
padding: MediaQuery.of(context).viewInsets, return Column(
child: Container( mainAxisSize: MainAxisSize.min,
height: 310, children: [
decoration: BoxDecoration( Flexible(
borderRadius: const BorderRadius.only( child: SingleChildScrollView(
topLeft: Radius.circular(28), child: Wrap(
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,
children: [ children: [
Padding( Row(
padding: const EdgeInsets.only(top: 24), mainAxisAlignment: MainAxisAlignment.center,
child: Icon( children: [
Icons.comment_rounded, Column(
size: 24, children: [
color: Theme.of(context).colorScheme.secondary, 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(
padding: const EdgeInsets.symmetric(horizontal: 24), padding: const EdgeInsets.symmetric(horizontal: 24),
child: TextFormField( child: TextFormField(
@ -95,38 +95,64 @@ class _CommentModalState extends State<CommentModal> {
], ],
), ),
), ),
Padding( ),
padding: const EdgeInsets.all(24), Padding(
child: Row( padding: const EdgeInsets.all(24),
mainAxisAlignment: MainAxisAlignment.end, child: Row(
children: [ mainAxisAlignment: MainAxisAlignment.end,
TextButton( children: [
onPressed: () => Navigator.pop(context), TextButton(
child: Text(AppLocalizations.of(context)!.cancel) onPressed: () => Navigator.pop(context),
), child: Text(AppLocalizations.of(context)!.cancel)
const SizedBox(width: 20), ),
TextButton( const SizedBox(width: 20),
onPressed: validData == true TextButton(
? () { onPressed: validData == true
Navigator.pop(context); ? () {
widget.onConfirm("# ${commentController.text}"); Navigator.pop(context);
} widget.onConfirm("# ${commentController.text}");
: null, }
child: Text( : null,
AppLocalizations.of(context)!.confirm, child: Text(
style: TextStyle( AppLocalizations.of(context)!.confirm,
color: validData == true style: TextStyle(
? Theme.of(context).colorScheme.primary color: validData == true
: Colors.grey ? 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()
),
);
}
} }
} }

View file

@ -1,6 +1,9 @@
// ignore_for_file: use_build_context_synchronously // ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_split_view/flutter_split_view.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -76,6 +79,19 @@ class _DnsSettingsWidgetState extends State<DnsSettingsWidget> {
final serversProvider = Provider.of<ServersProvider>(context); final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context); final appConfigProvider = Provider.of<AppConfigProvider>(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() { Widget generateBody() {
switch (widget.serversProvider.dnsInfo.loadStatus) { switch (widget.serversProvider.dnsInfo.loadStatus) {
case 0: case 0:
@ -105,51 +121,51 @@ class _DnsSettingsWidgetState extends State<DnsSettingsWidget> {
CustomListTile( CustomListTile(
title: AppLocalizations.of(context)!.upstreamDns, title: AppLocalizations.of(context)!.upstreamDns,
subtitle: AppLocalizations.of(context)!.upstreamDnsDescription, subtitle: AppLocalizations.of(context)!.upstreamDnsDescription,
onTap: () => Navigator.push(context, MaterialPageRoute( onTap: () => navigate(
builder: (context) => UpstreamDnsScreen( UpstreamDnsScreen(
serversProvider: serversProvider serversProvider: serversProvider
) )
)), ),
icon: Icons.upload_rounded, icon: Icons.upload_rounded,
), ),
CustomListTile( CustomListTile(
title: AppLocalizations.of(context)!.bootstrapDns, title: AppLocalizations.of(context)!.bootstrapDns,
subtitle: AppLocalizations.of(context)!.bootstrapDnsDescription, subtitle: AppLocalizations.of(context)!.bootstrapDnsDescription,
onTap: () => Navigator.push(context, MaterialPageRoute( onTap: () => navigate(
builder: (context) => BootstrapDnsScreen( BootstrapDnsScreen(
serversProvider: serversProvider serversProvider: serversProvider
) )
)), ),
icon: Icons.dns_rounded, icon: Icons.dns_rounded,
), ),
CustomListTile( CustomListTile(
title: AppLocalizations.of(context)!.privateReverseDnsServers, title: AppLocalizations.of(context)!.privateReverseDnsServers,
subtitle: AppLocalizations.of(context)!.privateReverseDnsDescription, subtitle: AppLocalizations.of(context)!.privateReverseDnsDescription,
onTap: () => Navigator.push(context, MaterialPageRoute( onTap: () => navigate(
builder: (context) => PrivateReverseDnsServersScreen( PrivateReverseDnsServersScreen(
serversProvider: serversProvider serversProvider: serversProvider
) )
)), ),
icon: Icons.person_rounded, icon: Icons.person_rounded,
), ),
CustomListTile( CustomListTile(
title: AppLocalizations.of(context)!.dnsServerSettings, title: AppLocalizations.of(context)!.dnsServerSettings,
subtitle: AppLocalizations.of(context)!.dnsServerSettingsDescription, subtitle: AppLocalizations.of(context)!.dnsServerSettingsDescription,
onTap: () => Navigator.push(context, MaterialPageRoute( onTap: () => navigate(
builder: (context) => DnsServerSettingsScreen( DnsServerSettingsScreen(
serversProvider: serversProvider serversProvider: serversProvider
) )
)), ),
icon: Icons.settings, icon: Icons.settings,
), ),
CustomListTile( CustomListTile(
title: AppLocalizations.of(context)!.dnsCacheConfig, title: AppLocalizations.of(context)!.dnsCacheConfig,
subtitle: AppLocalizations.of(context)!.dnsCacheConfigDescription, subtitle: AppLocalizations.of(context)!.dnsCacheConfigDescription,
onTap: () => Navigator.push(context, MaterialPageRoute( onTap: () => navigate(
builder: (context) => CacheConfigDnsScreen( CacheConfigDnsScreen(
serversProvider: serversProvider serversProvider: serversProvider
) )
)), ),
icon: Icons.storage_rounded, icon: Icons.storage_rounded,
), ),
], ],

View file

@ -229,8 +229,7 @@ class _PrivateReverseDnsServersScreenState extends State<PrivateReverseDnsServer
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
SizedBox( Expanded(
width: MediaQuery.of(context).size.width-74,
child: TextFormField( child: TextFormField(
controller: c['controller'], controller: c['controller'],
onChanged: (value) => validateAddress(c, value), onChanged: (value) => validateAddress(c, value),
@ -246,6 +245,7 @@ class _PrivateReverseDnsServersScreenState extends State<PrivateReverseDnsServer
) )
), ),
), ),
const SizedBox(width: 8),
IconButton( IconButton(
onPressed: () { onPressed: () {
setState(() => reverseResolversControllers = reverseResolversControllers.where((con) => con != c).toList()); setState(() => reverseResolversControllers = reverseResolversControllers.where((con) => con != c).toList());

View file

@ -1,5 +1,7 @@
// ignore_for_file: use_build_context_synchronously // ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -71,36 +73,73 @@ class _UpstreamDnsScreenState extends State<UpstreamDnsScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final serversProvider = Provider.of<ServersProvider>(context); final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context); final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
void openAddCommentModal() { void openAddCommentModal() {
showModalBottomSheet( if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) {
context: context, showDialog(
builder: (context) => CommentModal( context: context,
onConfirm: (value) { builder: (context) => CommentModal(
dnsServers.add({ onConfirm: (value) {
'comment': value setState(() {
}); dnsServers.add({
}, 'comment': value
), });
backgroundColor: Colors.transparent, });
isScrollControlled: true, },
isDismissible: true 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<String, dynamic> item, int position) { void openEditCommentModal(Map<String, dynamic> item, int position) {
showModalBottomSheet( if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) {
context: context, showDialog(
builder: (context) => CommentModal( context: context,
comment: item['comment'], builder: (context) => CommentModal(
onConfirm: (value) { comment: item['comment'],
setState(() => dnsServers[position] = { 'comment': value }); onConfirm: (value) {
}, setState(() => dnsServers[position] = { 'comment': value });
), },
backgroundColor: Colors.transparent, dialog: true,
isScrollControlled: true, ),
isDismissible: 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 { void saveData() async {
@ -185,13 +224,12 @@ class _UpstreamDnsScreenState extends State<UpstreamDnsScreen> {
), ),
...dnsServers.map((item) => Padding( ...dnsServers.map((item) => Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 16, right: 6, bottom: 20 left: 16, right: 6, bottom: 24
), ),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
if (item['controller'] != null) SizedBox( if (item['controller'] != null) Expanded(
width: MediaQuery.of(context).size.width-74,
child: TextFormField( child: TextFormField(
controller: item['controller'], controller: item['controller'],
onChanged: (_) => checkValidValues(), onChanged: (_) => checkValidValues(),
@ -206,6 +244,7 @@ class _UpstreamDnsScreenState extends State<UpstreamDnsScreen> {
) )
), ),
), ),
const SizedBox(width: 8),
if (item['comment'] != null) Expanded( if (item['comment'] != null) Expanded(
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -232,10 +271,12 @@ class _UpstreamDnsScreenState extends State<UpstreamDnsScreen> {
}, },
icon: const Icon(Icons.remove_circle_outline), icon: const Icon(Icons.remove_circle_outline),
tooltip: AppLocalizations.of(context)!.remove, tooltip: AppLocalizations.of(context)!.remove,
) ),
const SizedBox(width: 4),
], ],
), ),
)).toList(), )).toList(),
const SizedBox(height: 12),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,

View file

@ -7,10 +7,12 @@ import 'package:adguard_home_manager/models/rewrite_rules.dart';
class AddDnsRewriteModal extends StatefulWidget { class AddDnsRewriteModal extends StatefulWidget {
final void Function(RewriteRulesData) onConfirm; final void Function(RewriteRulesData) onConfirm;
final bool dialog;
const AddDnsRewriteModal({ const AddDnsRewriteModal({
Key? key, Key? key,
required this.onConfirm required this.onConfirm,
required this.dialog
}) : super(key: key); }) : super(key: key);
@override @override
@ -50,45 +52,45 @@ class _AddDnsRewriteModalState extends State<AddDnsRewriteModal> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( Widget content() {
padding: MediaQuery.of(context).viewInsets, return Column(
child: Container( mainAxisSize: MainAxisSize.min,
height: Platform.isIOS ? 416 : 400, children: [
decoration: BoxDecoration( Flexible(
borderRadius: const BorderRadius.only( child: SingleChildScrollView(
topLeft: Radius.circular(28), child: Wrap(
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,
children: [ children: [
Padding( Row(
padding: const EdgeInsets.only(top: 24), mainAxisAlignment: MainAxisAlignment.center,
child: Icon( children: [
Icons.add, Column(
size: 24, children: [
color: Theme.of(context).listTileTheme.iconColor 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(
padding: const EdgeInsets.symmetric(horizontal: 24), padding: const EdgeInsets.only(
left: 24, right: 24, bottom: 12
),
child: TextFormField( child: TextFormField(
controller: domainController, controller: domainController,
onChanged: validateDomain, onChanged: validateDomain,
@ -104,9 +106,10 @@ class _AddDnsRewriteModalState extends State<AddDnsRewriteModal> {
), ),
), ),
), ),
const SizedBox(height: 30),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 24), padding: const EdgeInsets.only(
left: 24, right: 24, top: 12
),
child: TextFormField( child: TextFormField(
controller: answerController, controller: answerController,
onChanged: (_) => checkValidValues(), onChanged: (_) => checkValidValues(),
@ -124,44 +127,70 @@ class _AddDnsRewriteModalState extends State<AddDnsRewriteModal> {
], ],
), ),
), ),
Padding( ),
padding: const EdgeInsets.all(24), Padding(
child: Row( padding: const EdgeInsets.all(24),
mainAxisAlignment: MainAxisAlignment.end, child: Row(
children: [ mainAxisAlignment: MainAxisAlignment.end,
TextButton( children: [
onPressed: () => Navigator.pop(context), TextButton(
child: Text(AppLocalizations.of(context)!.cancel), onPressed: () => Navigator.pop(context),
), child: Text(AppLocalizations.of(context)!.cancel),
const SizedBox(width: 20), ),
TextButton( const SizedBox(width: 20),
onPressed: validData == true TextButton(
? () { onPressed: validData == true
Navigator.pop(context); ? () {
widget.onConfirm( Navigator.pop(context);
RewriteRulesData( widget.onConfirm(
domain: domainController.text, RewriteRulesData(
answer: answerController.text domain: domainController.text,
) answer: answerController.text
); )
} );
: null, }
child: Text( : null,
AppLocalizations.of(context)!.confirm, child: Text(
style: TextStyle( AppLocalizations.of(context)!.confirm,
color: validData == true style: TextStyle(
? Theme.of(context).colorScheme.primary color: validData == true
: Theme.of(context).colorScheme.onSurface.withOpacity(0.38) ? 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()
),
);
}
} }
} }

View file

@ -1,5 +1,7 @@
// ignore_for_file: use_build_context_synchronously // ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -70,6 +72,8 @@ class _DnsRewritesWidgetState extends State<DnsRewritesWidget> {
final serversProvider = Provider.of<ServersProvider>(context); final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context); final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
void deleteDnsRewrite(RewriteRulesData rule) async { void deleteDnsRewrite(RewriteRulesData rule) async {
ProcessModal processModal = ProcessModal(context: context); ProcessModal processModal = ProcessModal(context: context);
processModal.open(AppLocalizations.of(context)!.deleting); processModal.open(AppLocalizations.of(context)!.deleting);
@ -288,14 +292,26 @@ class _DnsRewritesWidgetState extends State<DnsRewritesWidget> {
body: generateBody(), body: generateBody(),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: () => { onPressed: () => {
showModalBottomSheet( if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) {
context: context, showDialog(
builder: (context) => AddDnsRewriteModal( context: context,
onConfirm: addDnsRewrite, builder: (context) => AddDnsRewriteModal(
), onConfirm: addDnsRewrite,
backgroundColor: Colors.transparent, dialog: true,
isScrollControlled: true ),
) )
}
else {
showModalBottomSheet(
context: context,
builder: (context) => AddDnsRewriteModal(
onConfirm: addDnsRewrite,
dialog: false,
),
backgroundColor: Colors.transparent,
isScrollControlled: true
)
}
}, },
child: const Icon(Icons.add), child: const Icon(Icons.add),
), ),

View file

@ -26,8 +26,12 @@ class EncryptionTextField extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: width > 900
? const EdgeInsets.symmetric(horizontal: 8)
: const EdgeInsets.symmetric(horizontal: 16),
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints( constraints: const BoxConstraints(
maxHeight: 200 maxHeight: 200

View file

@ -234,6 +234,8 @@ class _EncryptionSettingsWidgetState extends State<EncryptionSettingsWidget> {
final serversProvider = Provider.of<ServersProvider>(context); final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context); final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
void saveData() async { void saveData() async {
ProcessModal processModal = ProcessModal(context: context); ProcessModal processModal = ProcessModal(context: context);
processModal.open(AppLocalizations.of(context)!.savingConfig); processModal.open(AppLocalizations.of(context)!.savingConfig);
@ -343,43 +345,59 @@ class _EncryptionSettingsWidgetState extends State<EncryptionSettingsWidget> {
disabled: !enabled, disabled: !enabled,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
EncryptionTextField( Wrap(
enabled: enabled, children: [
controller: httpsPortController, FractionallySizedBox(
icon: Icons.numbers_rounded, widthFactor: width > 900 ? 0.33 : 1,
onChanged: (value) { child: EncryptionTextField(
setState(() => httpsPortError = validatePort(context, value)); enabled: enabled,
onEditValidate(); controller: httpsPortController,
}, icon: Icons.numbers_rounded,
errorText: httpsPortError, onChanged: (value) {
label: AppLocalizations.of(context)!.httpsPort, setState(() => httpsPortError = validatePort(context, value));
keyboardType: TextInputType.number, onEditValidate();
), },
const SizedBox(height: 30), errorText: httpsPortError,
EncryptionTextField( label: AppLocalizations.of(context)!.httpsPort,
enabled: enabled, keyboardType: TextInputType.number,
controller: tlsPortController, ),
icon: Icons.numbers_rounded, ),
onChanged: (value) { Padding(
setState(() => tlsPortError = validatePort(context, value)); padding: width <= 900
onEditValidate(); ? const EdgeInsets.symmetric(vertical: 24)
}, : const EdgeInsets.all(0),
errorText: tlsPortError, child: FractionallySizedBox(
label: AppLocalizations.of(context)!.tlsPort, widthFactor: width > 900 ? 0.33 : 1,
keyboardType: TextInputType.number, child: EncryptionTextField(
), enabled: enabled,
const SizedBox(height: 30), controller: tlsPortController,
EncryptionTextField( icon: Icons.numbers_rounded,
enabled: enabled, onChanged: (value) {
controller: dnsOverQuicPortController, setState(() => tlsPortError = validatePort(context, value));
icon: Icons.numbers_rounded, onEditValidate();
onChanged: (value) { },
setState(() => dnsOverQuicPortError = validatePort(context, value)); errorText: tlsPortError,
onEditValidate(); label: AppLocalizations.of(context)!.tlsPort,
}, keyboardType: TextInputType.number,
errorText: dnsOverQuicPortError, ),
label: AppLocalizations.of(context)!.dnsOverQuicPort, ),
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( SectionLabel(
label: AppLocalizations.of(context)!.certificates, label: AppLocalizations.of(context)!.certificates,

View file

@ -52,24 +52,26 @@ class _SafeSearchSettingsScreenWidgetState extends State<SafeSearchSettingsScree
bool youtubeEnabled = false; bool youtubeEnabled = false;
Future requestSafeSearchSettings() async { Future requestSafeSearchSettings() async {
final result = await getServerStatus(widget.serversProvider.selectedServer!);
if (mounted) { if (mounted) {
if (result['result'] == 'success') { final result = await getServerStatus(widget.serversProvider.selectedServer!);
widget.serversProvider.setServerStatusData(result['data']); if (mounted) {
widget.serversProvider.setServerStatusLoad(1); if (result['result'] == 'success') {
setState(() { widget.serversProvider.setServerStatusData(result['data']);
generalEnabled = result['data'].safeSearchEnabled; widget.serversProvider.setServerStatusLoad(1);
bingEnabled = result['data'].safeSeachBing; setState(() {
duckduckgoEnabled = result['data'].safeSearchDuckduckgo; generalEnabled = result['data'].safeSearchEnabled;
googleEnabled = result['data'].safeSearchGoogle; bingEnabled = result['data'].safeSeachBing;
pixabayEnabled = result['data'].safeSearchPixabay; duckduckgoEnabled = result['data'].safeSearchDuckduckgo;
yandexEnabled = result['data'].safeSearchYandex; googleEnabled = result['data'].safeSearchGoogle;
youtubeEnabled = result['data'].safeSearchYoutube; pixabayEnabled = result['data'].safeSearchPixabay;
}); yandexEnabled = result['data'].safeSearchYandex;
} youtubeEnabled = result['data'].safeSearchYoutube;
else { });
widget.appConfigProvider.addLog(result['log']); }
widget.serversProvider.setServerStatusLoad(2); else {
widget.appConfigProvider.addLog(result['log']);
widget.serversProvider.setServerStatusLoad(2);
}
} }
} }
} }

View file

@ -1,6 +1,9 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_split_view/flutter_split_view.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/settings/server_info/server_info.dart'; import 'package:adguard_home_manager/screens/settings/server_info/server_info.dart';
@ -9,7 +12,6 @@ import 'package:adguard_home_manager/screens/settings/access_settings/access_set
import 'package:adguard_home_manager/screens/settings/customization/customization.dart'; import 'package:adguard_home_manager/screens/settings/customization/customization.dart';
import 'package:adguard_home_manager/screens/settings/dhcp/dhcp.dart'; import 'package:adguard_home_manager/screens/settings/dhcp/dhcp.dart';
import 'package:adguard_home_manager/screens/settings/safe_search_settings.dart'; import 'package:adguard_home_manager/screens/settings/safe_search_settings.dart';
import 'package:adguard_home_manager/widgets/section_label.dart';
import 'package:adguard_home_manager/screens/settings/update_server/update.dart'; import 'package:adguard_home_manager/screens/settings/update_server/update.dart';
import 'package:adguard_home_manager/screens/settings/dns/dns.dart'; import 'package:adguard_home_manager/screens/settings/dns/dns.dart';
import 'package:adguard_home_manager/screens/settings/dns_rewrites/dns_rewrites.dart'; import 'package:adguard_home_manager/screens/settings/dns_rewrites/dns_rewrites.dart';
@ -17,6 +19,8 @@ import 'package:adguard_home_manager/screens/servers/servers.dart';
import 'package:adguard_home_manager/screens/settings/advanced_setings.dart'; import 'package:adguard_home_manager/screens/settings/advanced_setings.dart';
import 'package:adguard_home_manager/screens/settings/general_settings.dart'; import 'package:adguard_home_manager/screens/settings/general_settings.dart';
import 'package:adguard_home_manager/widgets/custom_settings_tile.dart';
import 'package:adguard_home_manager/widgets/section_label.dart';
import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; import 'package:adguard_home_manager/widgets/custom_list_tile.dart';
import 'package:adguard_home_manager/constants/strings.dart'; import 'package:adguard_home_manager/constants/strings.dart';
@ -29,18 +33,85 @@ import 'package:adguard_home_manager/providers/app_config_provider.dart';
class Settings extends StatelessWidget { class Settings extends StatelessWidget {
const Settings({Key? key}) : super(key: key); const Settings({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
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)!.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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final appConfigProvider = Provider.of<AppConfigProvider>(context); final appConfigProvider = Provider.of<AppConfigProvider>(context);
final serversProvider = Provider.of<ServersProvider>(context); final serversProvider = Provider.of<ServersProvider>(context);
void navigateServers() { final width = MediaQuery.of(context).size.width;
Future.delayed(const Duration(milliseconds: 0), (() {
Navigator.of(context).push( if (width <= 900 && appConfigProvider.selectedSettingsScreen != null) {
MaterialPageRoute(builder: (context) => const Servers()) 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( return Scaffold(
appBar: AppBar( appBar: AppBar(
@ -49,85 +120,55 @@ class Settings extends StatelessWidget {
), ),
body: ListView( body: ListView(
children: [ children: [
if (serversProvider.selectedServer != null) ...[ if (serversProvider.selectedServer != null && serversProvider.serverStatus.data != null) ...[
SectionLabel(label: AppLocalizations.of(context)!.serverSettings), SectionLabel(label: AppLocalizations.of(context)!.serverSettings),
if (serverVersionIsAhead( if (serverVersionIsAhead(
currentVersion: serversProvider.serverStatus.data!.serverVersion, currentVersion: serversProvider.serverStatus.data!.serverVersion,
referenceVersion: 'v0.107.28', referenceVersion: 'v0.107.28',
referenceVersionBeta: 'v0.108.0-b.33' referenceVersionBeta: 'v0.108.0-b.33'
) == true) CustomListTile( ) == true) settingsTile(
icon: Icons.search_rounded, icon: Icons.search_rounded,
title: AppLocalizations.of(context)!.safeSearch, title: AppLocalizations.of(context)!.safeSearch,
subtitle: AppLocalizations.of(context)!.safeSearchSettings, subtitle: AppLocalizations.of(context)!.safeSearchSettings,
onTap: () => { thisItem: 0,
Navigator.of(context).push( screenToNavigate: const SafeSearchSettingsScreen(),
MaterialPageRoute(
builder: (context) => const SafeSearchSettingsScreen()
)
)
},
), ),
CustomListTile( settingsTile(
icon: Icons.lock_rounded, icon: Icons.lock_rounded,
title: AppLocalizations.of(context)!.accessSettings, title: AppLocalizations.of(context)!.accessSettings,
subtitle: AppLocalizations.of(context)!.accessSettingsDescription, subtitle: AppLocalizations.of(context)!.accessSettingsDescription,
onTap: () => { thisItem: 1,
Navigator.of(context).push( screenToNavigate: const AccessSettings(),
MaterialPageRoute(
builder: (context) => const AccessSettings()
)
)
},
), ),
CustomListTile( settingsTile(
icon: Icons.install_desktop_rounded, icon: Icons.install_desktop_rounded,
title: AppLocalizations.of(context)!.dhcpSettings, title: AppLocalizations.of(context)!.dhcpSettings,
subtitle: AppLocalizations.of(context)!.dhcpSettingsDescription, subtitle: AppLocalizations.of(context)!.dhcpSettingsDescription,
onTap: () => { thisItem: 2,
Navigator.of(context).push( screenToNavigate: const Dhcp(),
MaterialPageRoute(
builder: (context) => const Dhcp()
)
)
},
), ),
CustomListTile( settingsTile(
icon: Icons.dns_rounded, icon: Icons.dns_rounded,
title: AppLocalizations.of(context)!.dnsSettings, title: AppLocalizations.of(context)!.dnsSettings,
subtitle: AppLocalizations.of(context)!.dnsSettingsDescription, subtitle: AppLocalizations.of(context)!.dnsSettingsDescription,
onTap: () => { thisItem: 3,
Navigator.of(context).push( screenToNavigate: const DnsSettings(),
MaterialPageRoute(
builder: (context) => const DnsSettings()
)
)
},
), ),
CustomListTile( settingsTile(
icon: Icons.security_rounded, icon: Icons.security_rounded,
title: AppLocalizations.of(context)!.encryptionSettings, title: AppLocalizations.of(context)!.encryptionSettings,
subtitle: AppLocalizations.of(context)!.encryptionSettingsDescription, subtitle: AppLocalizations.of(context)!.encryptionSettingsDescription,
onTap: () => { thisItem: 4,
Navigator.of(context).push( screenToNavigate: const EncryptionSettings(),
MaterialPageRoute(
builder: (context) => const EncryptionSettings()
)
)
},
), ),
CustomListTile( settingsTile(
icon: Icons.route_rounded, icon: Icons.route_rounded,
title: AppLocalizations.of(context)!.dnsRewrites, title: AppLocalizations.of(context)!.dnsRewrites,
subtitle: AppLocalizations.of(context)!.dnsRewritesDescription, subtitle: AppLocalizations.of(context)!.dnsRewritesDescription,
onTap: () => { thisItem: 5,
Navigator.of(context).push( screenToNavigate: const DnsRewrites(),
MaterialPageRoute(
builder: (context) => const DnsRewrites()
)
)
},
), ),
if (serversProvider.updateAvailable.data != null) CustomListTile( if (serversProvider.updateAvailable.data != null) settingsTile(
icon: Icons.system_update_rounded, icon: Icons.system_update_rounded,
title: AppLocalizations.of(context)!.updates, title: AppLocalizations.of(context)!.updates,
subtitle: AppLocalizations.of(context)!.updatesDescription, subtitle: AppLocalizations.of(context)!.updatesDescription,
@ -144,37 +185,26 @@ class Settings extends StatelessWidget {
), ),
) )
: null, : null,
onTap: () => { thisItem: 6,
Navigator.of(context).push( screenToNavigate: const UpdateScreen(),
MaterialPageRoute(
builder: (context) => const UpdateScreen()
)
)
},
), ),
CustomListTile( settingsTile(
icon: Icons.info_rounded, icon: Icons.info_rounded,
title: AppLocalizations.of(context)!.serverInformation, title: AppLocalizations.of(context)!.serverInformation,
subtitle: AppLocalizations.of(context)!.serverInformationDescription, subtitle: AppLocalizations.of(context)!.serverInformationDescription,
onTap: () => { thisItem: 7,
Navigator.of(context).push( screenToNavigate: const ServerInformation(),
MaterialPageRoute(
builder: (context) => const ServerInformation()
)
)
},
), ),
], ],
SectionLabel(label: AppLocalizations.of(context)!.appSettings), SectionLabel(label: AppLocalizations.of(context)!.appSettings),
CustomListTile( settingsTile(
icon: Icons.palette_rounded, icon: Icons.palette_rounded,
title: AppLocalizations.of(context)!.customization, title: AppLocalizations.of(context)!.customization,
subtitle: AppLocalizations.of(context)!.customizationDescription, subtitle: AppLocalizations.of(context)!.customizationDescription,
onTap: () => Navigator.push(context, MaterialPageRoute( thisItem: 8,
builder: (context) => const Customization() screenToNavigate: const Customization(),
))
), ),
CustomListTile( settingsTile(
icon: Icons.storage_rounded, icon: Icons.storage_rounded,
title: AppLocalizations.of(context)!.servers, title: AppLocalizations.of(context)!.servers,
subtitle: serversProvider.selectedServer != null subtitle: serversProvider.selectedServer != null
@ -182,31 +212,22 @@ class Settings extends StatelessWidget {
? "${AppLocalizations.of(context)!.connectedTo} ${serversProvider.selectedServer!.name}" ? "${AppLocalizations.of(context)!.connectedTo} ${serversProvider.selectedServer!.name}"
: "${AppLocalizations.of(context)!.selectedServer} ${serversProvider.selectedServer!.name}" : "${AppLocalizations.of(context)!.selectedServer} ${serversProvider.selectedServer!.name}"
: AppLocalizations.of(context)!.noServerSelected, : AppLocalizations.of(context)!.noServerSelected,
onTap: navigateServers, thisItem: 9,
screenToNavigate: const Servers(),
), ),
CustomListTile( settingsTile(
icon: Icons.settings, icon: Icons.settings,
title: AppLocalizations.of(context)!.generalSettings, title: AppLocalizations.of(context)!.generalSettings,
subtitle: AppLocalizations.of(context)!.generalSettingsDescription, subtitle: AppLocalizations.of(context)!.generalSettingsDescription,
onTap: () => { thisItem: 10,
Navigator.of(context).push( screenToNavigate: const GeneralSettings(),
MaterialPageRoute(
builder: (context) => const GeneralSettings()
)
)
},
), ),
CustomListTile( settingsTile(
icon: Icons.build_outlined, icon: Icons.build_outlined,
title: AppLocalizations.of(context)!.advancedSettings, title: AppLocalizations.of(context)!.advancedSettings,
subtitle: AppLocalizations.of(context)!.advancedSetupDescription, subtitle: AppLocalizations.of(context)!.advancedSetupDescription,
onTap: () => { thisItem: 11,
Navigator.of(context).push( screenToNavigate: const AdvancedSettings(),
MaterialPageRoute(
builder: (context) => const AdvancedSettings()
)
)
},
), ),
SectionLabel(label: AppLocalizations.of(context)!.aboutApp), SectionLabel(label: AppLocalizations.of(context)!.aboutApp),
CustomListTile( CustomListTile(
@ -222,7 +243,7 @@ class Settings extends StatelessWidget {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
IconButton( if (Platform.isAndroid) IconButton(
onPressed: () => openUrl(Urls.playStore), onPressed: () => openUrl(Urls.playStore),
icon: SvgPicture.asset( icon: SvgPicture.asset(
'assets/resources/google-play.svg', 'assets/resources/google-play.svg',

View file

@ -65,16 +65,18 @@ class UpdateScreen extends StatelessWidget {
Widget headerPortrait() { Widget headerPortrait() {
return Column( return Column(
children: [ children: [
const SizedBox(height: 8),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
IconButton( if (Navigator.canPop(context)) IconButton(
icon: Icon( icon: Icon(
Icons.arrow_back, Icons.arrow_back,
color: Theme.of(context).colorScheme.onSurfaceVariant, color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
), ),
if (!Navigator.canPop(context)) const SizedBox(),
IconButton( IconButton(
icon: Icon( icon: Icon(
Icons.refresh_rounded, 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 final changelog = serversProvider.updateAvailable.loadStatus == LoadStatus.loaded && serversProvider.updateAvailable.data!.changelog != null
? ListView( ? ListView(
children: [ children: [
@ -313,51 +201,20 @@ class UpdateScreen extends StatelessWidget {
: null; : null;
return Scaffold( return Scaffold(
body: MediaQuery.of(context).size.width > 700 body: Column(
? Row( children: [
children: [ Container(
Expanded( color: Theme.of(context).colorScheme.surfaceVariant,
flex: 2, child: SafeArea(
child: Container( child: headerPortrait()
color: Theme.of(context).colorScheme.surfaceVariant, )
child: Column( ),
mainAxisSize: MainAxisSize.max, const SizedBox(height: 16),
children: [ changelog != null
Container( ? Expanded(child: changelog)
height: MediaQuery.of(context).size.height, : const SizedBox(),
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(),
]
)
); );
} }
} }

View file

@ -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<Map<String, dynamic>> data;
const TopItemsModal({
Key? key,
required this.type,
required this.title,
this.isClient,
required this.data,
}) : super(key: key);
@override
State<TopItemsModal> createState() => _TopItemsModalState();
}
class _TopItemsModalState extends State<TopItemsModal> {
bool searchActive = false;
final TextEditingController searchController = TextEditingController();
List<Map<String, dynamic>> data = [];
List<Map<String, dynamic>> screenData = [];
void search(String value) {
List<Map<String, dynamic>> 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<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final logsProvider = Provider.of<LogsProvider>(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,
),
),
),
)
],
),
),
);
}
}

View file

@ -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<dynamic> 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<dynamic> 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<bool> 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<dynamic> 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<bool> 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;
}
}

View file

@ -16,10 +16,12 @@ enum ConnectionType { http, https}
class AddServerModal extends StatefulWidget { class AddServerModal extends StatefulWidget {
final Server? server; final Server? server;
final bool window;
const AddServerModal({ const AddServerModal({
Key? key, Key? key,
this.server, this.server,
required this.window
}) : super(key: key); }) : super(key: key);
@override @override
@ -429,15 +431,227 @@ class _AddServerModalState extends State<AddServerModal> {
} }
} }
return Stack( List<Widget> form() {
children: [ return [
Scaffold( Container(
appBar: AppBar( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
title: Text(AppLocalizations.of(context)!.createConnection), margin: const EdgeInsets.only(
actions: [ 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<ConnectionType>(
segments: const [
ButtonSegment(
value: ConnectionType.http,
label: Text("HTTP")
),
ButtonSegment(
value: ConnectionType.https,
label: Text("HTTPS")
),
],
selected: <ConnectionType>{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(
padding: const EdgeInsets.only(right: 10), padding: const EdgeInsets.all(16),
child: IconButton( 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 tooltip: widget.server == null
? AppLocalizations.of(context)!.connect ? AppLocalizations.of(context)!.connect
: AppLocalizations.of(context)!.save, : AppLocalizations.of(context)!.save,
@ -452,203 +666,49 @@ class _AddServerModalState extends State<AddServerModal> {
: Icons.save_rounded : Icons.save_rounded
) )
), ),
), const SizedBox(width: 10)
], ],
toolbarHeight: 70, toolbarHeight: 70,
),
body: ListView(
children: form(),
)
), ),
body: ListView( AnimatedOpacity(
children: [ opacity: isConnecting == true ? 1 : 0,
Container( duration: const Duration(milliseconds: 250),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), curve: Curves.easeInOut,
margin: const EdgeInsets.only( child: IgnorePointer(
top: 24, ignoring: isConnecting == true ? false : true,
left: 24, child: Scaffold(
right: 24 backgroundColor: Colors.transparent,
), body: Container(
decoration: BoxDecoration( width: mediaQuery.size.width,
color: Theme.of(context).colorScheme.primary.withOpacity(0.05), height: mediaQuery.size.height,
borderRadius: BorderRadius.circular(30), color: const Color.fromRGBO(0, 0, 0, 0.7),
border: Border.all( child: Column(
color: Theme.of(context).colorScheme.primary mainAxisAlignment: MainAxisAlignment.center,
) children: [
), const CircularProgressIndicator(
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<ConnectionType>(
segments: const [
ButtonSegment(
value: ConnectionType.http,
label: Text("HTTP")
),
ButtonSegment(
value: ConnectionType.https,
label: Text("HTTPS")
),
],
selected: <ConnectionType>{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(
color: Colors.white, 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
),
)
],
),
), ),
), ),
), ),
), )
) ],
], );
); }
} }
} }

View file

@ -89,6 +89,10 @@ class BottomNavBar extends StatelessWidget {
if (value != 2) { if (value != 2) {
logsProvider.resetFilters(); logsProvider.resetFilters();
} }
// Reset settings selected screen
if (value != screens.length-1) {
appConfigProvider.setSelectedSettingsScreen(screen: null);
}
appConfigProvider.setSelectedScreen(value); appConfigProvider.setSelectedScreen(value);
}, },
); );

View file

@ -10,6 +10,7 @@ class CustomListTile extends StatelessWidget {
final EdgeInsets? padding; final EdgeInsets? padding;
final void Function()? onLongPress; final void Function()? onLongPress;
final bool? disabled; final bool? disabled;
final void Function(bool)? onHover;
const CustomListTile({ const CustomListTile({
Key? key, Key? key,
@ -21,7 +22,8 @@ class CustomListTile extends StatelessWidget {
this.trailing, this.trailing,
this.padding, this.padding,
this.onLongPress, this.onLongPress,
this.disabled this.disabled,
this.onHover,
}) : super(key: key); }) : super(key: key);
@override @override
@ -30,6 +32,7 @@ class CustomListTile extends StatelessWidget {
color: Colors.transparent, color: Colors.transparent,
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
onHover: onHover,
onLongPress: onLongPress, onLongPress: onLongPress,
child: Padding( child: Padding(
padding: padding ?? const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: padding ?? const EdgeInsets.symmetric(horizontal: 16, vertical: 12),

View file

@ -41,33 +41,35 @@ class CustomRadioListTile extends StatelessWidget {
backgroundColor: radioBackgroundColor, backgroundColor: radioBackgroundColor,
), ),
const SizedBox(width: 24), const SizedBox(width: 24),
Column( Expanded(
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
SizedBox( children: [
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),
SizedBox( SizedBox(
width: MediaQuery.of(context).size.width-110, width: MediaQuery.of(context).size.width-110,
child: Text( child: Text(
subtitle!, title,
style: TextStyle( style: TextStyle(
color: Theme.of(context).listTileTheme.textColor, fontSize: 16,
fontSize: 14 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
),
),
),
]
],
),
), ),
], ],
), ),

View file

@ -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
),
),
),
);
}
}

92
lib/widgets/menu_bar.dart Normal file
View file

@ -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<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
List<AppScreen> 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: <PlatformMenuItem>[
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: <PlatformMenuItem>[
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,
);
}
}

View file

@ -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<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final logsProvider = Provider.of<LogsProvider>(context);
List<AppScreen> 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),
);
}
}

View file

@ -20,6 +20,7 @@ class ProcessDialog extends StatelessWidget {
horizontal: 30 horizontal: 30
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min,
children: [ children: [
const CircularProgressIndicator(), const CircularProgressIndicator(),
const SizedBox(width: 40), const SizedBox(width: 40),

View file

@ -23,9 +23,17 @@ class DeleteModal extends StatelessWidget {
final appConfigProvider = Provider.of<AppConfigProvider>(context); final appConfigProvider = Provider.of<AppConfigProvider>(context);
void removeServer() async { void removeServer() async {
final previouslySelectedServer = serversProvider.selectedServer;
final deleted = await serversProvider.removeServer(serverToDelete); final deleted = await serversProvider.removeServer(serverToDelete);
Navigator.pop(context); Navigator.pop(context);
if (deleted == true) { if (deleted == true) {
if (previouslySelectedServer != null && previouslySelectedServer.id == serverToDelete.id) {
appConfigProvider.setSelectedScreen(0);
}
showSnacbkar( showSnacbkar(
context: context, context: context,
appConfigProvider: appConfigProvider, appConfigProvider: appConfigProvider,

View file

@ -5,6 +5,7 @@ import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.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_list_item.dart';
import 'package:adguard_home_manager/widgets/servers_list/servers_tile_item.dart';
import 'package:adguard_home_manager/providers/servers_provider.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart';
@ -13,6 +14,7 @@ class ServersList extends StatelessWidget {
final List<ExpandableController> controllers; final List<ExpandableController> controllers;
final Function(int) onChange; final Function(int) onChange;
final ScrollController scrollController; final ScrollController scrollController;
final double breakingWidth;
const ServersList({ const ServersList({
Key? key, Key? key,
@ -20,23 +22,44 @@ class ServersList extends StatelessWidget {
required this.controllers, required this.controllers,
required this.onChange, required this.onChange,
required this.scrollController, required this.scrollController,
required this.breakingWidth
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final serversProvider = Provider.of<ServersProvider>(context); final serversProvider = Provider.of<ServersProvider>(context);
final width = MediaQuery.of(context).size.width;
if (serversProvider.serversList.isNotEmpty) { if (serversProvider.serversList.isNotEmpty) {
return ListView.builder( if (width > breakingWidth) {
controller: scrollController, return ListView(
itemCount: serversProvider.serversList.length, children: [
itemBuilder: (context, index) => ServersListItem( Wrap(
expandableController: controllers[index], children: serversProvider.serversList.asMap().entries.map(
server: serversProvider.serversList[index], (s) => ServersTileItem(
index: index, server: serversProvider.serversList[s.key],
onChange: onChange 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 { else {
return SizedBox( return SizedBox(

View file

@ -71,6 +71,8 @@ class _ServersListItemState extends State<ServersListItem> with SingleTickerProv
final serversProvider = Provider.of<ServersProvider>(context); final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context); final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
void showDeleteModal(Server server) async { void showDeleteModal(Server server) async {
await Future.delayed(const Duration(seconds: 0), () => { await Future.delayed(const Duration(seconds: 0), () => {
showDialog( showDialog(
@ -85,10 +87,25 @@ class _ServersListItemState extends State<ServersListItem> with SingleTickerProv
void openAddServerBottomSheet({Server? server}) async { void openAddServerBottomSheet({Server? server}) async {
await Future.delayed(const Duration(seconds: 0), (() => { await Future.delayed(const Duration(seconds: 0), (() => {
Navigator.push(context, MaterialPageRoute( if (width > 700) {
fullscreenDialog: true, showDialog(
builder: (BuildContext context) => AddServerModal(server: server) 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<ServersListItem> with SingleTickerProv
); );
} }
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border( border: Border(

View file

@ -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<ServersTileItem> createState() => _ServersTileItemState();
}
class _ServersTileItemState extends State<ServersTileItem> with SingleTickerProviderStateMixin {
@override
Widget build(BuildContext context) {
final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(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)
],
),
),
),
);
}
}

View file

@ -15,6 +15,8 @@ class CustomTabContentList extends StatelessWidget {
final double? refreshIndicatorOffset; final double? refreshIndicatorOffset;
final Widget? fab; final Widget? fab;
final bool? fabVisible; final bool? fabVisible;
final bool? noSliver;
final EdgeInsets? listPadding;
const CustomTabContentList({ const CustomTabContentList({
Key? key, Key? key,
@ -27,7 +29,9 @@ class CustomTabContentList extends StatelessWidget {
required this.onRefresh, required this.onRefresh,
this.refreshIndicatorOffset, this.refreshIndicatorOffset,
this.fab, this.fab,
this.fabVisible this.fabVisible,
this.noSliver,
this.listPadding
}) : super(key: key); }) : super(key: key);
@override @override
@ -36,95 +40,157 @@ class CustomTabContentList extends StatelessWidget {
switch (loadStatus) { switch (loadStatus) {
case LoadStatus.loading: case LoadStatus.loading:
return SafeArea( if (noSliver == true) {
top: false, return Padding(
bottom: false, padding: const EdgeInsets.symmetric(horizontal: 16),
child: Builder( child: loadingGenerator()
builder: (BuildContext context) => CustomScrollView( );
slivers: [ }
SliverOverlapInjector( else {
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), return SafeArea(
), top: false,
SliverFillRemaining( bottom: false,
child: Padding( child: Builder(
padding: const EdgeInsets.symmetric(horizontal: 16), builder: (BuildContext context) => CustomScrollView(
child: loadingGenerator() slivers: [
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
), ),
) SliverFillRemaining(
], child: Padding(
), padding: const EdgeInsets.symmetric(horizontal: 16),
) child: loadingGenerator()
); ),
)
],
),
)
);
}
case LoadStatus.loaded: case LoadStatus.loaded:
return Stack( if (noSliver == true) {
children: [ if (itemsCount > 0) {
SafeArea( return Stack(
top: false, children: [
bottom: false, ListView.builder(
child: Builder( padding: listPadding,
builder: (BuildContext context) { itemCount: itemsCount,
return RefreshIndicator( itemBuilder: (context, index) => contentWidget(index),
onRefresh: onRefresh, ),
edgeOffset: refreshIndicatorOffset ?? 95, if (fab != null) AnimatedPositioned(
child: CustomScrollView( duration: const Duration(milliseconds: 100),
slivers: <Widget>[ curve: Curves.easeInOut,
SliverOverlapInjector( bottom: fabVisible != null && fabVisible == true ?
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), appConfigProvider.showingSnackbar
), ? 70 : 20
if (itemsCount > 0) SliverList( : -70,
delegate: SliverChildBuilderDelegate( right: 20,
(context, index) => contentWidget(index), child: fab!
childCount: itemsCount ),
],
);
}
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: <Widget>[
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
), ),
), if (itemsCount > 0) SliverList(
if (itemsCount == 0) SliverFillRemaining( delegate: SliverChildBuilderDelegate(
child: noData, (context, index) => contentWidget(index),
) childCount: itemsCount
], ),
), ),
); if (itemsCount == 0) SliverFillRemaining(
}, child: noData,
)
],
),
);
},
),
), ),
), if (fab != null) AnimatedPositioned(
if (fab != null) AnimatedPositioned( duration: const Duration(milliseconds: 100),
duration: const Duration(milliseconds: 100), curve: Curves.easeInOut,
curve: Curves.easeInOut, bottom: fabVisible != null && fabVisible == true ?
bottom: fabVisible != null && fabVisible == true ? appConfigProvider.showingSnackbar
appConfigProvider.showingSnackbar ? 70 : 20
? 70 : 20 : -70,
: -70, right: 20,
right: 20, child: fab!
child: fab! ),
), ],
], );
); }
case LoadStatus.error: case LoadStatus.error:
return SafeArea( if (noSliver == true) {
top: false, return Padding(
bottom: false, padding: const EdgeInsets.only(
child: Builder( top: 95,
builder: (BuildContext context) => CustomScrollView( left: 16,
slivers: [ right: 16
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
SliverFillRemaining(
child: Padding(
padding: const EdgeInsets.only(
top: 95,
left: 16,
right: 16
),
child: errorGenerator()
),
)
],
), ),
) 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: default:
return const SizedBox(); return const SizedBox();

View file

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -22,14 +24,30 @@ class UpdateModal extends StatefulWidget {
class _UpdateModalState extends State<UpdateModal> { class _UpdateModalState extends State<UpdateModal> {
bool doNotRemember = false; bool doNotRemember = false;
String getDownloadLink() { String? getDownloadLink() {
return widget.gitHubRelease.assets.firstWhere((item) => item.browserDownloadUrl.contains('apk')).browserDownloadUrl; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final appConfigProvider = Provider.of<AppConfigProvider>(context); final appConfigProvider = Provider.of<AppConfigProvider>(context);
final downloadLink = getDownloadLink();
return AlertDialog( return AlertDialog(
scrollable: true, scrollable: true,
title: Column( title: Column(
@ -104,10 +122,10 @@ class _UpdateModalState extends State<UpdateModal> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
TextButton( if (downloadLink != null) TextButton(
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(context);
widget.onDownload(getDownloadLink(), widget.gitHubRelease.tagName); widget.onDownload(downloadLink, widget.gitHubRelease.tagName);
}, },
child: Text(AppLocalizations.of(context)!.download) child: Text(AppLocalizations.of(context)!.download)
), ),

1
linux/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
flutter/ephemeral

138
linux/CMakeLists.txt Normal file
View file

@ -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 "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>: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()

View file

@ -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}
)

View file

@ -0,0 +1,27 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <dynamic_color/dynamic_color_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
#include <window_size/window_size_plugin.h>
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);
}

View file

@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

Some files were not shown because too many files have changed in this diff Show more