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.
version:
revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
revision: f72efea43c3013323d1b95cff571f3c1caa37583
channel: stable
project_type: app
@ -13,26 +13,11 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
- platform: android
create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
- platform: ios
create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
create_revision: f72efea43c3013323d1b95cff571f3c1caa37583
base_revision: f72efea43c3013323d1b95cff571f3c1caa37583
- platform: linux
create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
- platform: macos
create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
- platform: web
create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
- platform: windows
create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1
create_revision: f72efea43c3013323d1b95cff571f3c1caa37583
base_revision: f72efea43c3013323d1b95cff571f3c1caa37583
# User provided section

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

View file

@ -1,6 +1,7 @@
// ignore_for_file: use_build_context_synchronously, depend_on_referenced_packages
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:animations/animations.dart';
@ -10,7 +11,9 @@ import 'package:store_checker/store_checker.dart';
import 'package:flutter/services.dart';
import 'package:adguard_home_manager/widgets/bottom_nav_bar.dart';
import 'package:adguard_home_manager/widgets/menu_bar.dart';
import 'package:adguard_home_manager/widgets/update_modal.dart';
import 'package:adguard_home_manager/widgets/navigation_rail.dart';
import 'package:adguard_home_manager/providers/app_config_provider.dart';
import 'package:adguard_home_manager/models/github_release.dart';
@ -53,11 +56,23 @@ class _BaseState extends State<Base> with WidgetsBindingObserver {
}
Future<GitHubRelease?> checkInstallationSource() async {
Source installationSource = await StoreChecker.getSource;
if (installationSource != Source.IS_INSTALLED_FROM_PLAY_STORE) {
final result = await checkAppUpdatesGitHub();
if (result['result'] == 'success') {
if (updateExists(widget.appConfigProvider.getAppInfo!.version, result['body'].tagName)) {
final update = updateExists(widget.appConfigProvider.getAppInfo!.version, result['body'].tagName);
if (update == true) {
if (Platform.isAndroid) {
Source installationSource = await StoreChecker.getSource;
if (installationSource == Source.IS_INSTALLED_FROM_PLAY_STORE) {
return null;
}
else {
return result['body'];
}
}
else if (Platform.isIOS) {
return null;
}
else {
return result['body'];
}
}
@ -107,11 +122,14 @@ class _BaseState extends State<Base> with WidgetsBindingObserver {
final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
List<AppScreen> screens = serversProvider.selectedServer != null
? screensServerConnected
: screensSelectServer;
return AnnotatedRegion<SystemUiOverlayStyle>(
return CustomMenuBar(
child: AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarBrightness: Theme.of(context).brightness == Brightness.light
@ -126,7 +144,11 @@ class _BaseState extends State<Base> with WidgetsBindingObserver {
: Brightness.light,
),
child: Scaffold(
body: PageTransitionSwitcher(
body: Row(
children: [
if (width > 900) const SideNavigationRail(),
Expanded(
child: PageTransitionSwitcher(
duration: const Duration(milliseconds: 200),
transitionBuilder: (
(child, primaryAnimation, secondaryAnimation) => FadeThroughTransition(
@ -137,8 +159,14 @@ class _BaseState extends State<Base> with WidgetsBindingObserver {
),
child: screens[appConfigProvider.selectedScreen].body,
),
bottomNavigationBar: const BottomNavBar(),
),
],
),
bottomNavigationBar: width <= 900
? const BottomNavBar()
: null,
)
),
);
}
}

View file

@ -1,6 +1,10 @@
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';
import 'package:url_launcher/url_launcher.dart';
void openUrl(String url) async {
if (Platform.isAndroid || Platform.isIOS) {
FlutterWebBrowser.openWebPage(
url: url,
customTabsOptions: const CustomTabsOptions(
@ -14,4 +18,13 @@ void openUrl(String url) {
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",
"serverStatus": "Server status",
"connectionNotUpdated": "Connection not updated",
"ruleFilteringWidget": "Rule\nfiltering",
"safeBrowsingWidget": "Safe\nbrowsing",
"parentalFilteringWidget": "Parental\nfiltering",
"safeSearchWidget": "Safe\nsearch",
"ruleFilteringWidget": "Rule filtering",
"safeBrowsingWidget": "Safe browsing",
"parentalFilteringWidget": "Parental filtering",
"safeSearchWidget": "Safe search",
"ruleFiltering": "Rule filtering",
"safeBrowsing": "Safe browsing",
"parentalFiltering": "Parental filtering",
@ -606,5 +606,11 @@
"remainingTime": "Remaining time",
"safeSearchSettings": "Safe search settings",
"loadingSafeSearchSettings": "Loading safe search settings...",
"safeSearchSettingsNotLoaded": "Error when loading safe search settings."
"safeSearchSettingsNotLoaded": "Error when loading safe search settings.",
"loadingLogsSettings": "Loading logs settings...",
"selectOptionLeftColumn": "Select an option of the left column",
"selectClientLeftColumn": "Select a client of the left column",
"disableList": "Disable list",
"enableList": "Enable list",
"screens": "Screens"
}

View file

@ -45,10 +45,10 @@
"save": "Guardar",
"connectionNotUpdated": "Conexión no actualizada",
"serverStatus": "Estado del servidor",
"ruleFilteringWidget": "Bloqueo por\nfiltros",
"safeBrowsingWidget": "Navegación\nsegura",
"parentalFilteringWidget": "Control\nparental",
"safeSearchWidget": "Búsqueda\nsegura",
"ruleFilteringWidget": "Bloqueo por filtros",
"safeBrowsingWidget": "Navegación segura",
"parentalFilteringWidget": "Control parental",
"safeSearchWidget": "Búsqueda segura",
"ruleFiltering": "Bloqueo por filtros",
"safeBrowsing": "Navegación segura",
"parentalFiltering": "Control parental",
@ -606,5 +606,11 @@
"remainingTime": "Tiempo restante",
"safeSearchSettings": "Configuración de búsqueda segura",
"loadingSafeSearchSettings": "Cargando configuración de búsqueda segura...",
"safeSearchSettingsNotLoaded": "Error al cargar la configuración de búsqueda segura."
"safeSearchSettingsNotLoaded": "Error al cargar la configuración de búsqueda segura.",
"loadingLogsSettings": "Cargando configuración de registros...",
"selectOptionLeftColumn": "Selecciona una opción de la columna de la izquierda",
"selectClientLeftColumn": "Selecciona un cliente de la columna de la izquierda",
"disableList": "Deshabilitar lista",
"enableList": "Habilitar lista",
"screens": "Pantallas"
}

View file

@ -7,25 +7,33 @@ import 'package:provider/provider.dart';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:window_size/window_size.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/base.dart';
import 'package:adguard_home_manager/classes/http_override.dart';
import 'package:adguard_home_manager/services/database.dart';
import 'package:adguard_home_manager/services/db/database.dart';
import 'package:adguard_home_manager/constants/colors.dart';
import 'package:adguard_home_manager/providers/logs_provider.dart';
import 'package:adguard_home_manager/providers/app_config_provider.dart';
import 'package:adguard_home_manager/providers/servers_provider.dart';
import 'package:adguard_home_manager/config/theme.dart';
import 'package:adguard_home_manager/providers/servers_provider.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]
);
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
setWindowMinSize(const Size(500, 500));
}
if (Platform.isWindows || Platform.isLinux) {
sqfliteFfiInit();
databaseFactory = databaseFactoryFfi;
}
AppConfigProvider appConfigProvider = AppConfigProvider();
ServersProvider serversProvider = ServersProvider();

View file

@ -4,6 +4,7 @@ import 'package:flutter/scheduler.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:sqflite/sqlite_api.dart';
import 'package:adguard_home_manager/services/db/queries.dart';
import 'package:adguard_home_manager/functions/conversions.dart';
import 'package:adguard_home_manager/models/app_log.dart';
@ -16,6 +17,8 @@ class AppConfigProvider with ChangeNotifier {
int _selectedScreen = 0;
int? _selectedSettingsScreen;
bool _showingSnackbar = false;
int _selectedTheme = 0;
@ -118,6 +121,10 @@ class AppConfigProvider with ChangeNotifier {
return _doNotRememberVersion;
}
int? get selectedSettingsScreen {
return _selectedSettingsScreen;
}
void setDbInstance(Database db) {
_dbInstance = db;
}
@ -159,8 +166,19 @@ class AppConfigProvider with ChangeNotifier {
notifyListeners();
}
void setSelectedSettingsScreen({required int? screen, bool? notify}) {
_selectedSettingsScreen = screen;
if (notify == true) {
notifyListeners();
}
}
Future<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) {
_overrideSslCheck = status == true ? 1 : 0;
notifyListeners();
@ -172,7 +190,11 @@ class AppConfigProvider with ChangeNotifier {
}
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) {
_hideZeroValues = status == true ? 1 : 0;
notifyListeners();
@ -184,7 +206,11 @@ class AppConfigProvider with ChangeNotifier {
}
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) {
_showNameTimeLogs = status == true ? 1 : 0;
notifyListeners();
@ -196,7 +222,11 @@ class AppConfigProvider with ChangeNotifier {
}
Future<bool> setSelectedTheme(int value) async {
final updated = await _updateThemeDb(value);
final updated = await updateConfigQuery(
db: _dbInstance!,
column: 'theme',
value: value
);
if (updated == true) {
_selectedTheme = value;
notifyListeners();
@ -208,7 +238,11 @@ class AppConfigProvider with ChangeNotifier {
}
Future<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) {
_useDynamicColor = value;
notifyListeners();
@ -220,7 +254,11 @@ class AppConfigProvider with ChangeNotifier {
}
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) {
_useThemeColorForStatus = value;
notifyListeners();
@ -232,7 +270,11 @@ class AppConfigProvider with ChangeNotifier {
}
Future<bool> setStaticColor(int value) async {
final updated = await _updateStaticColorDb(value);
final updated = await updateConfigQuery(
db: _dbInstance!,
column: 'staticColor',
value: value
);
if (updated == true) {
_staticColor = value;
notifyListeners();
@ -244,109 +286,12 @@ class AppConfigProvider with ChangeNotifier {
}
Future<bool> setDoNotRememberVersion(String value) async {
try {
return await _dbInstance!.transaction((txn) async {
await txn.rawUpdate(
'UPDATE appConfig SET doNotRememberVersion = "$value"',
final updated = await updateConfigQuery(
db: _dbInstance!,
column: 'doNotRememberVersion',
value: value
);
_doNotRememberVersion = value;
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;
}
return updated;
}
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/filtering_status.dart';
import 'package:adguard_home_manager/models/clients_allowed_blocked.dart';
import 'package:adguard_home_manager/models/update_available.dart';
import 'package:adguard_home_manager/models/blocked_services.dart';
import 'package:adguard_home_manager/models/clients.dart';
import 'package:adguard_home_manager/models/server_status.dart';
import 'package:adguard_home_manager/models/server.dart';
import 'package:adguard_home_manager/models/update_available.dart';
import 'package:adguard_home_manager/services/http_requests.dart';
import 'package:adguard_home_manager/functions/time_server_disabled.dart';
import 'package:adguard_home_manager/functions/conversions.dart';
import 'package:adguard_home_manager/services/db/queries.dart';
import 'package:adguard_home_manager/functions/compare_versions.dart';
import 'package:adguard_home_manager/constants/enums.dart';
@ -33,6 +34,9 @@ class ServersProvider with ChangeNotifier {
loadStatus: LoadStatus.loading,
data: null
);
String? _searchTermClients;
List<AutoClient> _filteredActiveClients = [];
List<Client> _filteredAddedClients = [];
final Filtering _filtering = Filtering(
loadStatus: LoadStatus.loading,
@ -86,6 +90,18 @@ class ServersProvider with ChangeNotifier {
return _clients;
}
String? get searchTermClients {
return _searchTermClients;
}
List<AutoClient> get filteredActiveClients {
return _filteredActiveClients;
}
List<Client> get filteredAddedClients {
return _filteredAddedClients;
}
FilteringStatus? get filteringStatus {
return _filteringStatus;
}
@ -147,6 +163,43 @@ class ServersProvider with ChangeNotifier {
void setClientsData(ClientsData data) {
_clients.data = data;
if (_searchTermClients != null && _searchTermClients != '') {
_filteredActiveClients = _clients.data!.autoClientsData.where(
(client) => client.ip.contains(_searchTermClients!.toLowerCase()) || (client.name != null ? client.name!.contains(_searchTermClients!.toLowerCase()) : false)
).toList();
_filteredAddedClients = _clients.data!.clients.where(
(client) {
isContained(String value) => value.contains(value.toLowerCase());
return client.ids.any(isContained);
}
).toList();
}
else {
_filteredActiveClients = data.autoClientsData;
_filteredAddedClients = data.clients;
}
notifyListeners();
}
void setSearchTermClients(String? value) {
_searchTermClients = value;
if (value != null && value != '') {
if (_clients.data != null) {
_filteredActiveClients = _clients.data!.autoClientsData.where(
(client) => client.ip.contains(value.toLowerCase()) || (client.name != null ? client.name!.contains(value.toLowerCase()) : false)
).toList();
_filteredAddedClients = _clients.data!.clients.where(
(client) {
isContained(String value) => value.contains(value.toLowerCase());
return client.ids.any(isContained);
}
).toList();
}
}
else {
if (_clients.data != null) _filteredActiveClients = _clients.data!.autoClientsData;
if (_clients.data != null) _filteredAddedClients = _clients.data!.clients;
}
notifyListeners();
}
@ -249,7 +302,7 @@ class ServersProvider with ChangeNotifier {
}
Future<dynamic> createServer(Server server) async {
final saved = await saveServerIntoDb(server);
final saved = await saveServerQuery(_dbInstance!, server);
if (saved == null) {
if (server.defaultServer == true) {
final defaultServer = await setDefaultServer(server);
@ -274,7 +327,7 @@ class ServersProvider with ChangeNotifier {
}
Future<dynamic> setDefaultServer(Server server) async {
final updated = await setDefaultServerDb(server.id);
final updated = await setDefaultServerQuery(_dbInstance!, server.id);
if (updated == null) {
List<Server> newServers = _serversList.map((s) {
if (s.id == server.id) {
@ -296,7 +349,7 @@ class ServersProvider with ChangeNotifier {
}
Future<dynamic> editServer(Server server) async {
final result = await editServerDb(server);
final result = await editServerQuery(_dbInstance!, server);
if (result == null) {
List<Server> newServers = _serversList.map((s) {
if (s.id == server.id) {
@ -316,7 +369,7 @@ class ServersProvider with ChangeNotifier {
}
Future<bool> removeServer(Server server) async {
final result = await removeFromDb(server.id);
final result = await removeServerQuery(_dbInstance!, server.id);
if (result == true) {
_selectedServer = null;
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 {
setUpdateAvailableLoadStatus(LoadStatus.loading, true);
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
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:animations/animations.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_split_view/flutter_split_view.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/clients/client_screen.dart';
import 'package:adguard_home_manager/screens/clients/added_client_tile.dart';
import 'package:adguard_home_manager/screens/clients/remove_client_modal.dart';
import 'package:adguard_home_manager/screens/clients/fab.dart';
import 'package:adguard_home_manager/screens/clients/options_modal.dart';
@ -27,13 +31,19 @@ class AddedList extends StatefulWidget {
final LoadStatus loadStatus;
final List<Client> data;
final Future Function() fetchClients;
final void Function(Client) onClientSelected;
final Client? selectedClient;
final bool splitView;
const AddedList({
Key? key,
required this.scrollController,
required this.loadStatus,
required this.data,
required this.fetchClients
required this.fetchClients,
required this.onClientSelected,
this.selectedClient,
required this.splitView
}) : super(key: key);
@override
@ -69,6 +79,8 @@ class _AddedListState extends State<AddedList> {
final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
void confirmEditClient(Client client) async {
ProcessModal processModal = ProcessModal(context: context);
processModal.open(AppLocalizations.of(context)!.addingClient);
@ -130,6 +142,10 @@ class _AddedListState extends State<AddedList> {
clientsData.clients = clientsData.clients.where((c) => c.name != client.name).toList();
serversProvider.setClientsData(clientsData);
if (widget.splitView == true) {
SplitView.of(context).popUntil(0);
}
showSnacbkar(
context: context,
appConfigProvider: appConfigProvider,
@ -150,6 +166,20 @@ class _AddedListState extends State<AddedList> {
}
void openClientModal(Client client) {
if (width > 900 || !(Platform.isAndroid | Platform.isIOS)) {
showDialog(
barrierDismissible: false,
context: context,
builder: (BuildContext context) => ClientScreen(
onConfirm: confirmEditClient,
serverVersion: serversProvider.serverStatus.data!.serverVersion,
onDelete: deleteClient,
client: client,
dialog: true,
)
);
}
else {
Navigator.push(context, MaterialPageRoute(
fullscreenDialog: true,
builder: (BuildContext context) => ClientScreen(
@ -157,9 +187,11 @@ class _AddedListState extends State<AddedList> {
serverVersion: serversProvider.serverStatus.data!.serverVersion,
onDelete: deleteClient,
client: client,
dialog: false,
)
));
}
}
void openDeleteModal(Client client) {
showModal(
@ -181,9 +213,12 @@ class _AddedListState extends State<AddedList> {
}
return CustomTabContentList(
noSliver: !(Platform.isAndroid || Platform.isIOS),
listPadding: widget.splitView == true
? const EdgeInsets.only(top: 8)
: null,
loadingGenerator: () => SizedBox(
width: double.maxFinite,
height: MediaQuery.of(context).size.height-171,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
@ -202,104 +237,22 @@ class _AddedListState extends State<AddedList> {
),
),
itemsCount: widget.data.length,
contentWidget: (index) => ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
isThreeLine: true,
onLongPress: () => openOptionsModal(widget.data[index]),
onTap: () => openClientModal(widget.data[index]),
title: Padding(
padding: const EdgeInsets.only(bottom: 5),
child: Text(
widget.data[index].name,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.normal,
color: Theme.of(context).colorScheme.onSurface
),
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.data[index].ids.toString().replaceAll(RegExp(r'^\[|\]$'), ''),
style: TextStyle(
color: Theme.of(context).listTileTheme.textColor
),
),
const SizedBox(height: 7),
Row(
children: [
Icon(
Icons.filter_list_rounded,
size: 19,
color: widget.data[index].filteringEnabled == true
? appConfigProvider.useThemeColorForStatus == true
? Theme.of(context).colorScheme.primary
: Colors.green
: appConfigProvider.useThemeColorForStatus == true
? Colors.grey
: Colors.red,
),
const SizedBox(width: 10),
Icon(
Icons.vpn_lock_rounded,
size: 18,
color: widget.data[index].safebrowsingEnabled == true
? appConfigProvider.useThemeColorForStatus == true
? Theme.of(context).colorScheme.primary
: Colors.green
: appConfigProvider.useThemeColorForStatus == true
? Colors.grey
: Colors.red,
),
const SizedBox(width: 10),
Icon(
Icons.block,
size: 18,
color: widget.data[index].parentalEnabled == true
? appConfigProvider.useThemeColorForStatus == true
? Theme.of(context).colorScheme.primary
: Colors.green
: appConfigProvider.useThemeColorForStatus == true
? Colors.grey
: Colors.red,
),
const SizedBox(width: 10),
Icon(
Icons.search_rounded,
size: 19,
color: serverVersionIsAhead(
currentVersion: serversProvider.serverStatus.data!.serverVersion,
referenceVersion: 'v0.107.28',
referenceVersionBeta: 'v0.108.0-b.33'
) == true
? widget.data[index].safeSearch != null && widget.data[index].safeSearch!.enabled == true
? appConfigProvider.useThemeColorForStatus == true
? Theme.of(context).colorScheme.primary
: Colors.green
: appConfigProvider.useThemeColorForStatus == true
? Colors.grey
: Colors.red
: widget.data[index].safesearchEnabled == true
? appConfigProvider.useThemeColorForStatus == true
? Theme.of(context).colorScheme.primary
: Colors.green
: appConfigProvider.useThemeColorForStatus == true
? Colors.grey
: Colors.red,
)
],
)
],
),
contentWidget: (index) => AddedClientTile(
selectedClient: widget.selectedClient,
client: widget.data[index],
onTap: widget.onClientSelected,
onLongPress: openOptionsModal,
onEdit: openClientModal,
splitView: widget.splitView,
),
noData: SizedBox(
width: double.maxFinite,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
AppLocalizations.of(context)!.noClientsList,
textAlign: TextAlign.center,
style: TextStyle(
@ -307,6 +260,7 @@ class _AddedListState extends State<AddedList> {
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
const SizedBox(height: 30),
TextButton.icon(
onPressed: widget.fetchClients,
@ -318,7 +272,6 @@ class _AddedListState extends State<AddedList> {
),
errorGenerator: () => SizedBox(
width: double.maxFinite,
height: MediaQuery.of(context).size.height-171,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,

View file

@ -19,6 +19,7 @@ class ClientScreen extends StatefulWidget {
final String serverVersion;
final void Function(Client) onConfirm;
final void Function(Client)? onDelete;
final bool dialog;
const ClientScreen({
Key? key,
@ -26,6 +27,7 @@ class ClientScreen extends StatefulWidget {
required this.serverVersion,
required this.onConfirm,
this.onDelete,
required this.dialog
}) : super(key: key);
@override
@ -301,50 +303,12 @@ class _ClientScreenState extends State<ClientScreen> {
);
}
return Scaffold(
appBar: AppBar(
leading: IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.close)
),
title: Text(
widget.client != null
? AppLocalizations.of(context)!.client
: AppLocalizations.of(context)!.addClient
),
actions: [
if (widget.client == null || (widget.client != null && editMode == true)) IconButton(
onPressed: validValues == true
? () {
createClient();
Navigator.pop(context);
}
: null,
icon: Icon(
widget.client != null && editMode == true
? Icons.save_rounded
: Icons.check_rounded
),
tooltip: widget.client != null && editMode == true
? AppLocalizations.of(context)!.save
: AppLocalizations.of(context)!.confirm,
),
if (widget.client != null && editMode == false) IconButton(
onPressed: () => setState(() => editMode = true),
icon: const Icon(Icons.edit_rounded),
tooltip: AppLocalizations.of(context)!.edit,
),
if (widget.client != null) IconButton(
onPressed: openDeleteClientScreen,
icon: const Icon(Icons.delete_rounded),
tooltip: AppLocalizations.of(context)!.delete,
),
const SizedBox(width: 10),
],
),
body: ListView(
Widget content(bool withPaddingTop) {
return ListView(
padding: const EdgeInsets.only(top: 0),
children: [
const SizedBox(height: 24),
if (withPaddingTop == true) const SizedBox(height: 24),
if (withPaddingTop == false) const SizedBox(height: 6),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: TextFormField(
@ -693,10 +657,7 @@ class _ClientScreenState extends State<ClientScreen> {
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: editMode == true
? MediaQuery.of(context).size.width - 108
: MediaQuery.of(context).size.width - 40,
Expanded(
child: TextFormField(
enabled: editMode,
controller: controller['controller'],
@ -751,7 +712,125 @@ class _ClientScreenState extends State<ClientScreen> {
),
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_split_view/flutter_split_view.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/clients/clients_list.dart';
import 'package:adguard_home_manager/screens/clients/search_clients.dart';
import 'package:adguard_home_manager/screens/clients/logs_list_client.dart';
import 'package:adguard_home_manager/screens/clients/clients_desktop_view.dart';
import 'package:adguard_home_manager/screens/clients/added_list.dart';
import 'package:adguard_home_manager/models/app_log.dart';
@ -56,6 +61,9 @@ class _ClientsWidgetState extends State<ClientsWidget> with TickerProviderStateM
late TabController tabController;
final ScrollController scrollController = ScrollController();
bool searchMode = false;
final TextEditingController searchController = TextEditingController();
Future fetchClients() async {
widget.setLoadingStatus(LoadStatus.loading, false);
final result = await getClients(widget.server);
@ -90,21 +98,110 @@ class _ClientsWidgetState extends State<ClientsWidget> with TickerProviderStateM
@override
Widget build(BuildContext context) {
final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
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: 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
),
),
),
),
child: ClientsDesktopView(
serversProvider: serversProvider,
appConfigProvider: appConfigProvider,
fetchClients: fetchClients,
)
);
}
else {
if (!(Platform.isAndroid || Platform.isIOS)) {
return DefaultTabController(
length: 2,
child: NestedScrollView(
controller: scrollController,
headerSliverBuilder: ((context, innerBoxIsScrolled) {
return [
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
child: Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.clients),
pinned: true,
floating: true,
centerTitle: false,
forceElevated: innerBoxIsScrolled,
actions: [
if (serversProvider.clients.loadStatus == LoadStatus.loaded) ...[
IconButton(
@ -119,54 +216,89 @@ class _ClientsWidgetState extends State<ClientsWidget> with TickerProviderStateM
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,
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
),
Tab(
icon: const Icon(Icons.add_rounded),
text: AppLocalizations.of(context)!.added,
),
]
)
],
)
: 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: Container(
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
border: Border(
top: BorderSide(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.1)
)
)
),
child: TabBarView(
controller: tabController,
children: [
ClientsList(
scrollController: scrollController,
loadStatus: serversProvider.clients.loadStatus,
data: serversProvider.clients.loadStatus == LoadStatus.loaded
? serversProvider.clients.data!.autoClientsData : [],
fetchClients: fetchClients,
),
AddedList(
scrollController: scrollController,
loadStatus: serversProvider.clients.loadStatus,
data: serversProvider.clients.loadStatus == LoadStatus.loaded
? serversProvider.clients.data!.clients : [],
fetchClients: fetchClients,
),
]
)
),
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:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/widgets/custom_list_tile.dart';
import 'package:adguard_home_manager/screens/clients/active_client_tile.dart';
import 'package:adguard_home_manager/widgets/tab_content_list.dart';
import 'package:adguard_home_manager/models/clients.dart';
import 'package:adguard_home_manager/models/applied_filters.dart';
import 'package:adguard_home_manager/providers/app_config_provider.dart';
import 'package:adguard_home_manager/constants/enums.dart';
import 'package:adguard_home_manager/providers/logs_provider.dart';
class ClientsList extends StatelessWidget {
final ScrollController scrollController;
final LoadStatus loadStatus;
final List<AutoClient> data;
final Future Function() fetchClients;
final void Function(AutoClient) onClientSelected;
final AutoClient? selectedClient;
final bool splitView;
final bool sliver;
const ClientsList({
Key? key,
required this.scrollController,
required this.loadStatus,
required this.data,
required this.fetchClients
required this.fetchClients,
required this.onClientSelected,
this.selectedClient,
required this.splitView,
required this.sliver
}) : super(key: key);
@override
Widget build(BuildContext context) {
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final logsProvider = Provider.of<LogsProvider>(context);
return CustomTabContentList(
listPadding: splitView == true
? const EdgeInsets.only(top: 8)
: null,
noSliver: !sliver,
loadingGenerator: () => SizedBox(
width: double.maxFinite,
height: MediaQuery.of(context).size.height-171,
@ -52,31 +60,11 @@ class ClientsList extends StatelessWidget {
),
),
itemsCount: data.length,
contentWidget: (index) => CustomListTile(
title: data[index].name != ''
? data[index].name!
: data[index].ip,
subtitle: data[index].name != ''
? data[index].ip
: null,
trailing: Text(
data[index].source,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface
),
),
onTap: () {
logsProvider.setSearchText(null);
logsProvider.setSelectedClients([data[index].ip]);
logsProvider.setAppliedFilters(
AppliedFiters(
selectedResultStatus: 'all',
searchText: null,
clients: [data[index].ip]
)
);
appConfigProvider.setSelectedScreen(2);
},
contentWidget: (index) => ActiveClientTile(
client: data[index],
onTap: onClientSelected,
splitView: splitView,
selectedClient: selectedClient,
),
noData: SizedBox(
width: double.maxFinite,

View file

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

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,7 +64,11 @@ class _SafeSearchModalState extends State<SafeSearchModal> {
)
],
),
content: Wrap(
content: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 400
),
child: Wrap(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
@ -168,6 +172,7 @@ class _SafeSearchModalState extends State<SafeSearchModal> {
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),

View file

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

View file

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

View file

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

View file

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

View file

@ -1,17 +1,17 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import 'package:flutter_web_browser/flutter_web_browser.dart';
import 'package:adguard_home_manager/functions/open_url.dart';
import 'package:adguard_home_manager/constants/urls.dart';
import 'package:adguard_home_manager/providers/app_config_provider.dart';
class AddCustomRule extends StatefulWidget {
final void Function(String) onConfirm;
final bool dialog;
const AddCustomRule({
Key? key,
required this.onConfirm
required this.onConfirm,
required this.dialog
}) : super(key: key);
@override
@ -73,46 +73,14 @@ class _AddCustomRuleState extends State<AddCustomRule> {
return rule;
}
void openDocsPage() {
FlutterWebBrowser.openWebPage(
url: Urls.customRuleDocs,
customTabsOptions: const CustomTabsOptions(
instantAppsEnabled: true,
showTitle: true,
urlBarHidingEnabled: false,
),
safariVCOptions: const SafariViewControllerOptions(
barCollapsingEnabled: true,
dismissButtonStyle: SafariViewControllerDismissButtonStyle.close,
modalPresentationCapturesStatusBarAppearance: true,
)
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.addCustomRule),
actions: [
IconButton(
onPressed: checkValidValues() == true
? () {
Navigator.pop(context);
widget.onConfirm(buildRule());
}
: null,
icon: const Icon(Icons.check)
),
const SizedBox(width: 10)
],
),
body: ListView(
children: [
List<Widget> content() {
return [
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.symmetric(
@ -137,7 +105,7 @@ class _AddCustomRuleState extends State<AddCustomRule> {
),
],
),
const SizedBox(height: 30),
Container(height: 30),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: TextFormField(
@ -155,7 +123,7 @@ class _AddCustomRuleState extends State<AddCustomRule> {
),
),
),
const SizedBox(height: 30),
Container(height: 30),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: SegmentedButton(
@ -177,7 +145,7 @@ class _AddCustomRuleState extends State<AddCustomRule> {
onSelectionChanged: (value) => setState(() => preset = value.first),
),
),
const SizedBox(height: 20),
Container(height: 20),
Material(
color: Colors.transparent,
child: InkWell(
@ -206,7 +174,7 @@ class _AddCustomRuleState extends State<AddCustomRule> {
),
),
),
const SizedBox(height: 20),
Container(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Card(
@ -324,11 +292,11 @@ class _AddCustomRuleState extends State<AddCustomRule> {
),
),
),
const SizedBox(height: 20),
Container(height: 20),
Material(
color: Colors.transparent,
child: InkWell(
onTap: openDocsPage,
onTap: () => openUrl(Urls.customRuleDocs),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 10),
child: Row(
@ -356,9 +324,86 @@ class _AddCustomRuleState extends State<AddCustomRule> {
),
),
),
const SizedBox(height: 20)
Container(height: 20)
];
}
if (widget.dialog == true) {
return Dialog(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 500
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.clear_rounded),
tooltip: AppLocalizations.of(context)!.close,
),
const SizedBox(width: 8),
Text(
AppLocalizations.of(context)!.addCustomRule,
style: const TextStyle(
fontSize: 22
),
),
],
),
IconButton(
onPressed: checkValidValues() == true
? () {
Navigator.pop(context);
widget.onConfirm(buildRule());
}
: null,
icon: const Icon(Icons.check)
)
],
),
),
Flexible(
child: SingleChildScrollView(
child: Wrap(
alignment: WrapAlignment.center,
children: content(),
),
),
)
],
),
),
);
}
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 void Function({required String name, required String url, required String type})? onConfirm;
final void Function({required Filter list, required String type})? onEdit;
final bool dialog;
const AddListModal({
Key? key,
@ -17,6 +18,7 @@ class AddListModal extends StatefulWidget {
this.list,
this.onConfirm,
this.onEdit,
required this.dialog
}) : super(key: key);
@override
@ -68,24 +70,18 @@ class _AddListModalState extends State<AddListModal> {
@override
Widget build(BuildContext context) {
return Padding(
padding: MediaQuery.of(context).viewInsets,
child: Container(
height: Platform.isIOS ? 386 : 370,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28),
topRight: Radius.circular(28)
),
color: Theme.of(context).dialogBackgroundColor
),
child: Column(
Widget content() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: ListView(
physics: (Platform.isIOS ? 426 : 410) < MediaQuery.of(context).size.height
? const NeverScrollableScrollPhysics()
: null,
Flexible(
child: SingleChildScrollView(
child: Wrap(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 24),
@ -113,6 +109,10 @@ class _AddListModalState extends State<AddListModal> {
),
),
const SizedBox(height: 16),
],
),
],
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: TextFormField(
@ -129,7 +129,7 @@ class _AddListModalState extends State<AddListModal> {
),
),
),
const SizedBox(height: 30),
Container(height: 30),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: TextFormField(
@ -151,6 +151,7 @@ class _AddListModalState extends State<AddListModal> {
],
),
),
),
Padding(
padding: const EdgeInsets.all(24),
child: Row(
@ -197,8 +198,33 @@ class _AddListModalState extends State<AddListModal> {
),
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';
class BlockedServicesScreen extends StatelessWidget {
const BlockedServicesScreen({Key? key}) : super(key: key);
final bool dialog;
const BlockedServicesScreen({
Key? key,
required this.dialog
}) : super(key: key);
@override
Widget build(BuildContext context) {
@ -21,7 +26,8 @@ class BlockedServicesScreen extends StatelessWidget {
return BlockedServicesScreenWidget(
serversProvider: serversProvider,
appConfigProvider: appConfigProvider
appConfigProvider: appConfigProvider,
dialog: dialog,
);
}
}
@ -29,11 +35,13 @@ class BlockedServicesScreen extends StatelessWidget {
class BlockedServicesScreenWidget extends StatefulWidget {
final ServersProvider serversProvider;
final AppConfigProvider appConfigProvider;
final bool dialog;
const BlockedServicesScreenWidget({
Key? key,
required this.serversProvider,
required this.appConfigProvider,
required this.dialog
}) : super(key: key);
@override
@ -209,6 +217,55 @@ class _BlockedServicesScreenStateWidget extends State<BlockedServicesScreenWidge
}
}
if (widget.dialog == true) {
return Dialog(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 400
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
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),
@ -229,4 +286,5 @@ class _BlockedServicesScreenStateWidget extends State<BlockedServicesScreenWidge
),
);
}
}
}

View file

@ -10,7 +10,12 @@ import 'package:adguard_home_manager/services/http_requests.dart';
import 'package:adguard_home_manager/providers/servers_provider.dart';
class CheckHostModal extends StatefulWidget {
const CheckHostModal({Key? key}) : super(key: key);
final bool dialog;
const CheckHostModal({
Key? key,
required this.dialog
}) : super(key: key);
@override
State<CheckHostModal> createState() => _CheckHostModalState();
@ -117,26 +122,18 @@ class _CheckHostModalState extends State<CheckHostModal> {
}
}
return Padding(
padding: MediaQuery.of(context).viewInsets,
child: Container(
height: 330,
width: double.maxFinite,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28),
topRight: Radius.circular(28),
),
color: Theme.of(context).dialogBackgroundColor
),
child: Center(
child: Column(
Widget content() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: ListView(
physics: 350 < MediaQuery.of(context).size.height
? const NeverScrollableScrollPhysics()
: null,
Flexible(
child: SingleChildScrollView(
child: Wrap(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 24),
@ -156,6 +153,10 @@ class _CheckHostModalState extends State<CheckHostModal> {
),
),
const SizedBox(height: 16),
],
),
],
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: TextFormField(
@ -174,22 +175,15 @@ class _CheckHostModalState extends State<CheckHostModal> {
),
),
if (resultWidget != null) Padding(
padding: const EdgeInsets.only(
top: 20,
left: 20,
right: 20
),
padding: const EdgeInsets.all(24),
child: resultWidget,
),
if (resultWidget == null) Padding(
padding: const EdgeInsets.only(
top: 20,
left: 20,
right: 20
),
padding: const EdgeInsets.all(24),
child: Center(
child: Text(
AppLocalizations.of(context)!.insertDomain,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 16,
),
@ -199,6 +193,7 @@ class _CheckHostModalState extends State<CheckHostModal> {
],
),
),
),
Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
@ -234,9 +229,34 @@ class _CheckHostModalState extends State<CheckHostModal> {
],
)
],
);
}
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/rendering.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/filters/fab.dart';
import 'package:adguard_home_manager/screens/filters/remove_custom_rule_modal.dart';
import 'package:adguard_home_manager/screens/filters/add_button.dart';
import 'package:adguard_home_manager/widgets/tab_content_list.dart';
import 'package:adguard_home_manager/functions/snackbar.dart';
import 'package:adguard_home_manager/constants/enums.dart';
import 'package:adguard_home_manager/models/filtering.dart';
import 'package:adguard_home_manager/providers/app_config_provider.dart';
import 'package:adguard_home_manager/services/http_requests.dart';
import 'package:adguard_home_manager/providers/servers_provider.dart';
import 'package:adguard_home_manager/classes/process_modal.dart';
class CustomRulesList extends StatefulWidget {
final LoadStatus loadStatus;
final ScrollController scrollController;
final List<String> data;
final Future<void> Function() fetchData;
final void Function(String) onRemoveCustomRule;
const CustomRulesList({
Key? key,
required this.loadStatus,
required this.scrollController,
required this.data,
required this.fetchData
required this.fetchData,
required this.onRemoveCustomRule
}) : super(key: key);
@override
@ -61,52 +55,6 @@ class _CustomRulesListState extends State<CustomRulesList> {
@override
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) {
final regex = RegExp(r'^(!|#).*$');
if (regex.hasMatch(value)) {
@ -184,7 +132,7 @@ class _CustomRulesListState extends State<CustomRulesList> {
),
subtitle: generateSubtitle(widget.data[index]),
trailing: IconButton(
onPressed: () => openRemoveCustomRuleModal(widget.data[index]),
onPressed: () => widget.onRemoveCustomRule(widget.data[index]),
icon: const Icon(Icons.delete)
),
),
@ -239,8 +187,12 @@ class _CustomRulesListState extends State<CustomRulesList> {
),
loadStatus: widget.loadStatus,
onRefresh: widget.fetchData,
fab: const FiltersFab(
fab: AddFiltersButton(
type: 'custom_rule',
widget: (fn) => FloatingActionButton(
onPressed: fn,
child: const Icon(Icons.add),
),
),
fabVisible: isVisible,
);

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
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/filters/filters_list.dart';
import 'package:adguard_home_manager/screens/filters/check_host_modal.dart';
import 'package:adguard_home_manager/screens/filters/custom_rules_list.dart';
import 'package:adguard_home_manager/screens/filters/filters_tabs_view.dart';
import 'package:adguard_home_manager/screens/filters/filters_triple_column.dart';
import 'package:adguard_home_manager/screens/filters/list_details_screen.dart';
import 'package:adguard_home_manager/screens/filters/remove_custom_rule_modal.dart';
import 'package:adguard_home_manager/screens/filters/blocked_services_screen.dart';
import 'package:adguard_home_manager/screens/filters/update_interval_lists_modal.dart';
import 'package:adguard_home_manager/functions/snackbar.dart';
import 'package:adguard_home_manager/classes/process_modal.dart';
import 'package:adguard_home_manager/providers/app_config_provider.dart';
import 'package:adguard_home_manager/models/filtering.dart';
import 'package:adguard_home_manager/constants/enums.dart';
import 'package:adguard_home_manager/services/http_requests.dart';
import 'package:adguard_home_manager/models/clients.dart';
@ -47,10 +52,7 @@ class FiltersWidget extends StatefulWidget {
State<FiltersWidget> createState() => _FiltersWidgetState();
}
class _FiltersWidgetState extends State<FiltersWidget> with TickerProviderStateMixin {
late TabController tabController;
final ScrollController scrollController = ScrollController();
class _FiltersWidgetState extends State<FiltersWidget> {
Future fetchFilters() async {
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
void initState() {
fetchFilters();
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
@ -89,6 +85,8 @@ class _FiltersWidgetState extends State<FiltersWidget> with TickerProviderStateM
final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
void fetchUpdateLists() async {
ProcessModal processModal = ProcessModal(context: context);
processModal.open(AppLocalizations.of(context)!.updatingLists);
@ -139,12 +137,24 @@ class _FiltersWidgetState extends State<FiltersWidget> with TickerProviderStateM
void showCheckHostModal() {
Future.delayed(const Duration(seconds: 0), () {
if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) {
showDialog(
context: context,
builder: (context) => const CheckHostModal(
dialog: true,
),
);
}
else {
showModalBottomSheet(
context: context,
builder: (context) => const CheckHostModal(),
builder: (context) => const CheckHostModal(
dialog: false,
),
backgroundColor: Colors.transparent,
isScrollControlled: true,
);
}
});
}
@ -216,29 +226,98 @@ class _FiltersWidgetState extends State<FiltersWidget> with TickerProviderStateM
void openBlockedServicesModal() {
Future.delayed(const Duration(seconds: 0), () {
if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) {
showDialog(
context: context,
builder: (context) => const BlockedServicesScreen(
dialog: true,
),
barrierDismissible: false
);
}
else {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const BlockedServicesScreen(),
builder: (context) => const BlockedServicesScreen(
dialog: false,
),
)
);
}
});
}
return DefaultTabController(
length: 3,
child: NestedScrollView(
controller: scrollController,
headerSliverBuilder: ((context, innerBoxIsScrolled) {
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),
)
);
}
void openListDetails(Filter filter, String type) {
if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) {
showDialog(
context: context,
builder: (context) => ListDetailsScreen(
list: filter,
type: type,
dialog: true,
),
barrierDismissible: false
);
}
else {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ListDetailsScreen(
list: filter,
type: type,
dialog: false,
)
)
);
}
}
List<Widget> actions() {
if (serversProvider.filtering.loadStatus == LoadStatus.loaded) {
return [
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
title: Text(AppLocalizations.of(context)!.filters),
pinned: true,
floating: true,
forceElevated: innerBoxIsScrolled,
centerTitle: false,
actions: serversProvider.filtering.loadStatus == LoadStatus.loaded ? [
IconButton(
onPressed: enableDisableFiltering,
tooltip: serversProvider.filtering.data!.enabled == true
@ -279,17 +358,31 @@ class _FiltersWidgetState extends State<FiltersWidget> with TickerProviderStateM
),
IconButton(
onPressed: () {
if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) {
showDialog(
context: context,
builder: (context) => UpdateIntervalListsModal(
interval: serversProvider.filtering.data!.interval,
onChange: setUpdateFrequency,
dialog: true,
),
);
}
else {
showModalBottomSheet(
context: context,
builder: (context) => UpdateIntervalListsModal(
interval: serversProvider.filtering.data!.interval,
onChange: setUpdateFrequency
onChange: setUpdateFrequency,
dialog: false,
),
backgroundColor: Colors.transparent,
isScrollControlled: true
);
}
},
icon: const Icon(Icons.update_rounded)
icon: const Icon(Icons.update_rounded),
tooltip: AppLocalizations.of(context)!.updateFrequency,
),
PopupMenuButton(
itemBuilder: (context) => [
@ -326,59 +419,29 @@ class _FiltersWidgetState extends State<FiltersWidget> with TickerProviderStateM
]
),
const SizedBox(width: 5),
] : [],
bottom: TabBar(
controller: tabController,
isScrollable: false,
unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant,
tabs: [
Tab(
icon: const Icon(Icons.verified_user_rounded),
text: AppLocalizations.of(context)!.whitelists,
),
Tab(
icon: const Icon(Icons.gpp_bad_rounded),
text: AppLocalizations.of(context)!.blacklist,
),
Tab(
icon: const Icon(Icons.shield_rounded),
text: AppLocalizations.of(context)!.customRules,
),
]
)
),
)
];
}),
body: TabBarView(
controller: tabController,
children: [
FiltersList(
loadStatus: serversProvider.filtering.loadStatus,
scrollController: scrollController,
type: 'whitelist',
data: serversProvider.filtering.loadStatus == LoadStatus.loaded
? serversProvider.filtering.data!.whitelistFilters : [],
fetchData: fetchFilters,
),
FiltersList(
loadStatus: serversProvider.filtering.loadStatus,
scrollController: scrollController,
type: 'blacklist',
data: serversProvider.filtering.loadStatus == LoadStatus.loaded
? serversProvider.filtering.data!.filters : [],
fetchData: fetchFilters,
),
CustomRulesList(
loadStatus: serversProvider.filtering.loadStatus,
scrollController: scrollController,
data: serversProvider.filtering.loadStatus == LoadStatus.loaded
? serversProvider.filtering.data!.userRules : [],
fetchData: fetchFilters,
),
]
)
)
}
else {
return [];
}
}
if (width > 1200) {
return FiltersTripleColumn(
onRemoveCustomRule: openRemoveCustomRuleModal,
onOpenDetailsModal: openListDetails,
actions: actions(),
refreshData: fetchFilters,
);
}
else {
return FiltersTabsView(
appConfigProvider: appConfigProvider,
fetchFilters: fetchFilters,
actions: actions(),
onRemoveCustomRule: openRemoveCustomRuleModal,
onOpenDetailsModal: openListDetails,
);
}
}
}

View file

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

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:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/filters/filter_list_tile.dart';
import 'package:adguard_home_manager/screens/filters/add_list_modal.dart';
import 'package:adguard_home_manager/screens/filters/delete_list_modal.dart';
import 'package:adguard_home_manager/widgets/custom_list_tile.dart';
import 'package:adguard_home_manager/functions/format_time.dart';
import 'package:adguard_home_manager/providers/app_config_provider.dart';
@ -23,11 +23,13 @@ import 'package:adguard_home_manager/models/filtering.dart';
class ListDetailsScreen extends StatefulWidget {
final Filter list;
final String type;
final bool dialog;
const ListDetailsScreen({
Key? key,
required this.list,
required this.type,
required this.dialog
}) : super(key: key);
@override
@ -68,6 +70,8 @@ class _ListDetailsScreenState extends State<ListDetailsScreen> {
final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
void enableDisableList(Filter list, bool newStatus) async {
ProcessModal processModal = ProcessModal(context: context);
processModal.open(
@ -216,22 +220,122 @@ class _ListDetailsScreenState extends State<ListDetailsScreen> {
}
}
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.listDetails),
actions: [
List<Widget> content() {
return [
CustomListTile(
icon: Icons.shield_rounded,
title: AppLocalizations.of(context)!.currentStatus,
subtitleWidget: Text(
enabled == true
? AppLocalizations.of(context)!.enabled
: AppLocalizations.of(context)!.disabled,
style: TextStyle(
color: enabled == true
? appConfigProvider.useThemeColorForStatus == true
? Theme.of(context).colorScheme.primary
: Colors.green
: appConfigProvider.useThemeColorForStatus == true
? Colors.grey
: Colors.red,
fontWeight: FontWeight.w500
),
),
padding: widget.dialog == true
? const EdgeInsets.symmetric(
horizontal: 24,
vertical: 8
)
: null,
),
CustomListTile(
icon: Icons.badge_rounded,
title: AppLocalizations.of(context)!.name,
subtitle: name,
padding: widget.dialog == true
? const EdgeInsets.symmetric(
horizontal: 24,
vertical: 8
)
: null,
),
CustomListTile(
icon: Icons.link_rounded,
title: "URL",
subtitle: widget.list.url,
padding: widget.dialog == true
? const EdgeInsets.symmetric(
horizontal: 24,
vertical: 8
)
: null,
),
CustomListTile(
icon: Icons.list_rounded,
title: AppLocalizations.of(context)!.rules,
subtitle: widget.list.rulesCount.toString(),
padding: widget.dialog == true
? const EdgeInsets.symmetric(
horizontal: 24,
vertical: 8
)
: null,
),
CustomListTile(
icon: Icons.shield_rounded,
title: AppLocalizations.of(context)!.listType,
subtitle: widget.type == 'whitelist'
? AppLocalizations.of(context)!.whitelist
: AppLocalizations.of(context)!.blacklist,
padding: widget.dialog == true
? const EdgeInsets.symmetric(
horizontal: 24,
vertical: 8
)
: null,
),
if (widget.list.lastUpdated != null) CustomListTile(
icon: Icons.schedule_rounded,
title: AppLocalizations.of(context)!.latestUpdate,
subtitle: convertTimestampLocalTimezone(widget.list.lastUpdated!, 'dd-MM-yyyy HH:mm'),
padding: widget.dialog == true
? const EdgeInsets.symmetric(
horizontal: 24,
vertical: 8
)
: null,
),
if (widget.dialog == true) Container(height: 16)
];
}
List<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(
context: context,
builder: (ctx) => AddListModal(
list: widget.list,
type: widget.type,
onEdit: confirmEditList
onEdit: confirmEditList,
dialog: false,
),
isScrollControlled: true,
backgroundColor: Colors.transparent
)
}
},
icon: const Icon(Icons.edit),
tooltip: AppLocalizations.of(context)!.edit,
@ -249,55 +353,80 @@ class _ListDetailsScreenState extends State<ListDetailsScreen> {
tooltip: AppLocalizations.of(context)!.delete,
),
const SizedBox(width: 10),
];
}
if (widget.dialog == true) {
return Dialog(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 500
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.clear_rounded),
tooltip: AppLocalizations.of(context)!.close,
),
const SizedBox(width: 8),
Text(
AppLocalizations.of(context)!.listDetails,
style: const TextStyle(
fontSize: 22
),
)
],
),
Row(
children: [
IconButton(
onPressed: () => enableDisableList(widget.list, !enabled),
icon: Icon(
enabled == true
? Icons.gpp_bad_rounded
: Icons.verified_user_rounded,
),
tooltip: enabled == true
? AppLocalizations.of(context)!.disableList
: AppLocalizations.of(context)!.enableList,
),
...actions()
],
)
],
),
),
Flexible(
child: SingleChildScrollView(
child: Wrap(
children: content(),
),
)
)
],
),
)
);
}
else {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.listDetails),
actions: actions(),
),
body: Stack(
children: [
ListView(
children: [
FilterListTile(
icon: Icons.shield_rounded,
title: AppLocalizations.of(context)!.currentStatus,
subtitle: enabled == true
? AppLocalizations.of(context)!.enabled
: AppLocalizations.of(context)!.disabled,
color: enabled == true
? appConfigProvider.useThemeColorForStatus == true
? Theme.of(context).colorScheme.primary
: Colors.green
: appConfigProvider.useThemeColorForStatus == true
? Colors.grey
: Colors.red,
bold: true,
),
FilterListTile(
icon: Icons.badge_rounded,
title: AppLocalizations.of(context)!.name,
subtitle: name
),
FilterListTile(
icon: Icons.link_rounded,
title: "URL",
subtitle: widget.list.url
),
FilterListTile(
icon: Icons.list_rounded,
title: AppLocalizations.of(context)!.rules,
subtitle: widget.list.rulesCount.toString()
),
FilterListTile(
icon: Icons.shield_rounded,
title: AppLocalizations.of(context)!.listType,
subtitle: widget.type == 'whitelist'
? AppLocalizations.of(context)!.whitelist
: AppLocalizations.of(context)!.blacklist,
),
if (widget.list.lastUpdated != null) FilterListTile(
icon: Icons.schedule_rounded,
title: AppLocalizations.of(context)!.latestUpdate,
subtitle: convertTimestampLocalTimezone(widget.list.lastUpdated!, 'dd-MM-yyyy HH:mm'),
),
],
children: content(),
),
AnimatedPositioned(
duration: const Duration(milliseconds: 100),
@ -320,4 +449,5 @@ class _ListDetailsScreenState extends State<ListDetailsScreen> {
),
);
}
}
}

View file

@ -9,11 +9,13 @@ import 'package:adguard_home_manager/widgets/option_box.dart';
class UpdateIntervalListsModal extends StatefulWidget {
final int interval;
final void Function(int) onChange;
final bool dialog;
const UpdateIntervalListsModal({
Key? key,
required this.interval,
required this.onChange,
required this.dialog
}) : super(key: key);
@override
@ -39,25 +41,18 @@ class _UpdateIntervalListsModalState extends State<UpdateIntervalListsModal> {
Widget build(BuildContext context) {
final MediaQueryData mediaQueryData = MediaQuery.of(context);
return Padding(
padding: mediaQueryData.viewInsets,
child: Container(
height: Platform.isIOS ? 406 : 390,
decoration: BoxDecoration(
color: Theme.of(context).dialogBackgroundColor,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28),
topRight: Radius.circular(28)
),
),
child: Column(
Widget content() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: ListView(
physics: (Platform.isIOS ? 426 : 410) < MediaQuery.of(context).size.height
? const NeverScrollableScrollPhysics()
: null,
Flexible(
child: SingleChildScrollView(
child: Wrap(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 24),
@ -72,7 +67,6 @@ class _UpdateIntervalListsModalState extends State<UpdateIntervalListsModal> {
horizontal: 24,
vertical: 16
),
width: double.maxFinite,
child: Text(
AppLocalizations.of(context)!.updateFrequency,
textAlign: TextAlign.center,
@ -83,22 +77,19 @@ class _UpdateIntervalListsModalState extends State<UpdateIntervalListsModal> {
),
),
),
SizedBox(
width: double.maxFinite,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Container(
width: (mediaQueryData.size.width-70)/2,
margin: const EdgeInsets.only(
top: 10,
right: 5,
bottom: 5
],
)
],
),
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,
@ -118,13 +109,11 @@ class _UpdateIntervalListsModalState extends State<UpdateIntervalListsModal> {
),
),
),
Container(
width: (mediaQueryData.size.width-70)/2,
margin: const EdgeInsets.only(
top: 10,
left: 5,
bottom: 5
),
FractionallySizedBox(
widthFactor: 0.5,
child: Padding(
padding: const EdgeInsets.only(left: 6),
child: OptionBox(
optionsValue: selectedOption,
itemValue: 1,
@ -144,18 +133,11 @@ class _UpdateIntervalListsModalState extends State<UpdateIntervalListsModal> {
),
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Container(
width: (mediaQueryData.size.width-70)/2,
margin: const EdgeInsets.only(
top: 5,
right: 5,
bottom: 5
),
FractionallySizedBox(
widthFactor: 0.5,
child: Padding(
padding: const EdgeInsets.only(right: 6),
child: OptionBox(
optionsValue: selectedOption,
itemValue: 12,
@ -175,13 +157,11 @@ class _UpdateIntervalListsModalState extends State<UpdateIntervalListsModal> {
),
),
),
Container(
width: (mediaQueryData.size.width-70)/2,
margin: const EdgeInsets.only(
top: 5,
left: 5,
bottom: 5
),
FractionallySizedBox(
widthFactor: 0.5,
child: Padding(
padding: const EdgeInsets.only(left: 6),
child: OptionBox(
optionsValue: selectedOption,
itemValue: 24,
@ -201,18 +181,11 @@ class _UpdateIntervalListsModalState extends State<UpdateIntervalListsModal> {
),
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Container(
width: (mediaQueryData.size.width-70)/2,
margin: const EdgeInsets.only(
top: 5,
right: 5,
bottom: 10
),
FractionallySizedBox(
widthFactor: 0.5,
child: Padding(
padding: const EdgeInsets.only(right: 6),
child: OptionBox(
optionsValue: selectedOption,
itemValue: 72,
@ -232,13 +205,11 @@ class _UpdateIntervalListsModalState extends State<UpdateIntervalListsModal> {
),
),
),
Container(
width: (mediaQueryData.size.width-70)/2,
margin: const EdgeInsets.only(
top: 5,
left: 5,
bottom: 10
),
FractionallySizedBox(
widthFactor: 0.5,
child: Padding(
padding: const EdgeInsets.only(left: 6),
child: OptionBox(
optionsValue: selectedOption,
itemValue: 168,
@ -258,15 +229,14 @@ class _UpdateIntervalListsModalState extends State<UpdateIntervalListsModal> {
),
),
),
],
),
],
),
),
),
)
],
),
),
),
Padding(
padding: const EdgeInsets.all(24),
child: Row(
@ -301,8 +271,34 @@ class _UpdateIntervalListsModalState extends State<UpdateIntervalListsModal> {
),
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,14 +48,17 @@ class HomeChart extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
Flexible(
child: Text(
label,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurface
),
),
),
!isEmpty
? Column(
crossAxisAlignment: CrossAxisAlignment.end,

View file

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

View file

@ -55,6 +55,8 @@ class _HomeState extends State<Home> {
final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
Widget status() {
switch (serversProvider.serverStatus.loadStatus) {
case 0:
@ -92,38 +94,53 @@ class _HomeState extends State<Home> {
),
const SizedBox(height: 20),
HomeChart(
Wrap(
children: [
FractionallySizedBox(
widthFactor: width > 700 ? 0.5 : 1,
child: HomeChart(
data: serversProvider.serverStatus.data!.stats.dnsQueries,
label: AppLocalizations.of(context)!.dnsQueries,
primaryValue: intFormat(serversProvider.serverStatus.data!.stats.numDnsQueries, Platform.localeName),
secondaryValue: "${doubleFormat(serversProvider.serverStatus.data!.stats.avgProcessingTime*1000, Platform.localeName)} ms",
color: Colors.blue,
),
HomeChart(
),
FractionallySizedBox(
widthFactor: width > 700 ? 0.5 : 1,
child: HomeChart(
data: serversProvider.serverStatus.data!.stats.blockedFiltering,
label: AppLocalizations.of(context)!.blockedFilters,
primaryValue: intFormat(serversProvider.serverStatus.data!.stats.numBlockedFiltering, Platform.localeName),
secondaryValue: "${serversProvider.serverStatus.data!.stats.numDnsQueries > 0 ? doubleFormat((serversProvider.serverStatus.data!.stats.numBlockedFiltering/serversProvider.serverStatus.data!.stats.numDnsQueries)*100, Platform.localeName) : 0}%",
color: Colors.red,
),
HomeChart(
),
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,
),
HomeChart(
),
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,
),
),
],
),
if (width <= 700) ...[
TopItems(
label: AppLocalizations.of(context)!.topQueriedDomains,
data: serversProvider.serverStatus.data!.stats.topQueriedDomains,
@ -136,6 +153,7 @@ class _HomeState extends State<Home> {
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.2),
),
),
const SizedBox(height: 20),
TopItems(
@ -159,6 +177,56 @@ class _HomeState extends State<Home> {
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',
),
),
),
],
),
],
)
],
);
case 2:

View file

@ -15,7 +15,12 @@ import 'package:adguard_home_manager/services/http_requests.dart';
import 'package:adguard_home_manager/providers/servers_provider.dart';
class ManagementModal extends StatefulWidget {
const ManagementModal({Key? key}) : super(key: key);
final bool dialog;
const ManagementModal({
Key? key,
required this.dialog
}) : super(key: key);
@override
State<ManagementModal> createState() => _ManagementModalState();
@ -364,18 +369,8 @@ class _ManagementModalState extends State<ManagementModal> with SingleTickerProv
);
}
return SafeArea(
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28),
topRight: Radius.circular(28)
)
),
child: Wrap(
children: [
Row(
Widget header() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
@ -403,9 +398,13 @@ class _ManagementModalState extends State<ManagementModal> with SingleTickerProv
],
),
],
),
);
}
List<Widget> toggles() {
return [
mainSwitch(),
const SizedBox(height: 10),
Container(height: 10),
smallSwitch(
AppLocalizations.of(context)!.ruleFiltering,
Icons.filter_list_rounded,
@ -434,6 +433,28 @@ class _ManagementModalState extends State<ManagementModal> with SingleTickerProv
(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(
@ -452,4 +473,44 @@ class _ManagementModalState extends State<ManagementModal> with SingleTickerProv
),
);
}
else {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28),
topRight: Radius.circular(28)
)
),
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)
],
),
);
}
}
}

View file

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

View file

@ -44,7 +44,8 @@ class StatusBox extends StatelessWidget {
: Colors.grey.computeLuminance() > 0.5 ? Colors.black : Colors.white,
),
const SizedBox(width: 12),
Text(
Flexible(
child: Text(
label,
style: TextStyle(
color: appConfigProvider.useThemeColorForStatus == true
@ -52,6 +53,7 @@ class StatusBox extends StatelessWidget {
: 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
import 'dart:io';
import 'package:adguard_home_manager/screens/top_items/top_items_modal.dart';
import 'package:animations/animations.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
@ -35,6 +39,8 @@ class TopItems extends StatelessWidget {
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final logsProvider = Provider.of<LogsProvider>(context);
final width = MediaQuery.of(context).size.width;
bool? getIsBlocked() {
if (type == 'topBlockedDomains') {
return true;
@ -266,7 +272,21 @@ class TopItems extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.of(context).push(
onPressed: () => {
if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => TopItemsModal(
type: type,
title: label,
isClient: clients,
data: generateData(),
)
)
}
else {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => TopItemsScreen(
type: type,
@ -275,7 +295,9 @@ class TopItems extends StatelessWidget {
data: generateData(),
)
)
),
)
}
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [

View file

@ -8,10 +8,12 @@ import 'package:adguard_home_manager/providers/logs_provider.dart';
class ClientsModal extends StatefulWidget {
final List<String>? value;
final bool dialog;
const ClientsModal({
Key? key,
required this.value
required this.value,
required this.dialog
}) : super(key: key);
@override
@ -94,18 +96,11 @@ class _ClientsModalState extends State<ClientsModal> {
});
}
return Container(
height: height >= (logsProvider.clients!.length*64) == true
? logsProvider.clients!.length*64
: height-50,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28),
topRight: Radius.circular(28)
),
color: Theme.of(context).dialogBackgroundColor
),
child: Column(
Widget content() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Column(
children: [
Padding(
padding: const EdgeInsets.only(
@ -127,11 +122,10 @@ class _ClientsModalState extends State<ClientsModal> {
),
),
const SizedBox(height: 16),
Expanded(
],
),
Flexible(
child: ListView.builder(
physics: height >= (logsProvider.clients!.length*64) == true
? const NeverScrollableScrollPhysics()
: null,
itemCount: logsProvider.clients!.length,
itemBuilder: (context, index) => listItem(
label: logsProvider.clients![index].ip,
@ -150,7 +144,7 @@ class _ClientsModalState extends State<ClientsModal> {
}
}
)
),
)
),
Padding(
padding: const EdgeInsets.all(24),
@ -176,7 +170,35 @@ class _ClientsModalState extends State<ClientsModal> {
),
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 {
final String value;
final bool dialog;
const FilterStatusModal({
Key? key,
required this.value
required this.value,
required this.dialog
}) : super(key: key);
@override
@ -31,8 +33,6 @@ class _FilterStatusModalState extends State<FilterStatusModal> {
Widget build(BuildContext context) {
final logsProvider = Provider.of<LogsProvider>(context);
final height = MediaQuery.of(context).size.height;
void apply() async {
logsProvider.setSelectedResultStatus(selectedResultStatus);
@ -83,18 +83,18 @@ class _FilterStatusModalState extends State<FilterStatusModal> {
);
}
return Container(
height: height >= (Platform.isIOS ? 736 : 720) == true
? (Platform.isIOS ? 736 : 720)
: height-25,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28),
topRight: Radius.circular(28)
),
color: Theme.of(context).dialogBackgroundColor
),
child: Column(
Widget content() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: SingleChildScrollView(
child: Wrap(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
children: [
Padding(
padding: const EdgeInsets.only(
@ -115,13 +115,11 @@ class _FilterStatusModalState extends State<FilterStatusModal> {
color: Theme.of(context).colorScheme.onSurface
),
),
const SizedBox(height: 16),
Expanded(
child: ListView(
physics: height >= 720 == true
? const NeverScrollableScrollPhysics()
: null,
children: [
],
)
],
),
Container(height: 16),
filterStatusListItem(
id: "all",
icon: Icons.shield_rounded,
@ -174,6 +172,7 @@ class _FilterStatusModalState extends State<FilterStatusModal> {
],
),
),
),
Padding(
padding: const EdgeInsets.all(24),
child: Row(
@ -188,7 +187,30 @@ class _FilterStatusModalState extends State<FilterStatusModal> {
),
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 {
final Log log;
final bool dialog;
const LogDetailsScreen({
Key? key,
required this.log
required this.log,
required this.dialog
}) : super(key: key);
@override
@ -105,25 +107,8 @@ class LogDetailsScreen extends StatelessWidget {
}
}
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.logDetails),
actions: [
IconButton(
onPressed: () => blockUnblock(log, getFilteredStatus(context, appConfigProvider, log.reason, true)['filtered'] == true ? 'unblock' : 'block'),
icon: Icon(
getFilteredStatus(context, appConfigProvider, log.reason, true)['filtered'] == true
? Icons.check_circle_rounded
: Icons.block
),
tooltip: getFilteredStatus(context, appConfigProvider, log.reason, true)['filtered'] == true
? AppLocalizations.of(context)!.unblockDomain
: AppLocalizations.of(context)!.blockDomain,
),
const SizedBox(width: 10)
],
),
body: ListView(
Widget content() {
return ListView(
children: [
SectionLabel(label: AppLocalizations.of(context)!.status),
LogListTile(
@ -247,7 +232,87 @@ class LogDetailsScreen extends StatelessWidget {
)).toList()
]
],
);
}
if (dialog) {
return Dialog(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 500
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.clear_rounded)
),
const SizedBox(width: 16),
Text(
AppLocalizations.of(context)!.logDetails,
style: const TextStyle(
fontSize: 22
),
),
],
),
Row(
children: [
IconButton(
onPressed: () => blockUnblock(log, getFilteredStatus(context, appConfigProvider, log.reason, true)['filtered'] == true ? 'unblock' : 'block'),
icon: Icon(
getFilteredStatus(context, appConfigProvider, log.reason, true)['filtered'] == true
? Icons.check_circle_rounded
: Icons.block
),
tooltip: getFilteredStatus(context, appConfigProvider, log.reason, true)['filtered'] == true
? AppLocalizations.of(context)!.unblockDomain
: AppLocalizations.of(context)!.blockDomain,
),
],
)
],
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: content(),
),
)
],
),
),
);
}
else {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.logDetails),
actions: [
IconButton(
onPressed: () => blockUnblock(log, getFilteredStatus(context, appConfigProvider, log.reason, true)['filtered'] == true ? 'unblock' : 'block'),
icon: Icon(
getFilteredStatus(context, appConfigProvider, log.reason, true)['filtered'] == true
? Icons.check_circle_rounded
: Icons.block
),
tooltip: getFilteredStatus(context, appConfigProvider, log.reason, true)['filtered'] == true
? AppLocalizations.of(context)!.unblockDomain
: AppLocalizations.of(context)!.blockDomain,
),
const SizedBox(width: 10)
],
),
body: content()
);
}
}
}

View file

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

View file

@ -1,5 +1,7 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -7,6 +9,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/logs/logs_filters_modal.dart';
import 'package:adguard_home_manager/screens/logs/logs_config_modal.dart';
import 'package:adguard_home_manager/screens/logs/log_tile.dart';
import 'package:adguard_home_manager/screens/logs/log_details_screen.dart';
import 'package:adguard_home_manager/functions/snackbar.dart';
import 'package:adguard_home_manager/classes/process_modal.dart';
@ -64,6 +67,8 @@ class _LogsWidgetState extends State<LogsWidget> {
bool showDivider = true;
Log? selectedLog;
Future fetchLogs({
int? inOffset,
bool? loadingMore,
@ -188,6 +193,8 @@ class _LogsWidgetState extends State<LogsWidget> {
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final logsProvider = Provider.of<LogsProvider>(context);
final width = MediaQuery.of(context).size.width;
void updateConfig(Map<String, dynamic> data) async {
ProcessModal processModal = ProcessModal(context: context);
processModal.open(AppLocalizations.of(context)!.updatingSettings);
@ -252,13 +259,26 @@ class _LogsWidgetState extends State<LogsWidget> {
void openFilersModal() {
if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) {
showDialog(
context: context,
builder: (context) => const LogsFiltersModal(
dialog: true,
),
barrierDismissible: false
);
}
else {
showModalBottomSheet(
context: context,
builder: (context) => const LogsFiltersModal(),
builder: (context) => const LogsFiltersModal(
dialog: false,
),
backgroundColor: Colors.transparent,
isScrollControlled: true
);
}
}
final Map<String, String> translatedString = {
"all": AppLocalizations.of(context)!.all,
@ -319,6 +339,18 @@ class _LogsWidgetState extends State<LogsWidget> {
log: logsProvider.logsData!.data[index],
index: index,
length: logsProvider.logsData!.data.length,
isLogSelected: selectedLog != null && selectedLog == logsProvider.logsData!.data[index],
onLogTap: (log) {
if (width <= 1100) {
Navigator.push(context, MaterialPageRoute(
builder: (context) => LogDetailsScreen(
log: log,
dialog: false,
)
));
}
setState(() => selectedLog = log);
}
);
}
}
@ -383,28 +415,50 @@ class _LogsWidgetState extends State<LogsWidget> {
}
}
Widget logsScreen() {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.logs),
centerTitle: false,
actions: [
if (!(Platform.isAndroid || Platform.isIOS)) IconButton(
onPressed: () => fetchLogs(inOffset: 0),
icon: const Icon(Icons.refresh_rounded),
tooltip: AppLocalizations.of(context)!.refresh,
),
logsProvider.loadStatus == 1
? IconButton(
onPressed: openFilersModal,
icon: const Icon(Icons.filter_list_rounded)
icon: const Icon(Icons.filter_list_rounded),
tooltip: AppLocalizations.of(context)!.filters,
)
: const SizedBox(),
IconButton(
tooltip: AppLocalizations.of(context)!.settings,
onPressed: () => {
if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) {
showDialog(
context: context,
builder: (context) => LogsConfigModal(
onConfirm: updateConfig,
onClear: clearQueries,
dialog: true,
),
barrierDismissible: false
)
}
else {
showModalBottomSheet(
context: context,
builder: (context) => LogsConfigModal(
onConfirm: updateConfig,
onClear: clearQueries,
dialog: false,
),
backgroundColor: Colors.transparent,
isScrollControlled: true
)
}
},
icon: const Icon(Icons.settings)
),
@ -540,4 +594,31 @@ class _LogsWidgetState extends State<LogsWidget> {
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()
)
],
),
);
}
else {
return logsScreen();
}
}
}

View file

@ -12,11 +12,13 @@ import 'package:adguard_home_manager/providers/servers_provider.dart';
class LogsConfigModal extends StatelessWidget {
final void Function(Map<String, dynamic>) onConfirm;
final void Function() onClear;
final bool dialog;
const LogsConfigModal({
Key? key,
required this.onConfirm,
required this.onClear,
required this.dialog
}) : super(key: key);
@override
@ -30,6 +32,7 @@ class LogsConfigModal extends StatelessWidget {
context: context,
onConfirm: onConfirm,
onClear: onClear,
dialog: dialog,
);
}
}
@ -40,6 +43,7 @@ class LogsConfigModalWidget extends StatefulWidget {
final BuildContext context;
final void Function(Map<String, dynamic>) onConfirm;
final void Function() onClear;
final bool dialog;
const LogsConfigModalWidget({
Key? key,
@ -48,6 +52,7 @@ class LogsConfigModalWidget extends StatefulWidget {
required this.context,
required this.onConfirm,
required this.onClear,
required this.dialog
}) : super(key: key);
@override
@ -146,18 +151,42 @@ class _LogsConfigModalWidgetState extends State<LogsConfigModalWidget> {
Widget generateBody() {
switch (loadStatus) {
case 0:
return const Center(
child: CircularProgressIndicator(),
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 30),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text(
AppLocalizations.of(context)!.loadingLogsSettings,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 22,
color: Theme.of(context).colorScheme.onSurfaceVariant
),
),
)
],
),
);
case 1:
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: ListView(
physics: (Platform.isIOS ? 436 : 420) < MediaQuery.of(context).size.height
? const NeverScrollableScrollPhysics()
: null,
Flexible(
child: SingleChildScrollView(
child: Wrap(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 24),
@ -177,6 +206,10 @@ class _LogsConfigModalWidgetState extends State<LogsConfigModalWidget> {
),
),
const SizedBox(height: 16),
],
),
],
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Material(
@ -209,7 +242,7 @@ class _LogsConfigModalWidgetState extends State<LogsConfigModalWidget> {
),
),
),
const SizedBox(height: 16),
Container(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 14),
child: Column(
@ -238,7 +271,7 @@ class _LogsConfigModalWidgetState extends State<LogsConfigModalWidget> {
),
),
),
const SizedBox(height: 16),
Container(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: DropdownButtonFormField(
@ -267,6 +300,7 @@ class _LogsConfigModalWidgetState extends State<LogsConfigModalWidget> {
],
),
),
),
Padding(
padding: const EdgeInsets.all(24),
child: Row(
@ -316,11 +350,10 @@ class _LogsConfigModalWidgetState extends State<LogsConfigModalWidget> {
);
case 2:
return SizedBox(
width: double.maxFinite,
child: Column(
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.error,
@ -333,14 +366,13 @@ class _LogsConfigModalWidgetState extends State<LogsConfigModalWidget> {
child: Text(
AppLocalizations.of(context)!.logSettingsNotLoaded,
textAlign: TextAlign.center,
style: const TextStyle(
style: TextStyle(
fontSize: 22,
color: Colors.grey,
color: Theme.of(context).colorScheme.onSurfaceVariant
),
),
)
],
),
);
default:
@ -348,6 +380,17 @@ class _LogsConfigModalWidgetState extends State<LogsConfigModalWidget> {
}
}
if (widget.dialog == true) {
return Dialog(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 500
),
child: generateBody()
),
);
}
else {
return Container(
height: Platform.isIOS ? 436 : 420,
decoration: BoxDecoration(
@ -360,4 +403,5 @@ class _LogsConfigModalWidgetState extends State<LogsConfigModalWidget> {
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';
class LogsFiltersModal extends StatelessWidget {
const LogsFiltersModal({Key? key}) : super(key: key);
final bool dialog;
const LogsFiltersModal({
Key? key,
required this.dialog
}) : super(key: key);
@override
Widget build(BuildContext context) {
final logsProvider = Provider.of<LogsProvider>(context);
return LogsFiltersModalWidget(
logsProvider: logsProvider
logsProvider: logsProvider,
dialog: dialog,
);
}
}
class LogsFiltersModalWidget extends StatefulWidget {
final LogsProvider logsProvider;
final bool dialog;
const LogsFiltersModalWidget({
Key? key,
required this.logsProvider
required this.logsProvider,
required this.dialog
}) : super(key: key);
@override
@ -57,6 +65,8 @@ class _LogsFiltersModalWidgetState extends State<LogsFiltersModalWidget> {
final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
final Map<String, String> translatedString = {
"all": AppLocalizations.of(context)!.all,
"filtered": AppLocalizations.of(context)!.filtered,
@ -101,26 +111,52 @@ class _LogsFiltersModalWidgetState extends State<LogsFiltersModalWidget> {
}
void openSelectFilterStatus() {
if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) {
showDialog(
barrierDismissible: false,
context: context,
builder: (context) => FilterStatusModal(
value: logsProvider.selectedResultStatus,
dialog: true,
),
);
}
else {
showModalBottomSheet(
context: context,
builder: (context) => FilterStatusModal(
value: logsProvider.selectedResultStatus,
dialog: false,
),
isScrollControlled: true,
backgroundColor: Colors.transparent
);
}
}
void openSelectClients() {
if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) {
showDialog(
context: context,
builder: (context) => ClientsModal(
value: logsProvider.selectedClients,
dialog: true,
),
barrierDismissible: false
);
}
else {
showModalBottomSheet(
context: context,
builder: (context) => ClientsModal(
value: logsProvider.selectedClients,
dialog: false,
),
isScrollControlled: true,
backgroundColor: Colors.transparent
);
}
}
void filterLogs() async {
Navigator.pop(context);
@ -161,24 +197,18 @@ class _LogsFiltersModalWidgetState extends State<LogsFiltersModalWidget> {
}
}
return Padding(
padding: MediaQuery.of(context).viewInsets,
child: Container(
height: Platform.isIOS ? 446 : 430,
decoration: BoxDecoration(
color: Theme.of(context).dialogBackgroundColor,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28),
topRight: Radius.circular(28)
)
),
child: Column(
Widget content() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: ListView(
physics: (Platform.isIOS ? 416 : 400) < MediaQuery.of(context).size.height
? const NeverScrollableScrollPhysics()
: null,
Flexible(
child: SingleChildScrollView(
child: Wrap(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
children: [
Padding(
padding: const EdgeInsets.only(
@ -202,6 +232,10 @@ class _LogsFiltersModalWidgetState extends State<LogsFiltersModalWidget> {
),
),
const SizedBox(height: 16),
],
),
],
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Row(
@ -233,7 +267,7 @@ class _LogsFiltersModalWidgetState extends State<LogsFiltersModalWidget> {
],
),
),
const SizedBox(height: 16),
Container(height: 16),
CustomListTile(
title: AppLocalizations.of(context)!.client,
subtitle: logsProvider.selectedClients != null
@ -270,6 +304,7 @@ class _LogsFiltersModalWidgetState extends State<LogsFiltersModalWidget> {
],
),
),
),
Padding(
padding: const EdgeInsets.all(24),
child: Row(
@ -288,8 +323,36 @@ class _LogsFiltersModalWidgetState extends State<LogsFiltersModalWidget> {
),
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';
class Servers extends StatefulWidget {
const Servers({Key? key}) : super(key: key);
final double? breakingWidth;
const Servers({
Key? key,
this.breakingWidth
}) : super(key: key);
@override
State<Servers> createState() => _ServersState();
@ -55,16 +60,31 @@ class _ServersState extends State<Servers> {
final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
for (var i = 0; i < serversProvider.serversList.length; i++) {
expandableControllerList.add(ExpandableController());
}
void openAddServerModal() async {
await Future.delayed(const Duration(seconds: 0), (() => {
if (width > 700) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const AddServerModal(
window: true,
),
)
}
else {
Navigator.push(context, MaterialPageRoute(
fullscreenDialog: true,
builder: (BuildContext context) => const AddServerModal()
builder: (BuildContext context) => const AddServerModal(
window: false,
)
))
}
}));
}
@ -79,7 +99,8 @@ class _ServersState extends State<Servers> {
context: context,
controllers: expandableControllerList,
onChange: expandOrContract,
scrollController: scrollController
scrollController: scrollController,
breakingWidth: widget.breakingWidth ?? 700,
),
AnimatedPositioned(
duration: const Duration(milliseconds: 100),

View file

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -72,48 +74,9 @@ class _AccessSettingsWidgetState extends State<AccessSettingsWidget> with Ticker
@override
Widget build(BuildContext context) {
final serversProvider = Provider.of<ServersProvider>(context);
return Scaffold(
body: DefaultTabController(
length: 3,
child: NestedScrollView(
controller: scrollController,
headerSliverBuilder: ((context, innerBoxIsScrolled) {
return [
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverSafeArea(
top: false,
sliver: SliverAppBar(
title: Text(AppLocalizations.of(context)!.accessSettings),
pinned: true,
floating: true,
centerTitle: false,
forceElevated: innerBoxIsScrolled,
bottom: TabBar(
controller: tabController,
isScrollable: true,
unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant,
tabs: [
Tab(
icon: const Icon(Icons.check),
text: AppLocalizations.of(context)!.allowedClients,
),
Tab(
icon: const Icon(Icons.block),
text: AppLocalizations.of(context)!.disallowedClients,
),
Tab(
icon: const Icon(Icons.link_rounded),
text: AppLocalizations.of(context)!.disallowedDomains,
),
]
)
),
),
)
];
}),
body: TabBarView(
Widget body() {
return TabBarView(
controller: tabController,
children: [
ClientsList(
@ -141,9 +104,84 @@ class _AccessSettingsWidgetState extends State<AccessSettingsWidget> with Ticker
fetchClients: fetchClients
),
]
);
}
PreferredSizeWidget tabBar() {
return TabBar(
controller: tabController,
isScrollable: true,
unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant,
tabs: [
Tab(
child: Row(
children: [
const Icon(Icons.check),
const SizedBox(width: 8),
Text(AppLocalizations.of(context)!.allowedClients)
],
),
),
Tab(
child: Row(
children: [
const Icon(Icons.block),
const SizedBox(width: 8),
Text(AppLocalizations.of(context)!.disallowedClients)
],
),
),
Tab(
child: Row(
children: [
const Icon(Icons.link_rounded),
const SizedBox(width: 8),
Text(AppLocalizations.of(context)!.disallowedDomains)
],
),
),
]
);
}
if (Platform.isAndroid || Platform.isIOS) {
return Scaffold(
body: DefaultTabController(
length: 3,
child: NestedScrollView(
controller: scrollController,
headerSliverBuilder: ((context, innerBoxIsScrolled) {
return [
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverSafeArea(
top: false,
sliver: SliverAppBar(
title: Text(AppLocalizations.of(context)!.accessSettings),
pinned: true,
floating: true,
centerTitle: false,
forceElevated: innerBoxIsScrolled,
bottom: tabBar()
),
),
)
];
}),
body: body()
)
),
);
}
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 {
final String type;
final void Function(String, String) onConfirm;
final bool dialog;
const AddClientModal({
Key? key,
required this.type,
required this.onConfirm
required this.onConfirm,
required this.dialog,
}) : super(key: key);
@override
@ -65,32 +67,31 @@ class _AddClientModalState extends State<AddClientModal> {
}
}
Widget content() {
return Padding(
padding: MediaQuery.of(context).viewInsets,
child: Container(
height: Platform.isIOS ? 321 : 305,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Theme.of(context).dialogBackgroundColor,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28),
topRight: Radius.circular(28)
)
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: ListView(
physics: (Platform.isIOS ? 338 : 322) < MediaQuery.of(context).size.height
? const NeverScrollableScrollPhysics()
: null,
Flexible(
child: SingleChildScrollView(
child: Wrap(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon(),
size: 24,
color: Theme.of(context).listTileTheme.iconColor
),
const SizedBox(height: 16),
],
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
title(),
textAlign: TextAlign.center,
@ -99,7 +100,9 @@ class _AddClientModalState extends State<AddClientModal> {
color: Theme.of(context).colorScheme.onSurface
),
),
const SizedBox(height: 16),
],
),
),
TextFormField(
controller: fieldController,
onChanged: (_) => checkValidValues(),
@ -120,6 +123,7 @@ class _AddClientModalState extends State<AddClientModal> {
],
),
),
),
Padding(
padding: const EdgeInsets.only(top: 24),
child: Row(
@ -129,7 +133,7 @@ class _AddClientModalState extends State<AddClientModal> {
onPressed: () => Navigator.pop(context),
child: Text(AppLocalizations.of(context)!.cancel)
),
const SizedBox(width: 20),
const SizedBox(width: 16),
TextButton(
onPressed: validData == true
? () {
@ -149,10 +153,38 @@ class _AddClientModalState extends State<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
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:provider/provider.dart';
@ -68,6 +70,8 @@ class _ClientsListState extends State<ClientsList> {
final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
void confirmRemoveItem(String client, String type) async {
Map<String, List<String>> body = {
"allowed_clients": serversProvider.clients.data!.clientsAllowedBlocked?.allowedClients ?? [],
@ -209,6 +213,7 @@ class _ClientsListState extends State<ClientsList> {
}
return CustomTabContentList(
noSliver: !(Platform.isAndroid || Platform.isIOS) ? true : false,
loadingGenerator: () => SizedBox(
width: double.maxFinite,
height: MediaQuery.of(context).size.height-171,
@ -362,15 +367,28 @@ class _ClientsListState extends State<ClientsList> {
refreshIndicatorOffset: 0,
fab: FloatingActionButton(
onPressed: () {
if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) {
showDialog(
context: context,
builder: (context) => AddClientModal(
type: widget.type,
onConfirm: confirmAddItem,
dialog: true,
),
);
}
else {
showModalBottomSheet(
context: context,
builder: (context) => AddClientModal(
type: widget.type,
onConfirm: confirmAddItem
onConfirm: confirmAddItem,
dialog: false,
),
backgroundColor: Colors.transparent,
isScrollControlled: true
);
}
},
child: const Icon(Icons.add),
),

View file

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

View file

@ -5,10 +5,12 @@ import 'package:adguard_home_manager/models/dhcp.dart';
class AddStaticLeaseModal extends StatefulWidget {
final void Function(Lease) onConfirm;
final bool dialog;
const AddStaticLeaseModal({
Key? key,
required this.onConfirm,
required this.dialog
}) : super(key: key);
@override
@ -65,24 +67,20 @@ class _AddStaticLeaseModalState extends State<AddStaticLeaseModal> {
@override
Widget build(BuildContext context) {
return Padding(
padding: MediaQuery.of(context).viewInsets,
child: Container(
height: 510,
decoration: BoxDecoration(
color: Theme.of(context).dialogBackgroundColor,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28),
topRight: Radius.circular(28)
)
),
child: Column(
Widget content() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: ListView(
physics: 550 < MediaQuery.of(context).size.height
? const NeverScrollableScrollPhysics()
: null,
Flexible(
child: SingleChildScrollView(
child: Wrap(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 24),
@ -101,9 +99,15 @@ class _AddStaticLeaseModalState extends State<AddStaticLeaseModal> {
color: Theme.of(context).colorScheme.onSurface
),
),
const SizedBox(height: 16),
],
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 28),
padding: const EdgeInsets.only(
left: 24, right: 24, bottom: 12
),
child: TextFormField(
controller: macController,
onChanged: validateMac,
@ -119,9 +123,8 @@ class _AddStaticLeaseModalState extends State<AddStaticLeaseModal> {
),
),
),
const SizedBox(height: 30),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 28),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
child: TextFormField(
controller: ipController,
onChanged: validateIp,
@ -137,9 +140,10 @@ class _AddStaticLeaseModalState extends State<AddStaticLeaseModal> {
),
),
),
const SizedBox(height: 30),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 28),
padding: const EdgeInsets.only(
left: 24, right: 24, top: 12
),
child: TextFormField(
controller: hostNameController,
onChanged: (value) {
@ -166,8 +170,9 @@ class _AddStaticLeaseModalState extends State<AddStaticLeaseModal> {
],
),
),
),
Padding(
padding: const EdgeInsets.all(20),
padding: const EdgeInsets.all(24),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
@ -202,8 +207,33 @@ class _AddStaticLeaseModalState extends State<AddStaticLeaseModal> {
),
)
],
);
}
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
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_split_view/flutter_split_view.dart';
import 'package:provider/provider.dart';
import 'package:bottom_sheet/bottom_sheet.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/widgets/section_label.dart';
@ -204,7 +206,7 @@ class _DhcpWidgetState extends State<DhcpWidget> {
@override
void initState() {
loadDhcpStatus();
if (mounted) loadDhcpStatus();
super.initState();
}
@ -213,6 +215,8 @@ class _DhcpWidgetState extends State<DhcpWidget> {
final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
void saveSettings() async {
ProcessModal processModal = ProcessModal(context: context);
processModal.open(AppLocalizations.of(context)!.savingSettings);
@ -354,24 +358,33 @@ class _DhcpWidgetState extends State<DhcpWidget> {
void selectInterface() {
ScaffoldMessenger.of(context).clearSnackBars();
Future.delayed(const Duration(seconds: 0), () {
showFlexibleBottomSheet(
minHeight: 0.6,
initHeight: 0.6,
maxHeight: 0.95,
isCollapsible: true,
duration: const Duration(milliseconds: 250),
anchors: [0.95],
if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) {
showDialog(
context: context,
builder: (ctx, controller, offset) => SelectInterfaceModal(
builder: (context) => SelectInterfaceModal(
interfaces: serversProvider.dhcp.data!.networkInterfaces,
scrollController: controller,
onSelect: (interface) => setState(() {
clearAll();
selectedInterface = interface;
})
),
bottomSheetColor: Colors.transparent
}),
dialog: true,
)
);
}
else {
showModalBottomSheet(
context: context,
builder: (context) => SelectInterfaceModal(
interfaces: serversProvider.dhcp.data!.networkInterfaces,
onSelect: (i) => setState(() {
clearAll();
selectedInterface = i;
}),
dialog: false,
),
isScrollControlled: true
);
}
});
}
@ -399,7 +412,8 @@ class _DhcpWidgetState extends State<DhcpWidget> {
case 1:
if (selectedInterface != null) {
return ListView(
return SingleChildScrollView(
child: Wrap(
children: [
Padding(
padding: const EdgeInsets.only(
@ -459,10 +473,16 @@ class _DhcpWidgetState extends State<DhcpWidget> {
if (selectedInterface!.ipv4Addresses.isNotEmpty) ...[
SectionLabel(
label: AppLocalizations.of(context)!.ipv4settings,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
padding: const EdgeInsets.only(
top: 24, left: 16, right: 16, bottom: 8
)
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
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),
@ -479,9 +499,13 @@ class _DhcpWidgetState extends State<DhcpWidget> {
keyboardType: TextInputType.number,
),
),
const SizedBox(height: 30),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
),
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),
@ -498,9 +522,13 @@ class _DhcpWidgetState extends State<DhcpWidget> {
keyboardType: TextInputType.number,
),
),
const SizedBox(height: 30),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
),
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),
@ -517,9 +545,13 @@ class _DhcpWidgetState extends State<DhcpWidget> {
keyboardType: TextInputType.number,
),
),
const SizedBox(height: 30),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
),
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),
@ -536,9 +568,11 @@ class _DhcpWidgetState extends State<DhcpWidget> {
keyboardType: TextInputType.number,
),
),
const SizedBox(height: 30),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
),
FractionallySizedBox(
widthFactor: 1,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: TextFormField(
controller: ipv4LeaseTimeController,
onChanged: (value) {
@ -562,13 +596,19 @@ class _DhcpWidgetState extends State<DhcpWidget> {
keyboardType: TextInputType.number,
),
),
),
],
if (selectedInterface!.ipv6Addresses.isNotEmpty) ...[
SectionLabel(
label: AppLocalizations.of(context)!.ipv6settings,
padding: const EdgeInsets.all(16)
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 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),
@ -585,9 +625,13 @@ class _DhcpWidgetState extends State<DhcpWidget> {
keyboardType: TextInputType.number,
),
),
const SizedBox(height: 30),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
),
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),
@ -604,9 +648,11 @@ class _DhcpWidgetState extends State<DhcpWidget> {
keyboardType: TextInputType.number,
),
),
const SizedBox(height: 30),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
),
FractionallySizedBox(
widthFactor: 1,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: TextFormField(
controller: ipv6LeaseTimeController,
onChanged: (value) {
@ -630,12 +676,14 @@ class _DhcpWidgetState extends State<DhcpWidget> {
keyboardType: TextInputType.number,
),
),
),
],
const SizedBox(height: 20),
SectionLabel(
label: AppLocalizations.of(context)!.dhcpLeases,
padding: const EdgeInsets.all(16),
),
Material(
if (width <= 900) Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
@ -668,7 +716,7 @@ class _DhcpWidgetState extends State<DhcpWidget> {
),
),
),
Material(
if (width <= 900) Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
@ -701,12 +749,76 @@ class _DhcpWidgetState extends State<DhcpWidget> {
),
),
),
const SizedBox(height: 10)
],
if (width > 900) Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: () {
if (!(Platform.isAndroid || Platform.isIOS)) {
SplitView.of(context).push(
DhcpLeases(
items: serversProvider.dhcp.data!.dhcpStatus.leases,
staticLeases: false,
)
);
}
else {
return Column(
Navigator.push(context, MaterialPageRoute(
builder: (context) => DhcpLeases(
items: serversProvider.dhcp.data!.dhcpStatus.leases,
staticLeases: false,
)
));
}
},
child: Row(
children: [
Text(AppLocalizations.of(context)!.dhcpLeases),
const SizedBox(width: 8),
const Icon(Icons.arrow_forward_rounded)
],
)
),
ElevatedButton(
onPressed: () {
if (!(Platform.isAndroid || Platform.isIOS)) {
SplitView.of(context).push(
DhcpLeases(
items: serversProvider.dhcp.data!.dhcpStatus.staticLeases,
staticLeases: true,
)
);
}
else {
Navigator.push(context, MaterialPageRoute(
builder: (context) => DhcpLeases(
items: serversProvider.dhcp.data!.dhcpStatus.staticLeases,
staticLeases: true,
)
));
}
},
child: Row(
children: [
Text(AppLocalizations.of(context)!.dhcpStatic),
const SizedBox(width: 8),
const Icon(Icons.arrow_forward_rounded)
],
)
),
],
),
const SizedBox(height: 10)
],
),
);
}
else {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
@ -728,6 +840,9 @@ class _DhcpWidgetState extends State<DhcpWidget> {
child: Text(AppLocalizations.of(context)!.selectInterface)
),
],
),
),
],
);
}

View file

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

View file

@ -7,31 +7,30 @@ import 'package:adguard_home_manager/models/dhcp.dart';
class SelectInterfaceModal extends StatelessWidget {
final List<NetworkInterface> interfaces;
final ScrollController scrollController;
final void Function(NetworkInterface) onSelect;
final bool dialog;
const SelectInterfaceModal({
Key? key,
required this.interfaces,
required this.scrollController,
required this.onSelect,
required this.dialog
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).dialogBackgroundColor,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28),
topRight: Radius.circular(28)
)
),
child: Column(
Widget content() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: ListView(
controller: scrollController,
Flexible(
child: SingleChildScrollView(
child: Wrap(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 24),
@ -50,6 +49,10 @@ class SelectInterfaceModal extends StatelessWidget {
color: Theme.of(context).colorScheme.onSurface
),
),
],
),
],
),
const SizedBox(height: 16),
ListView.builder(
primary: false,
@ -176,6 +179,7 @@ class SelectInterfaceModal extends StatelessWidget {
],
),
),
),
Padding(
padding: const EdgeInsets.all(20),
child: Row(
@ -190,7 +194,30 @@ class SelectInterfaceModal extends StatelessWidget {
),
if (Platform.isIOS) const SizedBox(height: 16)
],
);
}
if (dialog == true) {
return Dialog(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 500
),
child: content()
),
);
}
else {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).dialogBackgroundColor,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28),
topRight: Radius.circular(28)
)
),
child: content()
);
}
}
}

View file

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

View file

@ -4,11 +4,13 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class CommentModal extends StatefulWidget {
final String? comment;
final void Function(String) onConfirm;
final bool dialog;
const CommentModal({
Key? key,
this.comment,
required this.onConfirm
required this.onConfirm,
required this.dialog
}) : super(key: key);
@override
@ -30,24 +32,18 @@ class _CommentModalState extends State<CommentModal> {
@override
Widget build(BuildContext context) {
return Padding(
padding: MediaQuery.of(context).viewInsets,
child: Container(
height: 310,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28),
topRight: Radius.circular(28)
),
color: Theme.of(context).dialogBackgroundColor
),
child: Column(
Widget content() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: ListView(
physics: MediaQuery.of(context).size.height >= 330 == true
? const NeverScrollableScrollPhysics()
: null,
Flexible(
child: SingleChildScrollView(
child: Wrap(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 24),
@ -67,6 +63,10 @@ class _CommentModalState extends State<CommentModal> {
),
),
const SizedBox(height: 16),
],
),
],
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: TextFormField(
@ -95,6 +95,7 @@ class _CommentModalState extends State<CommentModal> {
],
),
),
),
Padding(
padding: const EdgeInsets.all(24),
child: Row(
@ -125,8 +126,33 @@ class _CommentModalState extends State<CommentModal> {
),
)
],
);
}
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
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_split_view/flutter_split_view.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -76,6 +79,19 @@ class _DnsSettingsWidgetState extends State<DnsSettingsWidget> {
final serversProvider = Provider.of<ServersProvider>(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() {
switch (widget.serversProvider.dnsInfo.loadStatus) {
case 0:
@ -105,51 +121,51 @@ class _DnsSettingsWidgetState extends State<DnsSettingsWidget> {
CustomListTile(
title: AppLocalizations.of(context)!.upstreamDns,
subtitle: AppLocalizations.of(context)!.upstreamDnsDescription,
onTap: () => Navigator.push(context, MaterialPageRoute(
builder: (context) => UpstreamDnsScreen(
onTap: () => navigate(
UpstreamDnsScreen(
serversProvider: serversProvider
)
)),
),
icon: Icons.upload_rounded,
),
CustomListTile(
title: AppLocalizations.of(context)!.bootstrapDns,
subtitle: AppLocalizations.of(context)!.bootstrapDnsDescription,
onTap: () => Navigator.push(context, MaterialPageRoute(
builder: (context) => BootstrapDnsScreen(
onTap: () => navigate(
BootstrapDnsScreen(
serversProvider: serversProvider
)
)),
),
icon: Icons.dns_rounded,
),
CustomListTile(
title: AppLocalizations.of(context)!.privateReverseDnsServers,
subtitle: AppLocalizations.of(context)!.privateReverseDnsDescription,
onTap: () => Navigator.push(context, MaterialPageRoute(
builder: (context) => PrivateReverseDnsServersScreen(
onTap: () => navigate(
PrivateReverseDnsServersScreen(
serversProvider: serversProvider
)
)),
),
icon: Icons.person_rounded,
),
CustomListTile(
title: AppLocalizations.of(context)!.dnsServerSettings,
subtitle: AppLocalizations.of(context)!.dnsServerSettingsDescription,
onTap: () => Navigator.push(context, MaterialPageRoute(
builder: (context) => DnsServerSettingsScreen(
onTap: () => navigate(
DnsServerSettingsScreen(
serversProvider: serversProvider
)
)),
),
icon: Icons.settings,
),
CustomListTile(
title: AppLocalizations.of(context)!.dnsCacheConfig,
subtitle: AppLocalizations.of(context)!.dnsCacheConfigDescription,
onTap: () => Navigator.push(context, MaterialPageRoute(
builder: (context) => CacheConfigDnsScreen(
onTap: () => navigate(
CacheConfigDnsScreen(
serversProvider: serversProvider
)
)),
),
icon: Icons.storage_rounded,
),
],

View file

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

View file

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

View file

@ -7,10 +7,12 @@ import 'package:adguard_home_manager/models/rewrite_rules.dart';
class AddDnsRewriteModal extends StatefulWidget {
final void Function(RewriteRulesData) onConfirm;
final bool dialog;
const AddDnsRewriteModal({
Key? key,
required this.onConfirm
required this.onConfirm,
required this.dialog
}) : super(key: key);
@override
@ -50,24 +52,18 @@ class _AddDnsRewriteModalState extends State<AddDnsRewriteModal> {
@override
Widget build(BuildContext context) {
return Padding(
padding: MediaQuery.of(context).viewInsets,
child: Container(
height: Platform.isIOS ? 416 : 400,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28),
topRight: Radius.circular(28)
),
color: Theme.of(context).dialogBackgroundColor,
),
child: Column(
Widget content() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: ListView(
physics: (Platform.isIOS ? 426 : 410) < MediaQuery.of(context).size.height
? const NeverScrollableScrollPhysics()
: null,
Flexible(
child: SingleChildScrollView(
child: Wrap(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 24),
@ -87,8 +83,14 @@ class _AddDnsRewriteModalState extends State<AddDnsRewriteModal> {
),
),
const SizedBox(height: 16),
],
),
],
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
padding: const EdgeInsets.only(
left: 24, right: 24, bottom: 12
),
child: TextFormField(
controller: domainController,
onChanged: validateDomain,
@ -104,9 +106,10 @@ class _AddDnsRewriteModalState extends State<AddDnsRewriteModal> {
),
),
),
const SizedBox(height: 30),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
padding: const EdgeInsets.only(
left: 24, right: 24, top: 12
),
child: TextFormField(
controller: answerController,
onChanged: (_) => checkValidValues(),
@ -124,6 +127,7 @@ class _AddDnsRewriteModalState extends State<AddDnsRewriteModal> {
],
),
),
),
Padding(
padding: const EdgeInsets.all(24),
child: Row(
@ -160,8 +164,33 @@ class _AddDnsRewriteModalState extends State<AddDnsRewriteModal> {
),
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
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -70,6 +72,8 @@ class _DnsRewritesWidgetState extends State<DnsRewritesWidget> {
final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
void deleteDnsRewrite(RewriteRulesData rule) async {
ProcessModal processModal = ProcessModal(context: context);
processModal.open(AppLocalizations.of(context)!.deleting);
@ -288,14 +292,26 @@ class _DnsRewritesWidgetState extends State<DnsRewritesWidget> {
body: generateBody(),
floatingActionButton: FloatingActionButton(
onPressed: () => {
if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) {
showDialog(
context: context,
builder: (context) => AddDnsRewriteModal(
onConfirm: addDnsRewrite,
dialog: true,
),
)
}
else {
showModalBottomSheet(
context: context,
builder: (context) => AddDnsRewriteModal(
onConfirm: addDnsRewrite,
dialog: false,
),
backgroundColor: Colors.transparent,
isScrollControlled: true
)
}
},
child: const Icon(Icons.add),
),

View file

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

View file

@ -234,6 +234,8 @@ class _EncryptionSettingsWidgetState extends State<EncryptionSettingsWidget> {
final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
void saveData() async {
ProcessModal processModal = ProcessModal(context: context);
processModal.open(AppLocalizations.of(context)!.savingConfig);
@ -343,7 +345,11 @@ class _EncryptionSettingsWidgetState extends State<EncryptionSettingsWidget> {
disabled: !enabled,
),
const SizedBox(height: 10),
EncryptionTextField(
Wrap(
children: [
FractionallySizedBox(
widthFactor: width > 900 ? 0.33 : 1,
child: EncryptionTextField(
enabled: enabled,
controller: httpsPortController,
icon: Icons.numbers_rounded,
@ -355,8 +361,14 @@ class _EncryptionSettingsWidgetState extends State<EncryptionSettingsWidget> {
label: AppLocalizations.of(context)!.httpsPort,
keyboardType: TextInputType.number,
),
const SizedBox(height: 30),
EncryptionTextField(
),
Padding(
padding: width <= 900
? const EdgeInsets.symmetric(vertical: 24)
: const EdgeInsets.all(0),
child: FractionallySizedBox(
widthFactor: width > 900 ? 0.33 : 1,
child: EncryptionTextField(
enabled: enabled,
controller: tlsPortController,
icon: Icons.numbers_rounded,
@ -368,8 +380,11 @@ class _EncryptionSettingsWidgetState extends State<EncryptionSettingsWidget> {
label: AppLocalizations.of(context)!.tlsPort,
keyboardType: TextInputType.number,
),
const SizedBox(height: 30),
EncryptionTextField(
),
),
FractionallySizedBox(
widthFactor: width > 900 ? 0.33 : 1,
child: EncryptionTextField(
enabled: enabled,
controller: dnsOverQuicPortController,
icon: Icons.numbers_rounded,
@ -381,6 +396,9 @@ class _EncryptionSettingsWidgetState extends State<EncryptionSettingsWidget> {
label: AppLocalizations.of(context)!.dnsOverQuicPort,
keyboardType: TextInputType.number,
),
),
],
),
SectionLabel(
label: AppLocalizations.of(context)!.certificates,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),

View file

@ -52,6 +52,7 @@ class _SafeSearchSettingsScreenWidgetState extends State<SafeSearchSettingsScree
bool youtubeEnabled = false;
Future requestSafeSearchSettings() async {
if (mounted) {
final result = await getServerStatus(widget.serversProvider.selectedServer!);
if (mounted) {
if (result['result'] == 'success') {
@ -73,6 +74,7 @@ class _SafeSearchSettingsScreenWidgetState extends State<SafeSearchSettingsScree
}
}
}
}
@override
void initState() {

View file

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

View file

@ -65,16 +65,18 @@ class UpdateScreen extends StatelessWidget {
Widget headerPortrait() {
return Column(
children: [
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
if (Navigator.canPop(context)) IconButton(
icon: Icon(
Icons.arrow_back,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
onPressed: () => Navigator.pop(context),
),
if (!Navigator.canPop(context)) const SizedBox(),
IconButton(
icon: Icon(
Icons.refresh_rounded,
@ -170,120 +172,6 @@ class UpdateScreen extends StatelessWidget {
);
}
Widget headerLandscape() {
return Column(
mainAxisSize: MainAxisSize.max,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: Icon(
Icons.arrow_back,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
onPressed: () => Navigator.pop(context),
),
IconButton(
icon: Icon(
Icons.refresh_rounded,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
tooltip: AppLocalizations.of(context)!.checkUpdates,
onPressed: () => serversProvider.checkServerUpdatesAvailable(serversProvider.selectedServer!)
),
],
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(
top: 8, bottom: 16, left: 16, right: 16
),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
serversProvider.updateAvailable.loadStatus == LoadStatus.loading
? Column(
children: const [
CircularProgressIndicator(),
SizedBox(height: 4)
],
)
: Icon(
serversProvider.updateAvailable.data!.updateAvailable != null
? serversProvider.updateAvailable.data!.updateAvailable == true
? Icons.system_update_rounded
: Icons.system_security_update_good_rounded
: Icons.system_security_update_warning_rounded,
size: 40,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 16),
Text(
serversProvider.updateAvailable.loadStatus == LoadStatus.loading
? AppLocalizations.of(context)!.checkingUpdates
: serversProvider.updateAvailable.data!.updateAvailable != null
? serversProvider.updateAvailable.data!.updateAvailable == true
? AppLocalizations.of(context)!.updateAvailable
: AppLocalizations.of(context)!.serverUpdated
: AppLocalizations.of(context)!.unknownStatus,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.w400
),
),
const SizedBox(height: 40),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (serversProvider.updateAvailable.loadStatus == LoadStatus.loaded) Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
serversProvider.updateAvailable.data!.updateAvailable != null && serversProvider.updateAvailable.data!.updateAvailable == true
? AppLocalizations.of(context)!.newVersion
: AppLocalizations.of(context)!.currentVersion,
style: const TextStyle(
fontSize: 16,
),
),
const SizedBox(height: 4),
Text(
serversProvider.updateAvailable.data!.updateAvailable != null
? serversProvider.updateAvailable.data!.updateAvailable == true
? serversProvider.updateAvailable.data!.newVersion ?? 'N/A'
: serversProvider.updateAvailable.data!.currentVersion
: "N/A",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: Theme.of(context).colorScheme.onSurfaceVariant
),
)
],
),
if (serversProvider.updateAvailable.loadStatus != LoadStatus.loaded) const SizedBox(),
FilledButton.icon(
icon: const Icon(Icons.download_rounded),
label: Text(AppLocalizations.of(context)!.updateNow),
onPressed: serversProvider.updateAvailable.data!.updateAvailable != null && serversProvider.updateAvailable.data!.updateAvailable == true
? serversProvider.updateAvailable.data!.canAutoupdate == true
? () => update()
: () => showAutoUpdateUnavailableModal()
: null
)
],
),
),
],
),
),
),
],
);
}
final changelog = serversProvider.updateAvailable.loadStatus == LoadStatus.loaded && serversProvider.updateAvailable.data!.changelog != null
? ListView(
children: [
@ -313,39 +201,7 @@ class UpdateScreen extends StatelessWidget {
: null;
return Scaffold(
body: MediaQuery.of(context).size.width > 700
? Row(
children: [
Expanded(
flex: 2,
child: Container(
color: Theme.of(context).colorScheme.surfaceVariant,
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Container(
height: MediaQuery.of(context).size.height,
padding: EdgeInsets.only(
top: MediaQuery.of(context).viewPadding.top
),
child: headerLandscape(),
)
],
),
),
),
Expanded(
flex: 3,
child: SafeArea(
child: SizedBox(
width: MediaQuery.of(context).size.width*0.6,
child: changelog ?? const SizedBox(),
),
),
)
],
)
: Column(
body: Column(
children: [
Container(
color: Theme.of(context).colorScheme.surfaceVariant,
@ -353,6 +209,7 @@ class UpdateScreen extends StatelessWidget {
child: headerPortrait()
)
),
const SizedBox(height: 16),
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 {
final Server? server;
final bool window;
const AddServerModal({
Key? key,
this.server,
required this.window
}) : super(key: key);
@override
@ -429,35 +431,8 @@ class _AddServerModalState extends State<AddServerModal> {
}
}
return Stack(
children: [
Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.createConnection),
actions: [
Padding(
padding: const EdgeInsets.only(right: 10),
child: 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
)
),
),
],
toolbarHeight: 70,
),
body: ListView(
children: [
List<Widget> form() {
return [
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
margin: const EdgeInsets.only(
@ -612,8 +587,92 @@ class _AddServerModalState extends State<AddServerModal> {
),
),
const SizedBox(height: 20),
];
}
if (widget.window == true) {
return Dialog(
child: SizedBox(
width: 400,
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(
AppLocalizations.of(context)!.createConnection,
style: const TextStyle(
fontSize: 20
),
),
],
),
IconButton(
tooltip: widget.server == null
? AppLocalizations.of(context)!.connect
: AppLocalizations.of(context)!.save,
onPressed: allDataValid == true
? widget.server == null
? () => connect()
: () => edit()
: null,
icon: Icon(
widget.server == null
? Icons.login_rounded
: Icons.save_rounded
)
),
],
),
),
Expanded(
child: ListView(
children: form()
),
)
],
),
),
);
}
else {
return Stack(
children: [
Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.createConnection),
actions: [
IconButton(
tooltip: widget.server == null
? AppLocalizations.of(context)!.connect
: AppLocalizations.of(context)!.save,
onPressed: allDataValid == true
? widget.server == null
? () => connect()
: () => edit()
: null,
icon: Icon(
widget.server == null
? Icons.login_rounded
: Icons.save_rounded
)
),
const SizedBox(width: 10)
],
toolbarHeight: 70,
),
body: ListView(
children: form(),
)
),
AnimatedOpacity(
opacity: isConnecting == true ? 1 : 0,
@ -651,4 +710,5 @@ class _AddServerModalState extends State<AddServerModal> {
],
);
}
}
}

View file

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

View file

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

View file

@ -41,7 +41,8 @@ class CustomRadioListTile extends StatelessWidget {
backgroundColor: radioBackgroundColor,
),
const SizedBox(width: 24),
Column(
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
@ -69,6 +70,7 @@ class CustomRadioListTile extends StatelessWidget {
]
],
),
),
],
),
),

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
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
const SizedBox(width: 40),

View file

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

View file

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

View file

@ -71,6 +71,8 @@ class _ServersListItemState extends State<ServersListItem> with SingleTickerProv
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(
@ -85,10 +87,25 @@ class _ServersListItemState extends State<ServersListItem> with SingleTickerProv
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)
builder: (BuildContext context) => AddServerModal(
server: server,
window: false,
)
))
}
}));
}
@ -356,7 +373,6 @@ class _ServersListItemState extends State<ServersListItem> with SingleTickerProv
);
}
return Container(
decoration: BoxDecoration(
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 Widget? fab;
final bool? fabVisible;
final bool? noSliver;
final EdgeInsets? listPadding;
const CustomTabContentList({
Key? key,
@ -27,7 +29,9 @@ class CustomTabContentList extends StatelessWidget {
required this.onRefresh,
this.refreshIndicatorOffset,
this.fab,
this.fabVisible
this.fabVisible,
this.noSliver,
this.listPadding
}) : super(key: key);
@override
@ -36,6 +40,13 @@ class CustomTabContentList extends StatelessWidget {
switch (loadStatus) {
case LoadStatus.loading:
if (noSliver == true) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: loadingGenerator()
);
}
else {
return SafeArea(
top: false,
bottom: false,
@ -55,9 +66,51 @@ class CustomTabContentList extends StatelessWidget {
),
)
);
}
case LoadStatus.loaded:
if (noSliver == true) {
if (itemsCount > 0) {
return Stack(
children: [
ListView.builder(
padding: listPadding,
itemCount: itemsCount,
itemBuilder: (context, index) => contentWidget(index),
),
if (fab != null) AnimatedPositioned(
duration: const Duration(milliseconds: 100),
curve: Curves.easeInOut,
bottom: fabVisible != null && fabVisible == true ?
appConfigProvider.showingSnackbar
? 70 : 20
: -70,
right: 20,
child: fab!
),
],
);
}
else {
return Stack(
children: [
noData,
if (fab != null) AnimatedPositioned(
duration: const Duration(milliseconds: 100),
curve: Curves.easeInOut,
bottom: fabVisible != null && fabVisible == true ?
appConfigProvider.showingSnackbar
? 70 : 20
: -70,
right: 20,
child: fab!
),
],
);
}
}
else {
return Stack(
children: [
SafeArea(
@ -67,7 +120,7 @@ class CustomTabContentList extends StatelessWidget {
builder: (BuildContext context) {
return RefreshIndicator(
onRefresh: onRefresh,
edgeOffset: refreshIndicatorOffset ?? 95,
edgeOffset: refreshIndicatorOffset ?? 70,
child: CustomScrollView(
slivers: <Widget>[
SliverOverlapInjector(
@ -100,8 +153,20 @@ class CustomTabContentList extends StatelessWidget {
),
],
);
}
case LoadStatus.error:
if (noSliver == true) {
return Padding(
padding: const EdgeInsets.only(
top: 95,
left: 16,
right: 16
),
child: errorGenerator()
);
}
else {
return SafeArea(
top: false,
bottom: false,
@ -125,6 +190,7 @@ class CustomTabContentList extends StatelessWidget {
),
)
);
}
default:
return const SizedBox();

View file

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

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