Merge branch 'beta'

This commit is contained in:
Juan Gilsanz Polo 2024-01-29 16:14:53 +01:00
commit 1a7925f49d
93 changed files with 3653 additions and 2094 deletions

View file

@ -112,7 +112,6 @@ On [this repository](https://github.com/JuanRodenas/Pihole_list) you can find a
- [dynamic color](https://pub.dev/packages/dynamic_color)
- [device info](https://pub.dev/packages/device_info)
- [fl chart](https://pub.dev/packages/fl_chart)
- [flutter web browser](https://pub.dev/packages/flutter_web_browser)
- [flutter svg](https://pub.dev/packages/flutter_svg)
- [percent indicator](https://pub.dev/packages/percent_indicator)
- [store checker](https://pub.dev/packages/store_checker)
@ -130,6 +129,9 @@ On [this repository](https://github.com/JuanRodenas/Pihole_list) you can find a
- [flutter reorderable list](https://pub.dev/packages/flutter_reorderable_list)
- [pie chart](https://pub.dev/packages/pie_chart)
- [segmented button slide](https://pub.dev/packages/segmented_button_slide)
- [timezone](https://pub.dev/packages/timezone)
- [url launcher](https://pub.dev/packages/url_launcher)
- [flutter custom tabs](https://pub.dev/packages/flutter_custom_tabs)
<br>

View file

@ -1,3 +1,9 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) {
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
@ -21,10 +22,6 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
@ -80,5 +77,5 @@ flutter {
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.20"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 21 KiB

BIN
android/app/src/main/res/drawable-xxxhdpi/splash.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_monochrome" />
<monochrome android:drawable="@mipmap/ic_launcher_monochrome" />
</adaptive-icon>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 874 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View file

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#ffffff</color>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View file

@ -1,16 +1,3 @@
buildscript {
ext.kotlin_version = '1.8.20'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.2.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()

View file

@ -1,11 +1,26 @@
include ':app'
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}
settings.ext.flutterSdkPath = flutterSdkPath()
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.2.2" apply false
id "org.jetbrains.kotlin.android" version "1.8.20" apply false
}
include ":app"

View file

@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>11.0</string>
<string>12.0</string>
</dict>
</plist>

View file

@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '11.0'
# platform :ios, '12.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

67
ios/Podfile.lock Executable file → Normal file
View file

@ -2,60 +2,53 @@ PODS:
- device_info_plus (0.0.1):
- Flutter
- Flutter (1.0.0)
- flutter_custom_tabs_ios (2.0.0):
- Flutter
- flutter_native_splash (0.0.1):
- Flutter
- flutter_web_browser (0.17.1):
- Flutter
- FMDB (2.7.5):
- FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5)
- package_info_plus (0.4.5):
- Flutter
- Sentry/HybridSDK (8.15.2):
- SentryPrivate (= 8.15.2)
- Sentry/HybridSDK (8.18.0):
- SentryPrivate (= 8.18.0)
- sentry_flutter (0.0.1):
- Flutter
- FlutterMacOS
- Sentry/HybridSDK (= 8.15.2)
- SentryPrivate (8.15.2)
- Sentry/HybridSDK (= 8.18.0)
- SentryPrivate (8.18.0)
- sqflite (0.0.3):
- Flutter
- FMDB (>= 2.7.5)
- sqlite3 (3.44.0):
- sqlite3/common (= 3.44.0)
- sqlite3/common (3.44.0)
- sqlite3/fts5 (3.44.0):
- FlutterMacOS
- sqlite3 (3.45.0):
- sqlite3/common (= 3.45.0)
- sqlite3/common (3.45.0)
- sqlite3/fts5 (3.45.0):
- sqlite3/common
- sqlite3/perf-threadsafe (3.44.0):
- sqlite3/perf-threadsafe (3.45.0):
- sqlite3/common
- sqlite3/rtree (3.44.0):
- sqlite3/rtree (3.45.0):
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- Flutter
- sqlite3 (~> 3.44.0)
- sqlite3 (~> 3.45.0)
- sqlite3/fts5
- sqlite3/perf-threadsafe
- sqlite3/rtree
- store_checker (0.0.1):
- Flutter
- url_launcher_ios (0.0.1):
- Flutter
DEPENDENCIES:
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`)
- flutter_custom_tabs_ios (from `.symlinks/plugins/flutter_custom_tabs_ios/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_web_browser (from `.symlinks/plugins/flutter_web_browser/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`)
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`)
- store_checker (from `.symlinks/plugins/store_checker/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
SPEC REPOS:
trunk:
- FMDB
- Sentry
- SentryPrivate
- sqlite3
@ -65,39 +58,35 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/device_info_plus/ios"
Flutter:
:path: Flutter
flutter_custom_tabs_ios:
:path: ".symlinks/plugins/flutter_custom_tabs_ios/ios"
flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios"
flutter_web_browser:
:path: ".symlinks/plugins/flutter_web_browser/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
sentry_flutter:
:path: ".symlinks/plugins/sentry_flutter/ios"
sqflite:
:path: ".symlinks/plugins/sqflite/ios"
:path: ".symlinks/plugins/sqflite/darwin"
sqlite3_flutter_libs:
:path: ".symlinks/plugins/sqlite3_flutter_libs/ios"
store_checker:
:path: ".symlinks/plugins/store_checker/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
SPEC CHECKSUMS:
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_custom_tabs_ios: 62439c843b2691aae516fd50119a01eb9755fff7
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
flutter_web_browser: 7bccaafbb0c5b8862afe7bcd158f15557109f61f
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
Sentry: 6f5742b4c47c17c9adcf265f6f328cf4a0ed1923
sentry_flutter: 2c309a1d4b45e59d02cfa15795705687f1e2081b
SentryPrivate: b2f7996f37781080f04a946eb4e377ff63c64195
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
sqlite3: 6e2d4a4879854d0ec86b476bf3c3e30870bac273
sqlite3_flutter_libs: eb769059df0356dc52ddda040f09cacc9391a7cf
Sentry: 8984a4ffb2b9bd2894d74fb36e6f5833865bc18e
sentry_flutter: c87a0556eeb6cbf7f9f924d30e878bdedf22d364
SentryPrivate: 2f0c9ba4c3fc993f70eab6ca95673509561e0085
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
sqlite3: f307b6291c4db7b5086c38d6237446b98a738581
sqlite3_flutter_libs: aeb4d37509853dfa79d9b59386a2dac5dd079428
store_checker: 359c5051d9ec30ff0a8fa39eb5ec9df021bb745d
url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b
PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3
PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011
COCOAPODS: 1.14.3

View file

@ -1,30 +1,33 @@
import 'dart:io';
import 'package:flutter_web_browser/flutter_web_browser.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart' as flutter_custom_tabs;
import 'package:url_launcher/url_launcher.dart' as url_launcher;
import 'package:sentry_flutter/sentry_flutter.dart';
void openUrl(String url) async {
if (Platform.isAndroid || Platform.isIOS) {
FlutterWebBrowser.openWebPage(
url: url,
customTabsOptions: const CustomTabsOptions(
instantAppsEnabled: true,
showTitle: true,
urlBarHidingEnabled: false,
),
safariVCOptions: const SafariViewControllerOptions(
barCollapsingEnabled: true,
dismissButtonStyle: SafariViewControllerDismissButtonStyle.close,
modalPresentationCapturesStatusBarAppearance: true,
)
);
try {
await flutter_custom_tabs.launchUrl(
Uri.parse(url),
customTabsOptions: const flutter_custom_tabs.CustomTabsOptions(
shareState: flutter_custom_tabs.CustomTabsShareState.browserDefault,
urlBarHidingEnabled: true,
showTitle: true,
),
safariVCOptions: const flutter_custom_tabs.SafariViewControllerOptions(
barCollapsingEnabled: true,
dismissButtonStyle: flutter_custom_tabs.SafariViewControllerDismissButtonStyle.close,
),
);
} catch (e, stackTrace) {
Sentry.captureException(e, stackTrace: stackTrace);
}
}
else {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
} else {
throw 'Could not launch $url';
try {
url_launcher.launchUrl(Uri.parse(url));
} catch (e, stackTrace) {
Sentry.captureException(e, stackTrace: stackTrace);
}
}
}

View file

@ -9,10 +9,12 @@ void showSnacbkar({
required AppConfigProvider appConfigProvider,
required String label,
required Color color,
Color? labelColor
Color? labelColor,
GlobalKey<ScaffoldMessengerState>? key,
}) async {
final GlobalKey<ScaffoldMessengerState> scaffoldKey = key ?? scaffoldMessengerKey;
if (appConfigProvider.showingSnackbar == true) {
scaffoldMessengerKey.currentState?.clearSnackBars();
scaffoldKey.currentState?.clearSnackBars();
await Future.delayed(const Duration(milliseconds: 500));
}
appConfigProvider.setShowingSnackbar(true);
@ -26,7 +28,7 @@ void showSnacbkar({
),
backgroundColor: color,
);
scaffoldMessengerKey.currentState?.showSnackBar(snackBar).closed.then(
scaffoldKey.currentState?.showSnackBar(snackBar).closed.then(
(value) => appConfigProvider.setShowingSnackbar(false)
);
}

View file

@ -24,7 +24,7 @@
"invalidUsernamePassword": "Invalid username or password",
"tooManyAttempts": "Too many attempts. Try again later.",
"cantReachServer": "Can't reach server. Check connection data.",
"sslError": "SSL error. Go to Settings > Advanced settings and enable Override SSL validation.",
"sslError": "Handshake exception. Cannot establish a secure connection with the server. This can be a SSL error. Go to Settings > Advanced settings and enable Override SSL validation.",
"unknownError": "Unknown error",
"connectionNotCreated": "Connection couldn't be created",
"connecting": "Connecting...",
@ -719,12 +719,44 @@
"unblockClient": "Unblock client",
"blockingClient": "Blocking client...",
"unblockingClient": "Unblocking client...",
"upstreamDnsCacheConfiguration": "Configuración de la caché DNS upstream",
"upstreamDnsCacheConfiguration": "DNS upstream cache configuration",
"enableDnsCachingClient": "Enable DNS caching for this client",
"dnsCacheSize": "DNS cache size",
"nameInvalid": "Name is required",
"oneIdentifierRequired": "At least one identifier is required",
"dnsCacheNumber": "DNS cache size must be a number",
"errors": "Errors",
"redirectHttpsWarning": "If you have enabled \"Redirect to HTTPS automatically\" on your AdGuard Home server, you must select an HTTPS connection and use the HTTPS port of your server."
"redirectHttpsWarning": "If you have enabled \"Redirect to HTTPS automatically\" on your AdGuard Home server, you must select an HTTPS connection and use the HTTPS port of your server.",
"logsSettingsDescription": "Configure query logs",
"ignoredDomains": "Ignored domains",
"noIgnoredDomainsAdded": "No domains to ignore added",
"pauseServiceBlocking": "Pause service blocking",
"newSchedule": "New schedule",
"editSchedule": "Edit schedule",
"timezone": "Timezone",
"monday": "Monday",
"tuesday": "Tuesday",
"wednesday": "Wednesday",
"thursday": "Thursday",
"friday": "Friday",
"saturday": "Saturday",
"sunday": "Sunday",
"from": "From",
"to": "To",
"selectStartTime": "Select start time",
"selectEndTime": "Select end time",
"startTimeBeforeEndTime": "Start time must be before end time.",
"noBlockingScheduleThisDevice": "There's no blocking schedule for this device.",
"selectTimezone": "Select a timezone",
"selectClientsFiltersInfo": "Select the clients you want to display. If no clients are selected, all will be displayed.",
"noDataThisSection": "There's no data for this section.",
"statisticsSettings": "Statistics settings",
"statisticsSettingsDescription": "Configure data collection for statistics",
"loadingStatisticsSettings": "Loading statistics settings...",
"statisticsSettingsLoadError": "An error occured when loading statistics settings.",
"customTimeInHours": "Custom time (in hours)",
"invalidTime": "Invalid time",
"removeDomain": "Remove domain",
"addDomain": "Add domain",
"notLess1Hour": "Time cannot be less than 1 hour"
}

View file

@ -24,7 +24,7 @@
"invalidUsernamePassword": "Usuario o contraseña no válidos.",
"tooManyAttempts": "Demasiados intentos. Prueba de nuevo más tarde.",
"cantReachServer": "No se puede alcanzar el servidor. Comprueba los datos de conexión.",
"sslError": "Error de SSL. Ve a Ajustes > Ajustes avanzados y activa No comprobar SSL.",
"sslError": "Handshake exception. No se ha podido establecer una conexión segura con el servidor. Es posible que sea un error de SSL. Ve a Ajustes > Ajustes avanzados y activa No comprobar SSL.",
"unknownError": "Error desconocido",
"connectionNotCreated": "No se pudo crear la conexión",
"connecting": "Conectando...",
@ -726,5 +726,37 @@
"oneIdentifierRequired": "Se require al menos un identificador",
"dnsCacheNumber": "El tamaño de caché de DNS debe ser un número",
"errors": "Errores",
"redirectHttpsWarning": "Si tienes activado \"Redireccionar a HTTPS automáticamente\" en tu servidor AdGuard Home, debes seleccionar una conexión HTTPS y utilizar el puerto de HTTPS de tu servidor."
"redirectHttpsWarning": "Si tienes activado \"Redireccionar a HTTPS automáticamente\" en tu servidor AdGuard Home, debes seleccionar una conexión HTTPS y utilizar el puerto de HTTPS de tu servidor.",
"logsSettingsDescription": "Configura los registros de peticiones",
"ignoredDomains": "Dominios ignorados",
"noIgnoredDomainsAdded": "No hay añadidos dominios para ignorar",
"pauseServiceBlocking": "Pausa del servicio de bloqueo",
"newSchedule": "Nueva programación",
"editSchedule": "Editar programación",
"timezone": "Zona horaria",
"monday": "Lunes",
"tuesday": "Martes",
"wednesday": "Miércoles",
"thursday": "Jueves",
"friday": "Viernes",
"saturday": "Sábado",
"sunday": "Domingo",
"from": "Desde",
"to": "Hasta",
"selectStartTime": "Seleccionar hora de inicio",
"selectEndTime": "Seleccionar hora de fin",
"startTimeBeforeEndTime": "La hora de inicio debe ser anterior a la hora de fin.",
"noBlockingScheduleThisDevice": "No hay programación de bloqueo para este dispositivo.",
"selectTimezone": "Selecciona una zona horaria",
"selectClientsFiltersInfo": "Selecciona los clientes que quieres mostrar. Si no hay clientes seleccionados, se mostrarán todos.",
"noDataThisSection": "No hay datos para esta sección.",
"statisticsSettings": "Ajustes de estadísticas",
"statisticsSettingsDescription": "Configura la recolección de datos para estadísticas",
"loadingStatisticsSettings": "Cargando ajustes de estadísticas...",
"statisticsSettingsLoadError": "Ocurrió un error al cargar los ajustes de estadísticas.",
"customTimeInHours": "Tiempo personalizado (en horas)",
"invalidTime": "Tiempo no válido",
"removeDomain": "Eliminar dominio",
"addDomain": "Añadir dominio",
"notLess1Hour": "El tiempo no puede ser inferior a 1 hora"
}

730
lib/l10n/app_ru.arb Normal file
View file

@ -0,0 +1,730 @@
{
"home": "Главная",
"settings": "Настройки",
"connect": "Подключиться",
"servers": "Серверы",
"createConnection": "Создать подключение",
"editConnection": "Edit connection",
"name": "Имя",
"ipDomain": "IP-адрес или домен",
"path": "Путь",
"port": "Порт",
"username": "Логин",
"password": "Пароль",
"defaultServer": "Сервер по умолчанию",
"general": "Основное",
"connection": "Тип подключения",
"authentication": "Аутентификация",
"other": "Прочее",
"invalidPort": "Неверный порт",
"invalidPath": "Неверный путь",
"invalidIpDomain": "Неверный IP-адрес или домен",
"ipDomainNotEmpty": "IP-адрес или домен не могут быть пустыми",
"nameNotEmpty": "Имя не может быть пустым",
"invalidUsernamePassword": "Неверный логин или пароль",
"tooManyAttempts": "Слишком много попыток. Попробуйте позднее.",
"cantReachServer": "Не удаётся установить соединение с сервером. Проверьте настройки подключения.",
"sslError": "Ошибка SSL. Перейдите в «Настройки» > «Дополнительные настройки» и активируйте «Не проверять SSL-сертификат».",
"unknownError": "Неизвестная ошибка",
"connectionNotCreated": "Не удалось создать подключение",
"connecting": "Подключение...",
"connected": "Подключено",
"selectedDisconnected": "Выбран, но отключён",
"connectionDefaultSuccessfully": "Подключение успешно установлено как «подключение по умолчанию».",
"connectionDefaultFailed": "Не удалось установить «подключением по умолчанию».",
"noSavedConnections": "Нет сохранённых подключений",
"cannotConnect": "Не удается подключиться к серверу",
"connectionRemoved": "Подключение удалено успешно",
"connectionCannotBeRemoved": "Подключение не может быть удалено.",
"remove": "Удалить",
"removeWarning": "Вы уверены, что хотите удалить соединение с этим сервером AdGuard Home?",
"cancel": "Отмена",
"defaultConnection": "Подключение по умолчанию",
"setDefault": "Подключаться по умолчанию",
"edit": "Редактировать",
"delete": "Удалить",
"save": "Сохранить",
"serverStatus": "Статус сервера",
"connectionNotUpdated": "Соединение не было обновлено",
"ruleFilteringWidget": "Правила фильтрации",
"safeBrowsingWidget": "Безопасная навигация",
"parentalFilteringWidget": "Родительский контроль",
"safeSearchWidget": "Безопасный поиск",
"ruleFiltering": "Правила фильтрации",
"safeBrowsing": "Безопасная\nнавигация",
"parentalFiltering": "Родительский\nконтроль",
"safeSearch": "Безопасный поиск",
"serverStatusNotRefreshed": "Не удалось обновить статус сервера",
"loadingStatus": "Загрузка...",
"errorLoadServerStatus": "Не удалось получить статус сервера",
"topQueriedDomains": "Часто запрашиваемые\nдомены",
"viewMore": "Показать больше",
"topClients": "Частые клиенты",
"topBlockedDomains": "Часто блокируемые домены",
"appSettings": "Настройки приложения",
"theme": "Тема",
"light": "Светлая",
"dark": "Тёмная",
"systemDefined": "Системная тема",
"close": "Закрыть",
"connectedTo": "Подключено к:",
"selectedServer": "Выбранный сервер:",
"noServerSelected": "Нет выбранных серверов",
"manageServer": "Управление сервером",
"allProtections": "Защита",
"userNotEmpty": "Логин не может быть пустым",
"passwordNotEmpty": "Пароль не может быть пустым",
"examplePath": "Например: /adguard",
"helperPath": "Если используется реверсивный прокси",
"aboutApp": "О приложении",
"appVersion": "Версия приложения",
"createdBy": "Автор",
"clients": "Клиенты",
"allowed": "Обработан",
"blocked": "Заблокировано",
"noClientsList": "Список клиентов пуст",
"activeClients": "Активные",
"removeClient": "Удалить запись",
"removeClientMessage": "Вы уверены, что хотите удалить данную запись из списка?",
"confirm": "Ок",
"removingClient": "Удаление клиента...",
"clientNotRemoved": "Клиент не может быть удалён из списка",
"addClient": "Добавить клиента",
"list": "Список",
"ipAddress": "IP-адреса",
"ipNotValid": "Недопустимый IP-адрес",
"clientAddedSuccessfully": "Запись успешно добавлена в список",
"addingClient": "Добавление клиента...",
"clientNotAdded": "Клиент не может быть внесён в список",
"clientAnotherList": "Данный клиент уже занесён в один из списков",
"noSavedLogs": "Нет сохранённых журналов",
"logs": "Журнал",
"copyLogsClipboard": "Скопировать журнал в буфер обмена",
"logsCopiedClipboard": "Журнал скопирован в буфер обмена",
"advancedSettings": "Дополнительные настройки",
"dontCheckCertificate": "Не проверять SSL-сертификат",
"dontCheckCertificateDescription": "Переопределяет проверку SSL-сертификата сервера",
"advancedSetupDescription": "Расширенные параметры",
"settingsUpdatedSuccessfully": "Настройки успешно обновлены.",
"cannotUpdateSettings": "Не удалось обновить настройки.",
"restartAppTakeEffect": "Перезапустите приложение",
"loadingLogs": "Загрузка журнала...",
"logsNotLoaded": "Не удалось загрузить журнал",
"processed": "Обработан\nБез списка",
"processedRow": "Обработан",
"blockedBlacklist": "Заблокирован\nЧёрный список",
"blockedBlacklistRow": "Заблокирован Чёрным списком",
"blockedSafeBrowsing": "Заблокирован\nБезопасная навигация",
"blockedSafeBrowsingRow": "Заблокировано Безопасной навигацией",
"blockedParental": "Заблокирован\nРодительский контроль",
"blockedParentalRow": "Заблокировано Родительским контролем",
"blockedInvalid": "Заблокировано\nНеверный",
"blockedInvalidRow": "Заблокирован (Неверный)",
"blockedSafeSearch": "Заблокирован\nБезопасный поиск",
"blockedSafeSearchRow": "Заблокировано Безопасным поиском",
"blockedService": "Заблокирован\nЗаблокированный сервис",
"blockedServiceRow": "Заблокирован (заблокированный сервис)",
"processedWhitelist": "Разрешён\nБелый список",
"processedWhitelistRow": "Разрешён Белым списком",
"processedError": "Обработан\nОшибка",
"processedErrorRow": "Обработан (ошибка)",
"rewrite": "Переписан",
"status": "Статус",
"result": "Результат",
"time": "Время",
"blocklist": "Блок-лист",
"request": "Запрос",
"domain": "Хост",
"type": "Тип",
"clas": "Класс",
"response": "Ответ",
"dnsServer": "DNS-сервер",
"elapsedTime": "Затрачено",
"responseCode": "Код ответа",
"client": "Клиент",
"deviceIp": "IP-адрес",
"deviceName": "Имя устройства",
"logDetails": "Детали запроса",
"blockingRule": "Правило блокировки",
"blockDomain": "Заблокировать домен",
"couldntGetFilteringStatus": "Не удалось получить журнал фильтрации",
"unblockDomain": "Разблокировать домен",
"userFilteringRulesNotUpdated": "Не удалось обновить пользовательские правила фильтрации",
"userFilteringRulesUpdated": "Пользовательские правила фильтрации успешно обновлены",
"savingUserFilters": "Сохранение пользовательских правил фильтрации...",
"filters": "Фильтры",
"logsOlderThan": "Записи журнала старше, чем",
"responseStatus": "Статус ответа",
"selectTime": "Выберите время",
"notSelected": "Не выбрано",
"resetFilters": "Сбросить фильтры",
"noLogsDisplay": "Нет записей в журнале",
"noLogsThatOld": "Возможно, что за это выбранное время записи журнала не сохранены. Попробуйте выбрать более позднее время.",
"apply": "Применить",
"selectAll": "Отметить все",
"unselectAll": "Отменить выбор всех",
"all": "Все",
"filtered": "Отфильтрованные",
"checkAppLogs": "Проверьте журналы приложения",
"refresh": "Обновить",
"search": "Поиск",
"dnsQueries": "DNS-запросы",
"average": "Среднее",
"blockedFilters": "Заблокировано\nфильтрами",
"malwarePhishingBlocked": "Заблокированные\nвредоносные и\nфишинговые сайты",
"blockedAdultWebsites": "Заблокированные\n«взрослые» сайты",
"generalSettings": "Основные настройки",
"generalSettingsDescription": "Различные настройки",
"hideZeroValues": "Скрывать нулевые значения",
"hideZeroValuesDescription": "Скрывать блоки с нулевыми значениями на домашнем экране",
"webAdminPanel": "Веб-панель администрирования",
"visitGooglePlay": "Посетить страницу в Google Play",
"gitHub": "Исходный код приложения доступен на GitHub",
"blockClient": "Заблокировать клиента",
"selectTags": "Выбрать теги клиента",
"noTagsSelected": "Нет выбранных тегов",
"tags": "Теги",
"identifiers": "Идентификаторы",
"identifier": "Идентификатор",
"identifierHelper": "IP-адрес, CIDR, MAC или ClientID",
"noIdentifiers": "Идентификаторы не добавлены",
"useGlobalSettings": "Глобальные настройки",
"enableFiltering": "Включить фильтрацию",
"enableSafeBrowsing": "Безопасная навигация",
"enableParentalControl": "Родительский контроль",
"enableSafeSearch": "Безопасный поиск",
"blockedServices": "Заблокированные сервисы",
"selectBlockedServices": "Заблокированные сервисы",
"noBlockedServicesSelected": "Нет заблокированных сервисов",
"services": "Сервисы",
"servicesBlocked": "в блокировке",
"tagsSelected": "выбрано",
"upstreamServers": "Upstream DNS-серверы",
"serverAddress": "Адрес сервера",
"noUpstreamServers": "Нет upstream DNS-серверов.",
"willBeUsedGeneralServers": "Будут использоваться общие upstream DNS-сервера.",
"added": "Сохранённые",
"clientUpdatedSuccessfully": "Настройки клиента успешно обновлены",
"clientNotUpdated": "Не удалось обновить настройки клиента",
"clientDeletedSuccessfully": "Клиент успешно удалён",
"clientNotDeleted": "Не удалось удалить клиента",
"options": "Параметры",
"loadingFilters": "Загрузка фильтров...",
"filtersNotLoaded": "Не удалось загрузить фильтры.",
"whitelists": "Белые списки DNS",
"blacklists": "Чёрные списки DNS",
"rules": "Количество правил",
"customRules": "Пользовательские правила фильтрации",
"enabledRules": "Активных правил",
"enabled": "Включён",
"disabled": "Отключён",
"rule": "Правило",
"addCustomRule": "Добавить пользовательское правило фильтрации",
"removeCustomRule": "Удалить пользовательское правило фильтрации",
"removeCustomRuleMessage": "Вы уверены, что хотите удалить данное пользовательское правило фильтрации?",
"updatingRules": "Обновление пользовательских правил фильтрации...",
"ruleRemovedSuccessfully": "Правило успешно удалено",
"ruleNotRemoved": "Не удаётся удалить данное правило",
"ruleAddedSuccessfully": "Правило успешно добавлено",
"ruleNotAdded": "Не удаётся добавить правило",
"noCustomFilters": "Нет пользовательских правил фильтрации",
"noBlockedClients": "Нет запрещённых клиентов",
"noBlackLists": "Нет чёрных списков",
"noWhiteLists": "Нет белых списков",
"addWhitelist": "Добавить белый список",
"addBlacklist": "Добавить чёрный список",
"urlNotValid": "Неверный URL или абсолютный путь",
"urlAbsolutePath": "URL-адрес или абсолютный путь",
"addingList": "Добавление списка...",
"listAdded": "Список успешно добавлен. Добавлены:",
"listAlreadyAdded": "Список уже был добавлен",
"listUrlInvalid": "Неверный URL-адрес списка",
"listNotAdded": "Не удаётся добавить список",
"listDetails": "Параметры списка",
"listType": "Тип",
"whitelist": "Белый список",
"blacklist": "Чёрный список",
"latestUpdate": "Последнее обновление",
"disable": "Отключить",
"enable": "Включить",
"currentStatus": "Текущий статус",
"listDataUpdated": "Параметры списка успешно обновлены",
"listDataNotUpdated": "Не удалось обновить параметры списка",
"updatingListData": "Обновление параметров списка...",
"editWhitelist": "Редактировать белый список",
"editBlacklist": "Редактировать чёрный список",
"deletingList": "Удаление списка...",
"listDeleted": "Список успешно удалён",
"listNotDeleted": "Не удалось удалить список",
"deleteList": "Удалить список",
"deleteListMessage": "Вы уверены, что хотите удалить этот список? Это действие нельзя отменить.",
"serverSettings": "Настройки сервера",
"serverInformation": "Информация о сервере",
"serverInformationDescription": "Информация о сервере и текущий статус",
"loadingServerInfo": "Загрузка информации о сервере...",
"serverInfoNotLoaded": "Не удалось загрузить информацию о сервере.",
"dnsAddresses": "DNS-адреса",
"seeDnsAddresses": "Посмотреть DNS-адреса",
"dnsPort": "DNS-порт",
"httpPort": "HTTP-порт",
"protectionEnabled": "Защита активна",
"dhcpAvailable": "DHCP доступен",
"serverRunning": "Сервер запущен",
"serverVersion": "Версия сервера",
"serverLanguage": "Язык сервера",
"yes": "Да",
"no": "Нет",
"allowedClients": "Разрешённые клиенты",
"disallowedClients": "Запрещённые клиенты",
"disallowedDomains": "Неразрешённые домены",
"accessSettings": "Настройки доступа",
"accessSettingsDescription": "Настройка правил доступа к серверу",
"loadingClients": "Загрузка клиентов...",
"clientsNotLoaded": "Не удалось загрузить список клиентов.",
"noAllowedClients": "Нет разрешённых клиентов",
"allowedClientsDescription": "Если в списке есть записи, AdGuard Home будет принимать запросы только от этих клиентов.",
"blockedClientsDescription": "Если в списке есть записи, AdGuard Home будет игнорировать запросы от этих клиентов. Это поле игнорируется, если список разрешённых клиентов содержит записи.",
"disallowedDomainsDescription": "AdGuard Home будет игнорировать DNS-запросы с этими доменами. Такие DNS-запросы не будут отображаться в журнале.",
"addClientFieldDescription": "CIDR, IP-адрес или ClientID",
"clientIdentifier": "Идентификатор клиента",
"allowClient": "Разрешить клиент",
"disallowClient": "Запретить клиента",
"noDisallowedDomains": "Нет запрещенных доменов",
"domainNotAdded": "Не удалось добавить домен",
"statusSelected": "выбран статус",
"updateLists": "Проверить обновления",
"checkHostFiltered": "Проверить хост",
"updatingLists": "Обновление списков...",
"listsUpdated": "списки обновлены",
"listsNotUpdated": "Не удалось обновить списки",
"listsNotLoaded": "Не удалось загрузить списки",
"domainNotValid": "Недействительный домен",
"check": "Проверить",
"checkingHost": "Проверка хоста...",
"errorCheckingHost": "Не удалось проверить хост",
"block": "Запретить",
"unblock": "Разрешить",
"custom": "Своё правило",
"addImportant": "Добавить $important",
"howCreateRules": "Как создать пользовательские правила",
"examples": "Примеры",
"example1": "Заблокировать доступ к домену example.org и всем его поддоменам.",
"example2": "Разблокировать доступ к домену example.org и всем его поддоменам.",
"example3": "Добавлять комментарий.",
"example4": "Блокировать доступ к доменам, соответствующим заданному регулярному выражению.",
"moreInformation": "Больше информации",
"addingRule": "Добавление правила...",
"deletingRule": "Удаление правила...",
"enablingList": "Включение списка...",
"disablingList": "Отключение списка...",
"disableFiltering": "Отключить фильтрацию",
"savingList": "Сохранение списка...",
"enablingFiltering": "Включение фильтрации...",
"disablingFiltering": "Отключение фильтрации...",
"filteringStatusUpdated": "Статус фильтрации успешно обновлен",
"filteringStatusNotUpdated": "Не удалось обновить статус фильтрации",
"updateFrequency": "Частота обновления",
"never": "Никогда",
"hour1": "1 час",
"hours12": "12 часов",
"hours24": "24 часа",
"days3": "3 дня",
"days7": "7 дней",
"changingUpdateFrequency": "Обновление...",
"updateFrequencyChanged": "Частота обновления успешно изменена",
"updateFrequencyNotChanged": "Не удаётся изменить частоту обновления",
"updating": "Обновление значений...",
"blockedServicesUpdated": "Заблокированные сервисы успешно обновлены",
"blockedServicesNotUpdated": "Не удаётся обновить заблокированные сервисы",
"insertDomain": "Проверить фильтрацию имени хоста.",
"dhcpSettings": "Настройки DHCP",
"dhcpSettingsDescription": "Настройка DHCP-сервера",
"dhcpSettingsNotLoaded": "Не удалось загрузить настройки DHCP",
"loadingDhcp": "Загрузка настроек DHCP...",
"enableDhcpServer": "Включить DHCP-сервер",
"selectInterface": "Выбрать интерфейс DHCP",
"hardwareAddress": "MAC-адрес",
"gatewayIp": "IP-адрес шлюза",
"ipv4addresses": "Адрес IPv4",
"ipv6addresses": "Адрес IPv6",
"neededSelectInterface": "Необходимо выбрать интерфейс для настройки DHCP-сервера.",
"ipv4settings": "Настройки IPv4",
"startOfRange": "Начало диапазона",
"endOfRange": "Конец диапазона",
"ipv6settings": "Настройки IPv6",
"subnetMask": "Маска подсети",
"subnetMaskNotValid": "Недопустимая маска подсети",
"gateway": "Шлюз",
"gatewayNotValid": "Недопустимый шлюз",
"leaseTime": "Время аренды",
"seconds": "{time} секунд",
"leaseTimeNotValid": "Недопустимый срок аренды",
"restoreConfiguration": "Сбросить конфигурацию",
"restoreConfigurationMessage": "Вы уверены, что хотите продолжить? Это приведет к сбросу всей конфигурации. Данное действие не может быть отменено.",
"changeInterface": "Изменить интерфейс",
"savingSettings": "Сохранение настроек...",
"settingsSaved": "Настройки успешно сохранены",
"settingsNotSaved": "Не удалось сохранить настройки",
"restoringConfig": "Восстановление конфигурации...",
"configRestored": "Конфигурация успешно сброшена",
"configNotRestored": "Не удалось произвести сброс конфигурации",
"dhcpStatic": "Статические аренды DHCP",
"noDhcpStaticLeases": "Не найдено статических аренд DHCP",
"deleting": "Удаление...",
"staticLeaseDeleted": "Статическая аренда DHCP успешно удалена",
"staticLeaseNotDeleted": "Не удалось удалить статическую аренду DHCP",
"deleteStaticLease": "Удалить статическую аренду",
"deleteStaticLeaseDescription": "Статическая аренда DHCP будет удалена. Данное действие не может быть отменено.",
"addStaticLease": "Добавить статическую аренду",
"macAddress": "MAC-адрес",
"macAddressNotValid": "Недопустимый MAC-адрес",
"hostName": "Имя хоста",
"hostNameError": "Имя хоста не может быть пустым",
"creating": "Создание...",
"staticLeaseCreated": "Статическая аренда DHCP успешно создана",
"staticLeaseNotCreated": "Не удалось создать статическую аренду DHCP",
"staticLeaseExists": "Статическая аренда DHCP уже существует",
"serverNotConfigured": "Сервер не настроен",
"restoreLeases": "Сбросить аренды DHCP",
"restoreLeasesMessage": "Вы уверены, что хотите продолжить? Это приведет к сбросу всех существующих аренд DHCP. Данное действие не может быть отменено.",
"restoringLeases": "Сброс аренд DHCP...",
"leasesRestored": "Аренды DHCP успешно сброшены",
"leasesNotRestored": "Не удалось сбросить аренды DHCP",
"dhcpLeases": "Аренды DHCP",
"noLeases": "Не найдено аренд DHCP",
"dnsRewrites": "Перезапись DNS-запросов",
"dnsRewritesDescription": "Настройка пользовательских правил DNS",
"loadingRewriteRules": "Загрузка правил перезаписи...",
"rewriteRulesNotLoaded": "Не удалось загрузить правила перезаписи DNS.",
"noRewriteRules": "Нет правил перезаписи DNS",
"answer": "Ответ",
"deleteDnsRewrite": "Удалить правило перезаписи DNS-запросов",
"deleteDnsRewriteMessage": "Вы уверены, что хотите удалить это правило перезаписи DNS? Данное действие не может быть отменено.",
"dnsRewriteRuleDeleted": "Правило перезаписи DNS успешно удалено",
"dnsRewriteRuleNotDeleted": "Не удалось удалить правило перезаписи DNS",
"addDnsRewrite": "Добавить правило",
"addingRewrite": "Добавление правила перезаписи DNS-запросов...",
"dnsRewriteRuleAdded": "Правило перезаписи DNS успешно добавлено",
"dnsRewriteRuleNotAdded": "Не удалось добавить правило перезаписи DNS",
"logsSettings": "Настройки журнала",
"enableLog": "Включить журнал",
"clearLogs": "Очистить журнал",
"anonymizeClientIp": "Анонимизировать клиента",
"hours6": "6 часов",
"days30": "30 дней",
"days90": "90 дней",
"retentionTime": "Частота ротации журнала запросов",
"selectOneItem": "Выберите один элемент",
"logSettingsNotLoaded": "Не удалось загрузить настройки журнала.",
"updatingSettings": "Обновление настроек...",
"logsConfigUpdated": "Настройки журнала успешно обновлены",
"logsConfigNotUpdated": "Не удалось обновить настройки журнала",
"deletingLogs": "Очистка журнала...",
"logsCleared": "Журнал успешно очищен",
"logsNotCleared": "Не удалось очистить журнал",
"runningHomeAssistant": "Запускается на Home Assistant",
"serverError": "Ошибка сервера",
"noItems": "Здесь нет предметов для показа",
"dnsSettings": "Настройки DNS",
"dnsSettingsDescription": "Настройка подключения к DNS-серверам",
"upstreamDns": "Upstream DNS-серверы",
"bootstrapDns": "Bootstrap DNS-серверы",
"noUpstreamDns": "Не добавлены upstream DNS-серверы.",
"dnsMode": "Режим DNS",
"noDnsMode": "Не выбран режим DNS",
"loadBalancing": "Распределение нагрузки",
"parallelRequests": "Параллельные запросы",
"fastestIpAddress": "Самый быстрый IP-адрес",
"loadBalancingDescription": "Запрашивать по одному серверу за раз. AdGuard Home использует алгоритм взвешенного случайного выбора сервера, так что самый быстрый сервер используется чаще.",
"parallelRequestsDescription": "Использовать параллельные запросы ко всем серверам одновременно для ускорения обработки запроса.",
"fastestIpAddressDescription": "Опросить все DNS-серверы и вернуть самый быстрый IP-адрес из полученных ответов. Это замедлит DNS-запросы, так как нужно будет дождаться ответов со всех DNS-серверов, но улучшит соединение.",
"noBootstrapDns": "Не добавлены bootstrap DNS-серверы.",
"bootstrapDnsServersInfo": "Bootstrap DNS-сервера используются для поиска IP-адресов DoH/DoT upstream-серверов, которые вы указали.",
"privateReverseDnsServers": "Приватные серверы для обратного DNS",
"privateReverseDnsServersDescription": "DNS-серверы, которые AdGuard Home использует для локальных PTR-запросов. Эти серверы используются, чтобы получить доменные имена клиентов с приватными IP-адресами, например «192.168.12.34», с помощью обратного DNS. Если список пуст, AdGuard Home использует DNS-серверы по умолчанию вашей ОС.",
"reverseDnsDefault": "По умолчанию AdGuard Home использует следующие обратные DNS-серверы",
"addItem": "Добавить сервер",
"noServerAddressesAdded": "Адреса серверов не указаны.",
"usePrivateReverseDnsResolvers": "Использовать приватные обратные DNS-резолверы",
"usePrivateReverseDnsResolversDescription": "Посылать обратные DNS-запросы для локально обслуживаемых адресов на указанные серверы. Если отключено, AdGuard Home будет отвечать NXDOMAIN на все подобные PTR-запросы, кроме запросов о клиентах, уже известных по DHCP, /etc/hosts и так далее.",
"enableReverseResolving": "Включить запрашивание доменных имён для IP-адресов клиентов",
"enableReverseResolvingDescription": "Определять доменные имена клиентов через PTR-запросы к соответствующим серверам (приватные DNS-серверы для локальных клиентов, upstream-серверы для клиентов с публичным IP-адресом).",
"dnsServerSettings": "Настройки DNS-сервера",
"limitRequestsSecond": "Лимит запросов в секунду",
"valueNotNumber": "Значение - не число",
"enableEdns": "Включить отправку EDNS Client Subnet",
"enableEdnsDescription": "Добавлять опцию EDNS Client Subnet (ECS) к запросам к upstream-серверам, а также записывать присланные клиентами значения в журнал.",
"enableDnssec": "Включить DNSSEC",
"enableDnssecDescription": "Установите флаг DNSSEC в исходящих DNS-запросах и проверьте результат (требуется резолвер с поддержкой DNSSEC).",
"disableResolvingIpv6": "Отключить обработку IPv6-адресов",
"disableResolvingIpv6Description": "Игнорировать все DNS-запросы адресов IPv6 (тип AAAA) и удалять IPv6-данные из ответов типа HTTPS.",
"blockingMode": "Режим блокировки",
"defaultMode": "Стандартный",
"defaultDescription": "Отвечает с нулевым IP-адресом, (0.0.0.0 для A; :: для AAAA) когда заблокировано правилом в стиле Adblock; отвечает с IP-адресом, указанным в правиле, когда заблокировано правилом в стиле файлов hosts",
"refusedDescription": "Отвечает с кодом REFUSED",
"nxdomainDescription": "Отвечает с кодом NXDOMAIN",
"nullIp": "Нулевой IP",
"nullIpDescription": "Отвечает с нулевым IP-адресом (0.0.0.0 для A; :: для AAAA)",
"customIp": "Пользовательский IP",
"customIpDescription": "Отвечает с вручную настроенным IP-адресом",
"dnsCacheConfig": "Настройка кеша DNS",
"cacheSize": "Размер кеша",
"inBytes": "В байтах",
"overrideMinimumTtl": "Переопределить минимальный TTL",
"overrideMinimumTtlDescription": "Расширить короткие TTL-значения (в секундах), полученные с upstream-сервера при кешировании DNS-ответов.",
"overrideMaximumTtl": "Переопределить максимальный TTL",
"overrideMaximumTtlDescription": "Установить максимальное TTL-значение (в секундах) для записей в DNS-кеше.",
"optimisticCaching": "Оптимистическое кеширование",
"optimisticCachingDescription": "AdGuard Home будет отвечать из кеша, даже если ответы в нём неактуальны, и попытается обновить их.",
"loadingDnsConfig": "Загрузка конфигурации DNS...",
"dnsConfigNotLoaded": "Не удалось загрузить конфигурацию DNS.",
"blockingIpv4": "Блокируется IPv4",
"blockingIpv4Description": "IP-адрес, который будет возвращен для заблокированного запроса А",
"blockingIpv6": "Блокируется IPv6",
"blockingIpv6Description": "IP-адрес, который будет возвращен для заблокированного запроса AAAA",
"invalidIp": "Недопустимый IP-адрес",
"dnsConfigSaved": "Конфигурация DNS-сервера сохранена успешно",
"dnsConfigNotSaved": "Не удалось сохранить конфигурацию DNS-сервера",
"savingConfig": "Сохранение конфигурации...",
"someValueNotValid": "Некоторое значение недопустимо",
"upstreamDnsDescription": "Настройка upstream DNS-серверов и режима DNS",
"bootstrapDnsDescription": "Настройка bootstrap DNS-серверов",
"privateReverseDnsDescription": "Настройка пользовательских DNS-серверов и приватных серверы для обратного DNS",
"dnsServerSettingsDescription": "Настройка ограничения на количество запросов, режима блокировки и многое другое",
"dnsCacheConfigDescription": "Настройка, как сервер должен управлять кэшем DNS",
"comment": "Комментарий",
"address": "Адрес",
"commentsDescription": "Комментариям всегда предшествует #. Вам не обязательно добавлять его, он будет добавлен автоматически.",
"encryptionSettings": "Настройки шифрования",
"encryptionSettingsDescription": "Поддержка шифрования (HTTPS/QUIC/TLS)",
"loadingEncryptionSettings": "Загрузка настроек шифрования...",
"encryptionSettingsNotLoaded": "Не удалось загрузить настройки шифрования.",
"enableEncryption": "Включить шифрование",
"enableEncryptionTypes": "HTTPS, DNS-over-HTTPS и DNS-over-TLS",
"enableEncryptionDescription": "Если порт HTTPS настроен, веб-интерфейс администрирования AdGuard Home будет доступен через HTTPS, а также DNS-over-HTTPS сервер будет доступен по пути '/dns-query'.",
"serverConfiguration": "Конфигурация сервера",
"domainName": "Доменное имя",
"domainNameDescription": "Если задано, AdGuard Home распознаёт ClientID, отвечает на DDR-запросы, и дополнительно проверяет соединения. Если не задано, этот функционал отключён. Должно соответствовать одному из параметров DNS Names в сертификате.",
"redirectHttps": "Автоматически перенаправлять на HTTPS",
"httpsPort": "Порт HTTPS",
"tlsPort": "Порт DNS-over-TLS",
"dnsOverQuicPort": "Порт DNS-over-QUIC",
"certificates": "Сертификаты",
"certificatesDescription": "Для использования шифрования вам необходимо предоставить корректную цепочку SSL-сертификатов для вашего домена. Вы можете получить бесплатный сертификат на letsencrypt.org или вы можете купить его у одного из доверенных Центров Сертификации.",
"certificateFilePath": "Указать путь к файлу сертификатов",
"pasteCertificateContent": "Вставить содержимое сертификатов",
"certificatePath": "Путь к сертификату",
"certificateContent": "Содержимое сертификата",
"privateKey": "Закрытый ключ",
"privateKeyFile": "Указать файл закрытого ключа",
"pastePrivateKey": "Вставить содержимое закрытого ключа",
"usePreviousKey": "Использовать сохранённый ранее ключ",
"privateKeyPath": "Путь к закрытому ключу",
"invalidCertificate": "Цепочка сертификатов не прошла проверку",
"invalidPrivateKey": "Некорректный приватный ключ",
"validatingData": "Проверка данных",
"dataValid": "Данные действительны",
"dataNotValid": "Недопустимые данные",
"encryptionConfigSaved": "Настройки шифрования успешно сохранены",
"encryptionConfigNotSaved": "Не удаётся сохранить настройки шифрования",
"configError": "Ошибка конфигурации",
"enterOnlyCertificate": "Введите только сертификат. Не вводите строки ---BEGIN--- и ---END---.",
"enterOnlyPrivateKey": "Введите только ключ. Не вводите строки ---BEGIN--- и ---END---.",
"noItemsSearch": "Ничего не найдено по данному запросу.",
"clearSearch": "Очистить поиск",
"exitSearch": "Покинуть поиск",
"searchClients": "Поиск клиентов",
"noClientsSearch": "Не найдено клиентов по данному запросу.",
"customization": "Персонализация",
"customizationDescription": "Настройте внешний вид приложения",
"color": "Цветовая тема",
"useDynamicTheme": "Использовать динамическую тему",
"red": "Красный",
"green": "Зелёный",
"blue": "Синий",
"yellow": "Жёлтый",
"orange": "Оранжевый",
"brown": "Коричневый",
"cyan": "Бирюзовый",
"purple": "Пурпурный",
"pink": "Розовый",
"deepOrange": "Темно-оранжевый",
"indigo": "Индиго",
"useThemeColorStatus": "Использовать цвет темы для обозначения статуса",
"useThemeColorStatusDescription": "Заменяет зеленый и красный цвета статуса цветом темы и серым",
"invalidCertificateChain": "Цепочка сертификатов не прошла проверку",
"validCertificateChain": "Действительная цепочка сертификатов",
"subject": "Субъект",
"issuer": "Издатель",
"expires": "Истекает",
"validPrivateKey": "Действительный закрытый ключ",
"expirationDate": "Истекает",
"keysNotMatch": "Недействительный сертификат или ключ: tls: закрытый ключ не соответствует открытому ключу",
"timeLogs": "Время в записях журнала",
"timeLogsDescription": "Показывать время обработки в журнале",
"hostNames": "Имена хостов",
"keyType": "Тип ключа",
"updateAvailable": "Доступно обновление",
"installedVersion": "Установленная версия",
"newVersion": "Новая версия",
"source": "Источник",
"downloadUpdate": "Загрузить обновление",
"download": "Скачать",
"doNotRememberAgainUpdate": "Не запоминать снова для этой версии",
"downloadingUpdate": "Скачивание",
"completed": "завершено",
"permissionNotGranted": "Разрешение не предоставлено",
"inputSearchTerm": "Введите поисковый запрос.",
"answers": "Ответы",
"copyClipboard": "Скопировать в буфер обмена",
"domainCopiedClipboard": "Домен скопирован в буфер обмена",
"clearDnsCache": "Очистить кэш DNS",
"clearDnsCacheMessage": "Вы уверены, что хотите очистить кэш DNS?",
"dnsCacheCleared": "Кэш DNS очищен успешно",
"clearingDnsCache": "Очистка кэша...",
"dnsCacheNotCleared": "Не удалось очистить кэш DNS",
"clientsSelected": "выбранные клиенты",
"invalidDomain": "Недопустимый домен",
"loadingBlockedServicesList": "Загрузка списка заблокированных сервисов...",
"blockedServicesListNotLoaded": "Не удалось загрузить список заблокированных служб",
"error": "Ошибка",
"updates": "Обновления",
"updatesDescription": "Обновить AdGuard Home server",
"updateNow": "Обновить сейчас",
"currentVersion": "Текущая версия",
"requestStartUpdateFailed": "Не удалось выполнить запрос на запуск обновления",
"requestStartUpdateSuccessful": "Запрос на запуск обновления успешен",
"serverUpdated": "Сервер обновлён",
"unknownStatus": "Неизвестный статус",
"checkingUpdates": "Проверка обновлений..",
"checkUpdates": "Проверить обновления",
"requestingUpdate": "Запрос обновления...",
"autoupdateUnavailable": "Автообновление недоступно",
"autoupdateUnavailableDescription": "Служба автоматического обновления недоступна для этого сервера. Это может быть связано с тем, что сервер запущен в контейнере Docker. Вам необходимо обновить свой сервер вручную.",
"minute": "{time} минута",
"minutes": "{time} минут",
"hour": "{time} час",
"hours": "{time} часов",
"remainingTime": "Оставшееся время",
"safeSearchSettings": "Настройки безопасного поиска",
"loadingSafeSearchSettings": "Загрузка настроек безопасного поиска...",
"safeSearchSettingsNotLoaded": "Ошибка при загрузке настроек безопасного поиска.",
"loadingLogsSettings": "Загрузка настроек журнала...",
"selectOptionLeftColumn": "Выберите опцию в левой колонке",
"selectClientLeftColumn": "Выберите клиента в левой колонке",
"disableList": "Отключить список",
"enableList": "Включить список",
"screens": "Экраны",
"copiedClipboard": "Скопировано в буфер обмена",
"seeDetails": "Смотрите подробности",
"listNotAvailable": "Список недоступен",
"copyListUrl": "Скопировать URL",
"listUrlCopied": "URL списка сохранён в буфер обмена",
"unsupportedVersion": "Неподдерживаемая версия",
"unsupprtedVersionMessage": "Поддержка AdGuard Home версии {version} не гарантируется. Приложение может работать нестабильно с данной версией сервера.\n\nПриложение AdGuard Home Manager предназначено для работы со стабильными версиями AdGuard Home. Приложение может работать с альфа и бета версиями сервера, но совместимость и стабильность не гарантируются.",
"iUnderstand": "Продолжить",
"appUpdates": "Обновления приложений",
"usingLatestVersion": "Вы используете последнюю версию",
"ipLogs": "IP-адреса в записях журнала",
"ipLogsDescription": "Всегда показывать IP-адрес в записях журнала вместо имени клиента",
"application": "Приложение",
"combinedChart": "Объединять графики",
"combinedChartDescription": "Комбинирует все графики в один",
"statistics": "Статистика",
"errorLoadFilters": "Ошибка при загрузке фильтров.",
"clientRemovedSuccessfully": "Запись успешно удалена.",
"editRewriteRule": "Редактировать правило",
"dnsRewriteRuleUpdated": "Правило перезаписи DNS успешно обновлено",
"dnsRewriteRuleNotUpdated": "Не удалось обновить правило перезаписи DNS",
"updatingRule": "Обновление правила...",
"serverUpdateNeeded": "Требуется обновление сервера",
"updateYourServer": "Обновите сервер AdGuard Home до версии {version} или выше, чтобы использовать эту функцию.",
"january": "Январь",
"february": "Февраль",
"march": "Март",
"april": "Апрель",
"may": "Май",
"june": "Июнь",
"july": "Июль",
"august": "Август",
"september": "Сентябрь",
"october": "Октябрь",
"november": "Ноябрь",
"december": "Декабрь",
"malwarePhising": "Вредоносные/фишинговые сайты",
"queries": "Запросы",
"adultSites": "«Взрослые» сайты",
"quickFilters": "Быстрые фильтры",
"searchDomainInternet": "Поиск домена в Интернете",
"hideServerAddress": "Скрывать адрес сервера",
"hideServerAddressDescription": "Скрывает адрес сервера на главном экране",
"topItemsOrder": "Расположение блоков на главном экране",
"topItemsOrderDescription": "Упорядочьте расположение блоков на главном экране",
"topItemsReorderInfo": "Чтобы менять порядок элементов, удерживая элемент, перетащите его на новое место.",
"discardChanges": "Отменить изменения",
"discardChangesDescription": "Вы уверены, что хотите отменить изменения?",
"others": "Прочее",
"showChart": "Показать график",
"hideChart": "Скрыть график",
"showTopItemsChart": "Показывать ТОП-графики на главной странице",
"showTopItemsChartDescription": "По умолчанию на главной странице отображаются круговые диаграммы для часто запрашиваемых доменов, частых клиентов и прочего. Влияет только на просмотр с мобильного устройства",
"openMenu": "Открыть меню",
"closeMenu": "Закрыть меню",
"openListUrl": "Открыть URL списка",
"selectionMode": "Режим выбора",
"enableDisableSelected": "Включить или выключить выбранные элементы",
"deleteSelected": "Удалить выбранные элементы",
"deleteSelectedLists": "Удалить выбранные списки",
"allSelectedListsDeletedSuccessfully": "Все выбранные списки успешно удалены.",
"deletionResult": "Результат удаления",
"deletingLists": "Удаление списков...",
"failedElements": "Неудачные элементы",
"processingLists": "Обработка списков...",
"enableDisableResult": "Включить или выключить результат",
"selectedListsEnabledDisabledSuccessfully": "Все выбранные списки были включены или выключены успешно",
"sslWarning": "Если используется HTTPS-соединение с самоподписанным сертификатом, то должна быть активирована опция «Не проверять SSL-сертификат» в разделе «Настройки» > «Дополнительные настройки».",
"unsupportedServerVersion": "Неподдерживаемая версия сервера",
"unsupportedServerVersionMessage": "Данная версия AdGuard Home устарела и не поддерживается AdGuard Home Manager. Чтобы использовать данное приложение, необходимо выполнить обновление AdGuard Home до актуальной версии.",
"yourVersion": "Ваша версия: {version}",
"minimumRequiredVersion": "Минимальная требуемая версия: {version}",
"topUpstreams": "Часто запрашиваемые\nupstream-серверы",
"averageUpstreamResponseTime": "Среднее время отклика\nupstream-сервера",
"dhcpNotAvailable": "DHCP сервер не доступен.",
"osServerInstalledIncompatible": " Операционная система, в которой установлен сервер, несовместима с этой функцией.",
"resetSettings": "Сбросить настройки",
"resetEncryptionSettingsDescription": "Вы уверены, что хотите сбросить настройки шифрования к значениям по умолчанию?",
"resettingConfig": "Сброс конфигурации...",
"configurationResetSuccessfully": "Конфигурация успешно сброшена",
"configurationResetError": "Не удалось сбросить конфигурацию",
"testUpstreamDnsServers": "Тест upstream DNS-серверов",
"errorTestUpstreamDns": "Ошибка при тестировании upstream DNS-серверов.",
"useCustomIpEdns": "Use custom IP for EDNS",
"useCustomIpEdnsDescription": "Использовать собственный IP-адрес для EDNS",
"sortingOptions": "Параметры сортировки",
"fromHighestToLowest": "От большего к меньшему",
"fromLowestToHighest": "От меньшего к большему",
"queryLogsAndStatistics": "Журналы запросов и статистика",
"ignoreClientQueryLog": "Игнорировать этого клиента в журнале запросов",
"ignoreClientStatistics": "Игнорировать этого клиента в статистике",
"savingChanges": "Сохранение изменений...",
"fallbackDnsServers": "Резервные DNS-серверы",
"fallbackDnsServersDescription": "Настроить резервные DNS-серверы",
"fallbackDnsServersInfo": "Список резервных DNS-серверов, используемых в тех случаях, когда вышестоящие DNS-серверы недоступны. Синтаксис такой же, как и в поле Upstream DNS-серверы выше.",
"noFallbackDnsAdded": "Резервные DNS-серверы не добавлены.",
"blockedResponseTtl": "TTL заблокированного ответа",
"blockedResponseTtlDescription": "Указывает, в течение скольких секунд клиенты должны кешировать отфильтрованный ответ",
"invalidValue": "Недопустимое значение",
"noDataChart": "Нет данных для отображения графика.",
"noData": "Нет данных",
"unblockClient": "Разблокировать клиента",
"blockingClient": "Блокировка клиента...",
"unblockingClient": "Снятие блокироваки с клиента...",
"upstreamDnsCacheConfiguration": "Конфигурация кеша upstream DNS-серверов",
"enableDnsCachingClient": "Включить кеширование для пользовательской конфигурации upstream-серверов этого клиента",
"dnsCacheSize": "Размер DNS-кеша",
"nameInvalid": "Требуется имя",
"oneIdentifierRequired": "Требуется по крайней мере один идентификатор",
"dnsCacheNumber": "Размер кэша DNS должен быть числом",
"errors": "Ошибки",
"redirectHttpsWarning": "Если в AdGuard Home активирована опция «Автоматически перенаправлять на HTTPS», то необходимо использовать HTTPS-соединение и HTTPS-порт."
}

View file

@ -206,7 +206,7 @@ class _MainState extends State<Main> {
@override
Widget build(BuildContext context) {
final appConfigProvider = Provider.of<AppConfigProvider>(context);
return DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) => MaterialApp(
title: 'AdGuard Home Manager',
@ -234,7 +234,8 @@ class _MainState extends State<Main> {
Locale('zh', ''),
Locale('zh', 'CN'),
Locale('pl', ''),
Locale('tr', '')
Locale('tr', ''),
Locale('ru', '')
],
scaffoldMessengerKey: scaffoldMessengerKey,
navigatorKey: globalNavigatorKey,

View file

@ -91,6 +91,7 @@ class Client {
final bool? ignoreStatistics;
final bool? upstreamsCacheEnabled;
final int? upstreamsCacheSize;
final BlockedServicesSchedule? blockedServicesSchedule;
Client({
required this.name,
@ -108,6 +109,7 @@ class Client {
required this.ignoreStatistics,
required this.upstreamsCacheEnabled,
required this.upstreamsCacheSize,
required this.blockedServicesSchedule,
});
factory Client.fromJson(Map<String, dynamic> json) => Client(
@ -127,7 +129,8 @@ class Client {
ignoreQuerylog: json["ignore_querylog"],
ignoreStatistics: json["ignore_statistics"],
upstreamsCacheEnabled: json["upstreams_cache_enabled"],
upstreamsCacheSize: json["upstreams_cache_size"]
upstreamsCacheSize: json["upstreams_cache_size"],
blockedServicesSchedule: BlockedServicesSchedule.fromJson(json["blocked_services_schedule"])
);
Map<String, dynamic> toJson() => {
@ -145,6 +148,71 @@ class Client {
"ignore_querylog": ignoreQuerylog,
"ignore_statistics": ignoreStatistics,
"upstreams_cache_enabled": upstreamsCacheEnabled,
"upstreams_cache_size": upstreamsCacheSize
"upstreams_cache_size": upstreamsCacheSize,
"blocked_services_schedule":blockedServicesSchedule?.toJson()
};
}
class BlockedServicesSchedule {
final String? timeZone;
final BlockedServicesScheduleDay? mon;
final BlockedServicesScheduleDay? tue;
final BlockedServicesScheduleDay? wed;
final BlockedServicesScheduleDay? thu;
final BlockedServicesScheduleDay? fri;
final BlockedServicesScheduleDay? sat;
final BlockedServicesScheduleDay? sun;
BlockedServicesSchedule({
this.timeZone,
this.mon,
this.tue,
this.wed,
this.thu,
this.fri,
this.sat,
this.sun
});
factory BlockedServicesSchedule.fromJson(Map<String, dynamic> json) => BlockedServicesSchedule(
timeZone: json["time_zone"],
mon: json["mon"] == null ? null : BlockedServicesScheduleDay.fromJson(json["mon"]),
tue: json["tue"] == null ? null : BlockedServicesScheduleDay.fromJson(json["tue"]),
wed: json["wed"] == null ? null : BlockedServicesScheduleDay.fromJson(json["wed"]),
thu: json["thu"] == null ? null : BlockedServicesScheduleDay.fromJson(json["thu"]),
fri: json["fri"] == null ? null : BlockedServicesScheduleDay.fromJson(json["fri"]),
sat: json["sat"] == null ? null : BlockedServicesScheduleDay.fromJson(json["sat"]),
sun: json["sun"] == null ? null : BlockedServicesScheduleDay.fromJson(json["sun"]),
);
Map<String, dynamic> toJson() => {
"time_zone": timeZone,
"mon": mon?.toJson(),
"tue": tue?.toJson(),
"wed": wed?.toJson(),
"thu": thu?.toJson(),
"fri": fri?.toJson(),
"sat": sat?.toJson(),
"sun": sun?.toJson(),
};
}
class BlockedServicesScheduleDay {
final int? start;
final int? end;
BlockedServicesScheduleDay({
this.start,
this.end,
});
factory BlockedServicesScheduleDay.fromJson(Map<String, dynamic> json) => BlockedServicesScheduleDay(
start: json["start"],
end: json["end"],
);
Map<String, dynamic> toJson() => {
"start": start,
"end": end,
};
}

View file

@ -1,11 +1,9 @@
import 'dart:convert';
class DhcpModel {
bool dhcpAvailable;
List<NetworkInterface> networkInterfaces;
DhcpStatus? dhcpStatus;
DhcpModel({
required this.dhcpAvailable,
required this.networkInterfaces,
required this.dhcpStatus,
});
@ -83,8 +81,8 @@ class DhcpStatus {
Map<String, dynamic> toJson() => {
"interface_name": interfaceName,
"v4": v4 != null ? v4!.toJson() : null,
"v6": v6 != null ? v6!.toJson() : null,
"v4": v4?.toJson(),
"v6": v6?.toJson(),
"leases": List<Lease>.from(leases.map((x) => x)),
"static_leases": List<Lease>.from(staticLeases.map((x) => x)),
"enabled": enabled,

View file

@ -0,0 +1,27 @@
class QueryLogConfig {
final List<String>? ignored;
final int? interval;
final bool? enabled;
final bool? anonymizeClientIp;
QueryLogConfig({
this.ignored,
this.interval,
this.enabled,
this.anonymizeClientIp,
});
factory QueryLogConfig.fromJson(Map<String, dynamic> json) => QueryLogConfig(
ignored: json["ignored"] == null ? [] : List<String>.from(json["ignored"]!.map((x) => x)),
interval: json["interval"],
enabled: json["enabled"],
anonymizeClientIp: json["anonymize_client_ip"],
);
Map<String, dynamic> toJson() => {
"ignored": ignored == null ? [] : List<dynamic>.from(ignored!.map((x) => x)),
"interval": interval,
"enabled": enabled,
"anonymize_client_ip": anonymizeClientIp,
};
}

View file

@ -21,6 +21,7 @@ class ServerStatus {
bool? safeSearchPixabay;
bool? safeSearchYandex;
bool? safeSearchYoutube;
bool dhcpAvailable;
ServerStatus({
required this.stats,
@ -39,7 +40,8 @@ class ServerStatus {
required this.safeSearchDuckduckgo,
required this.safeSearchPixabay,
required this.safeSearchYandex,
required this.safeSearchYoutube
required this.safeSearchYoutube,
required this.dhcpAvailable,
});
factory ServerStatus.fromJson(Map<String, dynamic> json) => ServerStatus(
@ -64,5 +66,6 @@ class ServerStatus {
safeSearchPixabay: json['safeSearch']['pixabay'],
safeSearchYandex: json['safeSearch']['yandex'],
safeSearchYoutube: json['safeSearch']['youtube'],
dhcpAvailable: json['status']['dhcp_available']
);
}

View file

@ -0,0 +1,23 @@
class StatisticsConfig {
final List<dynamic>? ignored;
final int? interval;
final bool? enabled;
StatisticsConfig({
this.ignored,
this.interval,
this.enabled,
});
factory StatisticsConfig.fromJson(Map<String, dynamic> json) => StatisticsConfig(
ignored: json["ignored"] == null ? [] : List<dynamic>.from(json["ignored"]!.map((x) => x)),
interval: json["interval"],
enabled: json["enabled"],
);
Map<String, dynamic> toJson() => {
"ignored": ignored == null ? [] : List<dynamic>.from(ignored!.map((x) => x)),
"interval": interval,
"enabled": enabled,
};
}

View file

@ -52,8 +52,6 @@ class AppConfigProvider with ChangeNotifier {
int _combinedChartHome = 0;
int _showTopItemsChart = 0;
String? _doNotRememberVersion;
GitHubRelease? _appUpdatesAvailable;
@ -170,10 +168,6 @@ class AppConfigProvider with ChangeNotifier {
return _hideServerAddress == 1 ? true : false;
}
bool get showTopItemsChart {
return _showTopItemsChart == 1 ? true : false;
}
void setDbInstance(Database db) {
_dbInstance = db;
}
@ -408,23 +402,6 @@ class AppConfigProvider with ChangeNotifier {
}
}
Future<bool> setShowTopItemsChart(bool value) async {
final updated = await updateConfigQuery(
db: _dbInstance!,
column: 'showTopItemsChart',
value: value == true ? 1 : 0
);
if (updated == true) {
_showTopItemsChart = value == true ? 1 : 0;
notifyListeners();
return true;
}
else {
return false;
}
}
Future<bool> setDoNotRememberVersion(String value) async {
final updated = await updateConfigQuery(
db: _dbInstance!,
@ -446,7 +423,6 @@ class AppConfigProvider with ChangeNotifier {
_showIpLogs = dbData['showIpLogs'] ?? 0;
_combinedChartHome = dbData['combinedChart'] ?? 0;
_hideServerAddress = dbData['hideServerAddress'];
_showTopItemsChart = dbData['showTopItemsChart'];
if (dbData['homeTopItemsOrder'] != null) {
try {
final itemsOrder = List<HomeTopItems>.from(

View file

@ -20,7 +20,7 @@ class LogsProvider with ChangeNotifier {
DateTime? _logsOlderThan;
String _selectedResultStatus = 'all';
String? _searchText;
List<String>? _selectedClients;
List<String> _selectedClients = [];
int _logsQuantity = 100;
int _offset = 0;
@ -65,7 +65,7 @@ class LogsProvider with ChangeNotifier {
return _offset;
}
List<String>? get selectedClients {
List<String> get selectedClients {
return _selectedClients;
}
@ -131,7 +131,7 @@ class LogsProvider with ChangeNotifier {
}
void setSelectedClients(List<String>? clients) {
_selectedClients = clients;
_selectedClients = clients ?? [];
notifyListeners();
}

View file

@ -0,0 +1,209 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/clients/client/blocking_schedule_modal.dart';
import 'package:adguard_home_manager/widgets/custom_list_tile.dart';
import 'package:adguard_home_manager/widgets/section_label.dart';
import 'package:adguard_home_manager/models/clients.dart';
class EditBlockingSchedule {
final String timezone;
final List<String> weekday;
final int start;
final int end;
const EditBlockingSchedule({
required this.timezone,
required this.weekday,
required this.start,
required this.end,
});
}
class BlockingSchedule extends StatelessWidget {
final BlockedServicesSchedule blockedServicesSchedule;
final void Function(BlockedServicesSchedule) setBlockedServicesSchedule;
const BlockingSchedule({
super.key,
required this.blockedServicesSchedule,
required this.setBlockedServicesSchedule,
});
@override
Widget build(BuildContext context) {
void updateSchedule(EditBlockingSchedule v) {
final scheduleJson = blockedServicesSchedule.toJson();
for (var weekday in v.weekday) {
scheduleJson[weekday] = {
"start": v.start,
"end": v.end
};
}
scheduleJson["time_zone"] = v.timezone;
setBlockedServicesSchedule(BlockedServicesSchedule.fromJson(scheduleJson));
}
void openAddScheduleModal() {
showDialog(
context: context,
builder: (context) => BlockingScheduleModal(
onConfirm: updateSchedule,
),
);
}
void openEditScheduleModal(String weekday) {
showDialog(
context: context,
builder: (context) => BlockingScheduleModal(
schedule: EditBlockingSchedule(
timezone: blockedServicesSchedule.timeZone!,
weekday: [weekday],
start: blockedServicesSchedule.toJson()[weekday]['start'],
end: blockedServicesSchedule.toJson()[weekday]['end'],
),
onConfirm: updateSchedule,
),
);
}
void onDeleteSchedule(String weekday) {
final scheduleJson = blockedServicesSchedule.toJson();
scheduleJson[weekday] = null;
setBlockedServicesSchedule(BlockedServicesSchedule.fromJson(scheduleJson));
}
String formatTime(int time) {
final formatted = Duration(milliseconds: time);
final hours = formatted.inHours;
final minutes = formatted.inMinutes - hours*60;
return "${hours.toString().padLeft(2 , '0')}:${minutes.toString().padLeft(2, '0')}";
}
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SectionLabel(label: AppLocalizations.of(context)!.pauseServiceBlocking),
Padding(
padding: const EdgeInsets.only(right: 12),
child: IconButton(
onPressed: openAddScheduleModal,
icon: const Icon(Icons.add)
),
)
],
),
const SizedBox(height: 2),
if (
blockedServicesSchedule.mon == null &&
blockedServicesSchedule.tue == null &&
blockedServicesSchedule.wed == null &&
blockedServicesSchedule.thu == null &&
blockedServicesSchedule.fri == null &&
blockedServicesSchedule.sat == null &&
blockedServicesSchedule.sun == null
) Padding(
padding: const EdgeInsets.all(16),
child: Text(
AppLocalizations.of(context)!.noBlockingScheduleThisDevice,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
color: Theme.of(context).colorScheme.onSurfaceVariant
),
),
),
if (blockedServicesSchedule.mon != null) _ScheduleTile(
weekday: AppLocalizations.of(context)!.monday,
schedule: "${formatTime(blockedServicesSchedule.mon!.start!)} - ${formatTime(blockedServicesSchedule.mon!.end!)}",
onEdit: () => openEditScheduleModal("mon"),
onDelete: () => onDeleteSchedule("mon")
),
if (blockedServicesSchedule.tue != null) _ScheduleTile(
weekday: AppLocalizations.of(context)!.tuesday,
schedule: "${formatTime(blockedServicesSchedule.tue!.start!)} - ${formatTime(blockedServicesSchedule.tue!.end!)}",
onEdit: () => openEditScheduleModal("tue"),
onDelete: () => onDeleteSchedule("tue")
),
if (blockedServicesSchedule.wed != null) _ScheduleTile(
weekday: AppLocalizations.of(context)!.wednesday,
schedule: "${formatTime(blockedServicesSchedule.wed!.start!)} - ${formatTime(blockedServicesSchedule.wed!.end!)}",
onEdit: () => openEditScheduleModal("wed"),
onDelete: () => onDeleteSchedule("wed")
),
if (blockedServicesSchedule.thu != null) _ScheduleTile(
weekday: AppLocalizations.of(context)!.thursday,
schedule: "${formatTime(blockedServicesSchedule.thu!.start!)} - ${formatTime(blockedServicesSchedule.thu!.end!)}",
onEdit: () => openEditScheduleModal("thu"),
onDelete: () => onDeleteSchedule("thu")
),
if (blockedServicesSchedule.fri != null) _ScheduleTile(
weekday: AppLocalizations.of(context)!.friday,
schedule: "${formatTime(blockedServicesSchedule.fri!.start!)} - ${formatTime(blockedServicesSchedule.fri!.end!)}",
onEdit: () => openEditScheduleModal("fri"),
onDelete: () => onDeleteSchedule("fri")
),
if (blockedServicesSchedule.sat != null) _ScheduleTile(
weekday: AppLocalizations.of(context)!.saturday,
schedule: "${formatTime(blockedServicesSchedule.sat!.start!)} - ${formatTime(blockedServicesSchedule.sat!.end!)}",
onEdit: () => openEditScheduleModal("sat"),
onDelete: () => onDeleteSchedule("sat")
),
if (blockedServicesSchedule.sun != null) _ScheduleTile(
weekday: AppLocalizations.of(context)!.sunday,
schedule: "${formatTime(blockedServicesSchedule.sun!.start!)} - ${formatTime(blockedServicesSchedule.sun!.end!)}",
onEdit: () => openEditScheduleModal("sun"),
onDelete: () => onDeleteSchedule("sun")
),
],
);
}
}
class _ScheduleTile extends StatelessWidget {
final String weekday;
final String schedule;
final void Function() onEdit;
final void Function() onDelete;
const _ScheduleTile({
required this.weekday,
required this.schedule,
required this.onEdit,
required this.onDelete,
});
@override
Widget build(BuildContext context) {
return CustomListTile(
title: weekday,
subtitle: schedule,
trailing: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
onPressed: onEdit,
icon: const Icon(Icons.edit_rounded),
tooltip: AppLocalizations.of(context)!.edit,
),
const SizedBox(width: 4),
IconButton(
onPressed: onDelete,
icon: const Icon(Icons.delete_rounded),
tooltip: AppLocalizations.of(context)!.delete,
),
],
),
padding: const EdgeInsets.only(
left: 16,
top: 6,
right: 12,
bottom: 6
)
);
}
}

View file

@ -0,0 +1,325 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:timezone/timezone.dart' as tz;
import 'package:timezone/data/latest.dart' as tz;
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/clients/client/blocking_schedule.dart';
class BlockingScheduleModal extends StatefulWidget {
final EditBlockingSchedule? schedule;
final void Function(EditBlockingSchedule) onConfirm;
const BlockingScheduleModal({
super.key,
this.schedule,
required this.onConfirm,
});
@override
State<BlockingScheduleModal> createState() => _BlockingScheduleModalState();
}
class _BlockingScheduleModalState extends State<BlockingScheduleModal> {
final _weekdaysScrollController = ScrollController();
String? _timezone;
List<String> _weekdays = [];
TimeOfDay? _from;
TimeOfDay? _to;
bool _compareTimes(TimeOfDay startTime, TimeOfDay endTime) {
bool result = false;
int startTimeInt = (startTime.hour * 60 + startTime.minute) * 60;
int endTimeInt = (endTime.hour * 60 + endTime.minute) * 60;
if (endTimeInt > startTimeInt) {
result = true;
} else {
result = false;
}
return result;
}
bool _validate() {
return _timezone != null &&
_weekdays.isNotEmpty &&
_from != null &&
_to != null &&
_compareTimes(_from!, _to!);
}
int _timeOfDayToInt(TimeOfDay timeOfDay) {
return Duration(
days: 0,
hours: timeOfDay.hour,
minutes: timeOfDay.minute,
seconds: 0
).inMilliseconds;
}
TimeOfDay _intToTimeOfDay(int value) {
final duration = Duration(milliseconds: value);
final minutes = duration.inMinutes - duration.inHours*60;
return TimeOfDay(hour: duration.inHours, minute: minutes);
}
@override
void initState() {
tz.initializeTimeZones();
if (widget.schedule != null) {
_timezone = widget.schedule!.timezone;
_weekdays = widget.schedule!.weekday;
_from = _intToTimeOfDay(widget.schedule!.start);
_to = _intToTimeOfDay(widget.schedule!.end);
}
super.initState();
}
@override
Widget build(BuildContext context) {
void onSelectWeekday(bool newStatus, String day) {
if (newStatus == true && !_weekdays.contains(day)) {
setState(() => _weekdays.add(day));
}
else if (newStatus == false) {
setState(() => _weekdays = _weekdays.where((e) => e != day).toList());
}
}
void onConfirm() {
widget.onConfirm(
EditBlockingSchedule(
timezone: _timezone!,
weekday: _weekdays,
start: _timeOfDayToInt(_from!),
end: _timeOfDayToInt(_to!)
)
);
Navigator.pop(context);
}
final valid = _validate();
final validTimes = _from != null && _to != null
? _compareTimes(_from!, _to!)
: null;
return Dialog(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 500),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.schedule_rounded,
size: 24,
color: Theme.of(context).listTileTheme.iconColor
),
const SizedBox(height: 16),
Text(
widget.schedule != null
? AppLocalizations.of(context)!.editSchedule
: AppLocalizations.of(context)!.newSchedule,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 24,
),
),
const SizedBox(height: 30),
LayoutBuilder(
builder: (context, constraints) => DropdownButtonFormField(
items: tz.timeZoneDatabase.locations.keys.map((item) => DropdownMenuItem(
value: item,
child: SizedBox(
width: constraints.maxWidth-48,
child: Text(
item,
overflow: TextOverflow.ellipsis,
),
),
)).toList(),
value: _timezone,
onChanged: (v) => setState(() => _timezone = v),
decoration: InputDecoration(
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10)
)
),
label: Text(AppLocalizations.of(context)!.timezone)
),
borderRadius: BorderRadius.circular(20),
),
),
const SizedBox(height: 16),
SizedBox(
height: Platform.isMacOS || Platform.isLinux || Platform.isWindows ? 66 : 50,
child: Scrollbar(
controller: _weekdaysScrollController,
thumbVisibility: Platform.isMacOS || Platform.isLinux || Platform.isWindows,
interactive: Platform.isMacOS || Platform.isLinux || Platform.isWindows,
thickness: Platform.isMacOS || Platform.isLinux || Platform.isWindows ? 8 : 0,
child: Padding(
padding: EdgeInsets.only(
bottom: Platform.isMacOS || Platform.isLinux || Platform.isWindows ? 16 : 0
),
child: ListView(
controller: _weekdaysScrollController,
scrollDirection: Axis.horizontal,
children: [
FilterChip(
label: Text(AppLocalizations.of(context)!.monday),
selected: _weekdays.contains("mon"),
onSelected: (value) => onSelectWeekday(value, "mon")
),
const SizedBox(width: 8),
FilterChip(
label: Text(AppLocalizations.of(context)!.tuesday),
selected: _weekdays.contains("tue"),
onSelected: (value) => onSelectWeekday(value, "tue")
),
const SizedBox(width: 8),
FilterChip(
label: Text(AppLocalizations.of(context)!.wednesday),
selected: _weekdays.contains("wed"),
onSelected: (value) => onSelectWeekday(value, "wed")
),
const SizedBox(width: 8),
FilterChip(
label: Text(AppLocalizations.of(context)!.thursday),
selected: _weekdays.contains("thu"),
onSelected: (value) => onSelectWeekday(value, "thu")
),
const SizedBox(width: 8),
FilterChip(
label: Text(AppLocalizations.of(context)!.friday),
selected: _weekdays.contains("fri"),
onSelected: (value) => onSelectWeekday(value, "fri")
),
const SizedBox(width: 8),
FilterChip(
label: Text(AppLocalizations.of(context)!.saturday),
selected: _weekdays.contains("sat"),
onSelected: (value) => onSelectWeekday(value, "sat")
),
const SizedBox(width: 8),
FilterChip(
label: Text(AppLocalizations.of(context)!.sunday),
selected: _weekdays.contains("sun"),
onSelected: (value) => onSelectWeekday(value, "sun")
),
],
),
),
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: () async {
final selected = await showTimePicker(
context: context,
initialTime: _from ?? const TimeOfDay(hour: 0, minute: 0),
helpText: AppLocalizations.of(context)!.selectStartTime,
confirmText: AppLocalizations.of(context)!.confirm,
);
setState(() => _from = selected);
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Column(
children: [
Text(AppLocalizations.of(context)!.from),
const SizedBox(height: 2),
Text(_from != null ? "${_from!.hour.toString().padLeft(2, '0')}:${_from!.minute.toString().padLeft(2, '0')}" : "--:--")
],
),
)
),
ElevatedButton(
onPressed: () async {
final selected = await showTimePicker(
context: context,
initialTime: _to ?? const TimeOfDay(hour: 23, minute: 59),
helpText: AppLocalizations.of(context)!.selectEndTime,
confirmText: AppLocalizations.of(context)!.confirm
);
setState(() => _to = selected);
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Column(
children: [
Text(AppLocalizations.of(context)!.to),
const SizedBox(height: 2),
Text(_to != null ? "${_to!.hour.toString().padLeft(2, '0')}:${_to!.minute.toString().padLeft(2, '0')}" : "--:--")
],
),
)
),
],
),
if (validTimes == false) Padding(
padding: const EdgeInsets.only(top: 16),
child: Card(
color: const Color.fromARGB(255, 255, 182, 175),
elevation: 0,
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Icon(
Icons.error_rounded,
color: Theme.of(context).colorScheme.onSurfaceVariant
),
const SizedBox(width: 16),
Expanded(
child: Text(
AppLocalizations.of(context)!.startTimeBeforeEndTime,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface
),
),
),
],
),
),
)
)
],
),
),
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(AppLocalizations.of(context)!.close)
),
const SizedBox(width: 8),
TextButton(
onPressed: valid ? () => onConfirm() : null,
child: Text(AppLocalizations.of(context)!.confirm)
),
],
)
],
),
),
),
);
}
}

View file

@ -8,6 +8,7 @@ import 'package:adguard_home_manager/screens/clients/client/identifiers_section.
import 'package:adguard_home_manager/screens/clients/client/settings_tile.dart';
import 'package:adguard_home_manager/screens/clients/client/tags_section.dart';
import 'package:adguard_home_manager/screens/clients/client/upstream_servers_section.dart';
import 'package:adguard_home_manager/screens/clients/client/blocking_schedule.dart';
import 'package:adguard_home_manager/widgets/custom_switch_list_tile.dart';
import 'package:adguard_home_manager/widgets/custom_list_tile.dart';
@ -52,6 +53,8 @@ class ClientForm extends StatelessWidget {
final TextEditingController dnsCacheField;
final String? dnsCacheError;
final void Function(String?) updateDnsCacheError;
final BlockedServicesSchedule blockedServicesSchedule;
final void Function(BlockedServicesSchedule) setBlockedServicesSchedule;
const ClientForm({
super.key,
@ -90,6 +93,8 @@ class ClientForm extends StatelessWidget {
required this.dnsCacheField,
required this.dnsCacheError,
required this.updateDnsCacheError,
required this.blockedServicesSchedule,
required this.setBlockedServicesSchedule,
});
@override
@ -282,6 +287,11 @@ class ClientForm extends StatelessWidget {
keyboardType: TextInputType.number,
),
),
BlockingSchedule(
blockedServicesSchedule: blockedServicesSchedule,
setBlockedServicesSchedule: setBlockedServicesSchedule,
),
const SizedBox(height: 16),
],
);
}

View file

@ -81,6 +81,8 @@ class _ClientScreenState extends State<ClientScreen> {
bool _enableDnsCache = false;
final _dnsCacheField = TextEditingController();
String? _dnsCacheError;
BlockedServicesSchedule _blockedServicesSchedule = BlockedServicesSchedule();
// VALIDATIONS
bool _nameValid = true;
@ -140,6 +142,9 @@ class _ClientScreenState extends State<ClientScreen> {
_dnsCacheField.text = widget.client!.upstreamsCacheSize != null
? widget.client!.upstreamsCacheSize.toString()
: "";
if (widget.client!.blockedServicesSchedule != null) {
_blockedServicesSchedule = widget.client!.blockedServicesSchedule!;
}
}
super.initState();
}
@ -166,7 +171,8 @@ class _ClientScreenState extends State<ClientScreen> {
upstreamsCacheEnabled: _enableDnsCache,
upstreamsCacheSize: _dnsCacheField.text != ""
? int.parse(_dnsCacheField.text)
: null
: null,
blockedServicesSchedule: _blockedServicesSchedule
);
widget.onConfirm(client);
}
@ -268,7 +274,9 @@ class _ClientScreenState extends State<ClientScreen> {
updateEnableDnsCache: (v) => setState(() => _enableDnsCache = v),
dnsCacheField: _dnsCacheField,
dnsCacheError: _dnsCacheError,
updateDnsCacheError: (v) => setState(() => _dnsCacheError = v)
updateDnsCacheError: (v) => setState(() => _dnsCacheError = v),
blockedServicesSchedule: _blockedServicesSchedule,
setBlockedServicesSchedule: (v) => setState(() => _blockedServicesSchedule = v),
),
],
),
@ -353,7 +361,9 @@ class _ClientScreenState extends State<ClientScreen> {
updateEnableDnsCache: (v) => setState(() => _enableDnsCache = v),
dnsCacheField: _dnsCacheField,
dnsCacheError: _dnsCacheError,
updateDnsCacheError: (v) => setState(() => _dnsCacheError = v)
updateDnsCacheError: (v) => setState(() => _dnsCacheError = v),
blockedServicesSchedule: _blockedServicesSchedule,
setBlockedServicesSchedule: (v) => setState(() => _blockedServicesSchedule = v),
),
],
),

View file

@ -39,6 +39,7 @@ class AddFiltersButton extends StatelessWidget {
processModal.close();
if (!context.mounted) return;
if (result == true) {
showSnacbkar(
appConfigProvider: appConfigProvider,

View file

@ -64,8 +64,6 @@ class _ListDetailsScreenState extends State<ListDetailsScreen> {
final filteringProvider = Provider.of<FilteringProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
Filter? list;
try {
list = filteringProvider.filtering != null
@ -111,177 +109,6 @@ class _ListDetailsScreenState extends State<ListDetailsScreen> {
}
}
List<Widget> content() {
return [
CustomListTile(
icon: Icons.shield_rounded,
title: AppLocalizations.of(context)!.currentStatus,
subtitleWidget: Text(
list!.enabled == true
? AppLocalizations.of(context)!.enabled
: AppLocalizations.of(context)!.disabled,
style: TextStyle(
color: list.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: list.name,
padding: widget.dialog == true
? const EdgeInsets.symmetric(
horizontal: 24,
vertical: 8
)
: null,
),
CustomListTile(
icon: Icons.link_rounded,
title: "URL",
subtitle: list.url,
padding: widget.dialog == true
? const EdgeInsets.symmetric(
horizontal: 24,
vertical: 8
)
: null,
trailing: IconButton(
onPressed: () => openUrl(list!.url),
icon: const Icon(Icons.open_in_browser_rounded),
tooltip: AppLocalizations.of(context)!.openListUrl,
),
),
CustomListTile(
icon: Icons.list_rounded,
title: AppLocalizations.of(context)!.rules,
subtitle: 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 (list.lastUpdated != null) CustomListTile(
icon: Icons.schedule_rounded,
title: AppLocalizations.of(context)!.latestUpdate,
subtitle: convertTimestampLocalTimezone(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: list,
type: widget.type,
onEdit: ({required Filter list, required String type}) async => updateList(
action: FilteringListActions.edit,
filterList: list
),
dialog: true,
),
)
}
else {
showModalBottomSheet(
context: context,
useRootNavigator: true,
builder: (ctx) => AddListModal(
list: list,
type: widget.type,
onEdit: ({required Filter list, required String type}) async => updateList(
action: FilteringListActions.edit,
filterList: list
),
dialog: false,
),
isScrollControlled: true,
backgroundColor: Colors.transparent
)
}
},
icon: const Icon(Icons.edit),
tooltip: AppLocalizations.of(context)!.edit,
),
IconButton(
onPressed: () {
showDialog(
context: context,
builder: (c) => DeleteListModal(
onConfirm: () async {
ProcessModal processModal = ProcessModal();
processModal.open(AppLocalizations.of(context)!.deletingList);
final result = await filteringProvider.deleteList(
listUrl: list!.url,
type: widget.type,
);
processModal.close();
if (result == true) {
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.listDeleted,
color: Colors.green
);
Navigator.pop(context);
}
else {
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.listNotDeleted,
color: Colors.red
);
}
}
)
);
},
icon: const Icon(Icons.delete),
tooltip: AppLocalizations.of(context)!.delete,
),
const SizedBox(width: 10),
];
}
if (widget.dialog == true) {
return Dialog(
child: ConstrainedBox(
@ -330,7 +157,11 @@ class _ListDetailsScreenState extends State<ListDetailsScreen> {
? AppLocalizations.of(context)!.disableList
: AppLocalizations.of(context)!.enableList,
),
...actions()
_Actions(
list: list,
type: widget.type,
updateList: (action, filterList) => updateList(action: action, filterList: filterList),
)
],
)
],
@ -340,7 +171,12 @@ class _ListDetailsScreenState extends State<ListDetailsScreen> {
child: list != null
? SingleChildScrollView(
child: Wrap(
children: content(),
children: [
_Content(
isDialog: widget.dialog,
list: list,
)
],
),
)
: Center(
@ -361,17 +197,27 @@ class _ListDetailsScreenState extends State<ListDetailsScreen> {
return Dialog.fullscreen(
child: Scaffold(
appBar: AppBar(
leading: CloseButton(
onPressed: () => Navigator.pop(context),
),
title: Text(AppLocalizations.of(context)!.listDetails),
actions: list != null ? actions() : null,
actions: list != null
? [
_Actions(
list: list,
type: widget.type,
updateList: (action, filterList) => updateList(action: action, filterList: filterList),
)
]
: null,
),
body: SafeArea(
child: Stack(
children: [
if (list != null) ListView(
children: content(),
children: [
_Content(
isDialog: widget.dialog,
list: list,
)
],
),
if (list == null) Center(
child: Text(
@ -410,4 +256,204 @@ class _ListDetailsScreenState extends State<ListDetailsScreen> {
);
}
}
}
class _Content extends StatelessWidget {
final Filter list;
final bool isDialog;
const _Content({
required this.list,
required this.isDialog
});
@override
Widget build(BuildContext context) {
final appConfigProvider = Provider.of<AppConfigProvider>(context);
return Column(
children: [
CustomListTile(
icon: Icons.shield_rounded,
title: AppLocalizations.of(context)!.currentStatus,
subtitleWidget: Text(
list.enabled == true
? AppLocalizations.of(context)!.enabled
: AppLocalizations.of(context)!.disabled,
style: TextStyle(
color: list.enabled == true
? appConfigProvider.useThemeColorForStatus == true
? Theme.of(context).colorScheme.primary
: Colors.green
: appConfigProvider.useThemeColorForStatus == true
? Colors.grey
: Colors.red,
fontWeight: FontWeight.w500
),
),
padding: isDialog == true
? const EdgeInsets.symmetric(
horizontal: 24,
vertical: 8
)
: null,
),
CustomListTile(
icon: Icons.badge_rounded,
title: AppLocalizations.of(context)!.name,
subtitle: list.name,
padding: isDialog == true
? const EdgeInsets.symmetric(
horizontal: 24,
vertical: 8
)
: null,
),
CustomListTile(
icon: Icons.link_rounded,
title: "URL",
subtitle: list.url,
padding: isDialog == true
? const EdgeInsets.symmetric(
horizontal: 24,
vertical: 8
)
: null,
trailing: IconButton(
onPressed: () => openUrl(list.url),
icon: const Icon(Icons.open_in_browser_rounded),
tooltip: AppLocalizations.of(context)!.openListUrl,
),
),
CustomListTile(
icon: Icons.list_rounded,
title: AppLocalizations.of(context)!.rules,
subtitle: list.rulesCount.toString(),
padding: isDialog == true
? const EdgeInsets.symmetric(
horizontal: 24,
vertical: 8
)
: null,
),
CustomListTile(
icon: Icons.shield_rounded,
title: AppLocalizations.of(context)!.listType,
subtitle: isDialog == 'whitelist'
? AppLocalizations.of(context)!.whitelist
: AppLocalizations.of(context)!.blacklist,
padding: isDialog == true
? const EdgeInsets.symmetric(
horizontal: 24,
vertical: 8
)
: null,
),
if (list.lastUpdated != null) CustomListTile(
icon: Icons.schedule_rounded,
title: AppLocalizations.of(context)!.latestUpdate,
subtitle: convertTimestampLocalTimezone(list.lastUpdated!, 'dd-MM-yyyy HH:mm'),
padding: isDialog == true
? const EdgeInsets.symmetric(
horizontal: 24,
vertical: 8
)
: null,
),
if (isDialog == true) Container(height: 16)
],
);
}
}
class _Actions extends StatelessWidget {
final Filter? list;
final String type;
final void Function(FilteringListActions, Filter) updateList;
const _Actions({
required this.list,
required this.type,
required this.updateList,
});
@override
Widget build(BuildContext context) {
final filteringProvider = Provider.of<FilteringProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
return Row(
children: [
IconButton(
onPressed: () => {
if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) {
showDialog(
context: context,
builder: (ctx) => AddListModal(
list: list,
type: type,
onEdit: ({required Filter list, required String type}) async => updateList(FilteringListActions.edit, list),
dialog: true,
),
)
}
else {
showModalBottomSheet(
context: context,
useRootNavigator: true,
builder: (ctx) => AddListModal(
list: list,
type: type,
onEdit: ({required Filter list, required String type}) async => updateList(FilteringListActions.edit, list),
dialog: false,
),
isScrollControlled: true,
backgroundColor: Colors.transparent
)
}
},
icon: const Icon(Icons.edit),
tooltip: AppLocalizations.of(context)!.edit,
),
IconButton(
onPressed: () {
showDialog(
context: context,
builder: (c) => DeleteListModal(
onConfirm: () async {
ProcessModal processModal = ProcessModal();
processModal.open(AppLocalizations.of(context)!.deletingList);
final result = await filteringProvider.deleteList(
listUrl: list!.url,
type: type,
);
processModal.close();
if (result == true) {
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.listDeleted,
color: Colors.green
);
Navigator.pop(context);
}
else {
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.listNotDeleted,
color: Colors.red
);
}
}
)
);
},
icon: const Icon(Icons.delete),
tooltip: AppLocalizations.of(context)!.delete,
),
const SizedBox(width: 10),
],
);
}
}

View file

@ -188,31 +188,25 @@ class _FiltersState extends State<Filters> {
}
void openListDetails(Filter filter, String type) {
showGeneralDialog(
context: context,
barrierColor: !(width > 900 || !(Platform.isAndroid | Platform.isIOS))
?Colors.transparent
: Colors.black54,
transitionBuilder: (context, anim1, anim2, child) {
return SlideTransition(
position: Tween(
begin: const Offset(0, 1),
end: const Offset(0, 0)
).animate(
CurvedAnimation(
parent: anim1,
curve: Curves.easeInOutCubicEmphasized
)
),
child: child,
);
},
pageBuilder: (context, animation, secondaryAnimation) => ListDetailsScreen(
listId: filter.id,
type: type,
dialog: width > 900 || !(Platform.isAndroid | Platform.isIOS),
),
);
if (width > 900) {
showDialog(
context: context,
builder: (context) => ListDetailsScreen(
listId: filter.id,
type: type,
dialog: width > 900 || !(Platform.isAndroid | Platform.isIOS),
),
);
}
else {
Navigator.push(context, MaterialPageRoute(
builder: (context) => ListDetailsScreen(
listId: filter.id,
type: type,
dialog: width > 900 || !(Platform.isAndroid | Platform.isIOS),
),
));
}
}
List<Widget> actions() {

View file

@ -151,7 +151,8 @@ class HomeAppBar extends StatelessWidget {
),
)
]
)
),
const SizedBox(width: 8),
],
);
}

View file

@ -227,6 +227,8 @@ class _HomeState extends State<Home> {
),
TopItemsLists(order: appConfigProvider.homeTopItemsOrder),
const SizedBox(height: 16),
],
),
if (statusProvider.loadStatus == LoadStatus.error) SliverFillRemaining(

View file

@ -8,7 +8,7 @@ import 'package:adguard_home_manager/models/menu_option.dart';
import 'package:adguard_home_manager/constants/enums.dart';
import 'package:adguard_home_manager/providers/status_provider.dart';
class RowItem extends StatefulWidget {
class RowItem extends StatelessWidget {
final HomeTopItems type;
final Color chartColor;
final String domain;
@ -32,61 +32,14 @@ class RowItem extends StatefulWidget {
this.unit,
});
@override
State<RowItem> createState() => _RowItemState();
}
class _RowItemState extends State<RowItem> with TickerProviderStateMixin {
late AnimationController expandController;
late Animation<double> animation;
@override
void initState() {
super.initState();
prepareAnimations();
_runExpandCheck();
}
void prepareAnimations() {
expandController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 250)
);
animation = CurvedAnimation(
parent: expandController,
curve: Curves.ease,
);
}
void _runExpandCheck() {
if (widget.showColor) {
expandController.forward();
}
else {
expandController.reverse();
}
}
@override
void didUpdateWidget(oldWidget) {
super.didUpdateWidget(oldWidget);
_runExpandCheck();
}
@override
void dispose() {
expandController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final statusProvider = Provider.of<StatusProvider>(context);
String? name;
if (widget.clients == true) {
if (clients == true) {
try {
name = statusProvider.serverStatus!.clients.firstWhere((c) => c.ids.contains(widget.domain)).name;
name = statusProvider.serverStatus!.clients.firstWhere((c) => c.ids.contains(domain)).name;
} catch (e) {
// ---- //
}
@ -95,9 +48,9 @@ class _RowItemState extends State<RowItem> with TickerProviderStateMixin {
return Material(
color: Colors.transparent,
child: OptionsMenu(
value: widget.domain,
options: widget.options,
onTap: widget.onTapEntry,
value: domain,
options: options,
onTap: onTapEntry,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
@ -109,18 +62,13 @@ class _RowItemState extends State<RowItem> with TickerProviderStateMixin {
Flexible(
child: Row(
children: [
SizeTransition(
axisAlignment: 1.0,
sizeFactor: animation,
axis: Axis.horizontal,
child: Container(
margin: const EdgeInsets.only(right: 16),
width: 12,
height: 12,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30),
color: widget.chartColor
),
if (showColor == true) Container(
margin: const EdgeInsets.only(right: 16),
width: 12,
height: 12,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30),
color: chartColor
),
),
Expanded(
@ -128,7 +76,7 @@ class _RowItemState extends State<RowItem> with TickerProviderStateMixin {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.domain,
domain,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 16,
@ -154,7 +102,7 @@ class _RowItemState extends State<RowItem> with TickerProviderStateMixin {
),
const SizedBox(width: 16),
Text(
widget.number,
number,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface
),
@ -167,7 +115,7 @@ class _RowItemState extends State<RowItem> with TickerProviderStateMixin {
}
}
class OthersRowItem extends StatefulWidget {
class OthersRowItem extends StatelessWidget {
final List<Map<String, dynamic>> items;
final bool showColor;
@ -177,111 +125,60 @@ class OthersRowItem extends StatefulWidget {
required this.showColor,
});
@override
State<OthersRowItem> createState() => _OthersRowItemState();
}
class _OthersRowItemState extends State<OthersRowItem> with SingleTickerProviderStateMixin {
late AnimationController expandController;
late Animation<double> animation;
@override
void initState() {
super.initState();
prepareAnimations();
_runExpandCheck();
}
void prepareAnimations() {
expandController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 250)
);
animation = CurvedAnimation(
parent: expandController,
curve: Curves.ease,
);
}
void _runExpandCheck() {
if (widget.showColor) {
expandController.forward();
}
else {
expandController.reverse();
}
}
@override
void didUpdateWidget(oldWidget) {
super.didUpdateWidget(oldWidget);
_runExpandCheck();
}
@override
void dispose() {
expandController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (widget.items.length <= 5) {
if (items.length <= 5) {
return const SizedBox();
}
return SizeTransition(
axisAlignment: 1.0,
sizeFactor: animation,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 8
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Row(
children: [
Container(
margin: const EdgeInsets.only(right: 16),
width: 12,
height: 12,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30),
color: Colors.grey
),
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 8
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Row(
children: [
if (showColor == true) Container(
margin: const EdgeInsets.only(right: 16),
width: 12,
height: 12,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30),
color: Colors.grey
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(context)!.others,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurface
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(context)!.others,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurface
),
],
),
),
],
),
],
),
),
],
),
const SizedBox(width: 16),
Text(
List<int>.from(
widget.items.sublist(5, widget.items.length).map((e) => e.values.first.toInt())
).reduce((a, b) => a + b).toString(),
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface
),
)
],
),
),
const SizedBox(width: 16),
Text(
List<int>.from(
items.sublist(5, items.length).map((e) => e.values.first.toInt())
).reduce((a, b) => a + b).toString(),
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface
),
)
],
),
);
}

View file

@ -3,18 +3,17 @@
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/home/top_items/row_item.dart';
import 'package:adguard_home_manager/screens/home/top_items/top_items_screen.dart';
import 'package:adguard_home_manager/widgets/custom_pie_chart.dart';
import 'package:adguard_home_manager/functions/number_format.dart';
import 'package:adguard_home_manager/models/menu_option.dart';
import 'package:adguard_home_manager/constants/enums.dart';
import 'package:adguard_home_manager/providers/app_config_provider.dart';
class TopItemsSection extends StatefulWidget {
class TopItemsSection extends StatelessWidget {
final HomeTopItems type;
final String label;
final List<Map<String, dynamic>> data;
@ -36,45 +35,32 @@ class TopItemsSection extends StatefulWidget {
this.onTapEntry,
});
@override
State<TopItemsSection> createState() => _TopItemsState();
}
class _TopItemsState extends State<TopItemsSection> {
bool _showChart = true;
final colors = [
Colors.red,
Colors.green,
Colors.blue,
Colors.orange,
Colors.teal,
Colors.grey
];
@override
void initState() {
_showChart = Provider.of<AppConfigProvider>(context, listen: false).showTopItemsChart;
super.initState();
}
@override
Widget build(BuildContext context) {
final colors = [
Colors.red,
Colors.green,
Colors.blue,
Colors.orange,
Colors.teal,
Colors.grey
];
final width = MediaQuery.of(context).size.width;
final withChart = widget.type != HomeTopItems.avgUpstreamResponseTime;
final withChart = type != HomeTopItems.avgUpstreamResponseTime;
Map<String, double> chartData() {
Map<String, double> ringData() {
Map<String, double> values = {};
widget.data.sublist(0, widget.data.length > 5 ? 5 : widget.data.length).forEach((element) {
data.sublist(0, data.length > 5 ? 5 : data.length).forEach((element) {
values = {
...values,
element.keys.first: element.values.first.toDouble()
};
});
if (widget.data.length > 5) {
if (data.length > 5) {
final int rest = List<int>.from(
widget.data.sublist(5, widget.data.length).map((e) => e.values.first.toInt())
data.sublist(5, data.length).map((e) => e.values.first.toInt())
).reduce((a, b) => a + b);
values = {
...values,
@ -84,25 +70,37 @@ class _TopItemsState extends State<TopItemsSection> {
return values;
}
final Widget noItems = Padding(
padding: const EdgeInsets.only(
bottom: 20,
top: 10
),
child: Text(
AppLocalizations.of(context)!.noItems,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
);
List<Map<String, dynamic>> lineData() {
List<Map<String, dynamic>> values = [];
data.sublist(0, data.length > 5 ? 5 : data.length).forEach((element) {
values.add({
"label": element.keys.first,
"value": element.values.first.toDouble()
});
});
if (data.length > 5) {
final int rest = List<int>.from(
data.sublist(5, data.length).map((e) => e.values.first.toInt())
).reduce((a, b) => a + b);
values.add({
"label": AppLocalizations.of(context)!.others,
"value": rest.toDouble()
});
}
return values;
}
final List<Map<String, dynamic>> lineChartData = lineData();
final mapData = lineChartData.map((e) => e["value"]);
final double total = mapData.isNotEmpty
? mapData.reduce((a, b) => a + b)
: 0;
return SizedBox(
child: Column(
children: [
if (widget.data.isEmpty) noItems,
if (widget.data.isNotEmpty && width > 700) Padding(
if (data.isEmpty) _NoData(label: label),
if (data.isNotEmpty && width > 700) Padding(
padding: EdgeInsets.only(bottom: withChart == false ? 16 : 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
@ -116,7 +114,7 @@ class _TopItemsState extends State<TopItemsSection> {
child: Padding(
padding: const EdgeInsets.all(16),
child: CustomPieChart(
data: chartData(),
data: ringData(),
colors: colors
)
),
@ -132,7 +130,7 @@ class _TopItemsState extends State<TopItemsSection> {
bottom: 16
),
child: Text(
widget.label,
label,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500
@ -141,16 +139,16 @@ class _TopItemsState extends State<TopItemsSection> {
),
_ItemsList(
colors: colors,
data: widget.data,
clients: widget.type == HomeTopItems.recurrentClients,
type: widget.type,
showChart: withChart == true ? _showChart : false,
buildValue: widget.buildValue,
menuOptions: widget.menuOptions,
onTapEntry: widget.onTapEntry,
data: data,
clients: type == HomeTopItems.recurrentClients,
type: type,
showChart: withChart,
buildValue: buildValue,
menuOptions: menuOptions,
onTapEntry: onTapEntry,
),
if (withChart == true) OthersRowItem(
items: widget.data,
items: data,
showColor: true,
)
]
@ -159,128 +157,59 @@ class _TopItemsState extends State<TopItemsSection> {
],
),
),
if (widget.data.isNotEmpty && width <= 700) Builder(
builder: (context) {
if (widget.withChart == true) {
return Column(
children: [
ExpansionPanelList(
expandedHeaderPadding: const EdgeInsets.all(0),
elevation: 0,
expansionCallback: (_, isExpanded) => setState(() => _showChart = isExpanded),
animationDuration: const Duration(milliseconds: 250),
children: [
ExpansionPanel(
headerBuilder: (context, isExpanded) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
mainAxisAlignment: width <= 700
? MainAxisAlignment.spaceBetween
: MainAxisAlignment.center,
children: [
Flexible(
child: Text(
widget.label,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurface
),
),
),
],
),
if (data.isNotEmpty && width <= 700) ...[
Text(
label,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurface
),
),
const SizedBox(height: 8),
if (withChart == true) Padding(
padding: const EdgeInsets.all(16),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: SizedBox(
height: 20,
child: LayoutBuilder(
builder: (context, constraints) => Row(
children: lineChartData.asMap().entries.map((e) => Tooltip(
message:'${e.value["label"]} (${doubleFormat((e.value["value"]/total)*100, Platform.localeName)}%)',
child: Container(
width: constraints.maxWidth*(e.value["value"]/total),
decoration: BoxDecoration(
color: colors[e.key]
),
body: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
children: [
SizedBox(
height: 150,
child: CustomPieChart(
data: chartData(),
colors: colors
)
),
const SizedBox(height: 16),
],
),
),
isExpanded: _showChart
),
],
),
Padding(
padding: const EdgeInsets.only(top: 8),
child: _ItemsList(
colors: colors,
data: widget.data,
clients: widget.type == HomeTopItems.recurrentClients,
type: widget.type,
showChart: _showChart,
buildValue: widget.buildValue,
menuOptions: widget.menuOptions,
onTapEntry: widget.onTapEntry,
),
),
if (widget.withChart == true) OthersRowItem(
items: widget.data,
showColor: _showChart,
),
const SizedBox(height: 16),
],
);
}
else {
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 18),
child: Row(
mainAxisAlignment: width <= 700
? MainAxisAlignment.spaceBetween
: MainAxisAlignment.center,
children: [
Flexible(
child: Text(
widget.label,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurface
),
),
),
],
),
),
Padding(
padding: const EdgeInsets.only(top: 16),
child: _ItemsList(
colors: colors,
data: widget.data,
clients: widget.type == HomeTopItems.recurrentClients,
type: widget.type,
showChart: false,
buildValue: widget.buildValue,
menuOptions: widget.menuOptions,
onTapEntry: widget.onTapEntry,
),
),
if (widget.withChart == true) OthersRowItem(
items: widget.data,
showColor: false,
),
const SizedBox(height: 16),
],
);
}
},
),
)).toList()
)
)
),
),
),
Padding(
padding: const EdgeInsets.only(top: 8),
child: _ItemsList(
colors: colors,
data: data,
clients: type == HomeTopItems.recurrentClients,
type: type,
showChart: withChart,
buildValue: buildValue,
menuOptions: menuOptions,
onTapEntry: onTapEntry,
),
),
OthersRowItem(
items: data,
showColor: withChart,
),
],
if (widget.data.length > 5) ...[
if (data.length > 5) ...[
Padding(
padding: const EdgeInsets.only(right: 20),
child: Row(
@ -288,37 +217,37 @@ class _TopItemsState extends State<TopItemsSection> {
children: [
TextButton(
onPressed: () => {
showGeneralDialog(
context: context,
barrierColor: !(width > 700 || !(Platform.isAndroid | Platform.isIOS))
?Colors.transparent
: Colors.black54,
transitionBuilder: (context, anim1, anim2, child) {
return SlideTransition(
position: Tween(
begin: const Offset(0, 1),
end: const Offset(0, 0)
).animate(
CurvedAnimation(
parent: anim1,
curve: Curves.easeInOutCubicEmphasized
)
),
child: child,
);
},
pageBuilder: (context, animation, secondaryAnimation) => TopItemsScreen(
type: widget.type,
title: widget.label,
isClient: widget.type == HomeTopItems.recurrentClients,
data: widget.data,
withProgressBar: widget.withProgressBar,
buildValue: widget.buildValue,
options: widget.menuOptions,
onTapEntry: widget.onTapEntry,
isFullscreen: !(width > 700 || !(Platform.isAndroid | Platform.isIOS)),
if (width > 700) {
showDialog(
context: context,
builder: (context) => TopItemsScreen(
type: type,
title: label,
isClient: type == HomeTopItems.recurrentClients,
data: data,
withProgressBar: withProgressBar,
buildValue: buildValue,
options: menuOptions,
onTapEntry: onTapEntry,
isFullscreen: !(width > 700 || !(Platform.isAndroid | Platform.isIOS)),
),
)
)
}
else {
Navigator.push(context, MaterialPageRoute(
builder: (context) => TopItemsScreen(
type: type,
title: label,
isClient: type == HomeTopItems.recurrentClients,
data: data,
withProgressBar: withProgressBar,
buildValue: buildValue,
options: menuOptions,
onTapEntry: onTapEntry,
isFullscreen: !(width > 700 || !(Platform.isAndroid | Platform.isIOS)),
),
))
}
},
child: Row(
mainAxisSize: MainAxisSize.min,
@ -381,4 +310,38 @@ class _ItemsList extends StatelessWidget {
)).toList()
);
}
}
class _NoData extends StatelessWidget {
final String label;
const _NoData({
required this.label
});
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(
label,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurface
),
),
const SizedBox(height: 24),
Text(
AppLocalizations.of(context)!.noDataThisSection,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 16),
],
);
}
}

View file

@ -1,276 +0,0 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/logs/configuration/logs_config_modal.dart';
class LogsConfigOptions extends StatelessWidget {
final bool generalSwitch;
final void Function(bool) updateGeneralSwitch;
final bool anonymizeClientIp;
final void Function(bool) updateAnonymizeClientIp;
final List<RetentionItem> retentionItems;
final double? retentionTime;
final void Function(double?) updateRetentionTime;
final void Function() onClear;
final void Function() onConfirm;
const LogsConfigOptions({
super.key,
required this.generalSwitch,
required this.updateGeneralSwitch,
required this.anonymizeClientIp,
required this.updateAnonymizeClientIp,
required this.retentionItems,
required this.retentionTime,
required this.updateRetentionTime,
required this.onClear,
required this.onConfirm
});
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: SingleChildScrollView(
child: Wrap(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 24),
child: Icon(
Icons.settings,
size: 24,
color: Theme.of(context).listTileTheme.iconColor
),
),
const SizedBox(height: 16),
Text(
AppLocalizations.of(context)!.logsSettings,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 24,
color: Theme.of(context).colorScheme.onSurface
),
),
const SizedBox(height: 16),
],
),
],
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Material(
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(28),
child: InkWell(
onTap: () => updateGeneralSwitch(!generalSwitch),
borderRadius: BorderRadius.circular(28),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 8
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppLocalizations.of(context)!.enableLog,
style: const TextStyle(
fontSize: 18,
),
),
Switch(
value: generalSwitch,
onChanged: updateGeneralSwitch,
)
],
),
),
),
),
),
Container(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 14),
child: Column(
children: [
Material(
color: Colors.transparent,
child: InkWell(
onTap: () => updateAnonymizeClientIp(!anonymizeClientIp),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 30),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
AppLocalizations.of(context)!.anonymizeClientIp,
style: const TextStyle(
fontSize: 16
),
),
),
Switch(
value: anonymizeClientIp,
onChanged: updateAnonymizeClientIp,
)
],
),
),
),
),
Container(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: DropdownButtonFormField(
items: retentionItems.map((item) => DropdownMenuItem(
value: item.value,
child: Text(item.label),
)).toList(),
value: retentionTime,
onChanged: (value) => updateRetentionTime(value as double),
decoration: InputDecoration(
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10)
)
),
label: Text(AppLocalizations.of(context)!.retentionTime)
),
borderRadius: BorderRadius.circular(20),
),
),
],
),
)
],
),
),
),
Padding(
padding: const EdgeInsets.all(24),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (width > 500) TextButton(
onPressed: () {
Navigator.pop(context);
onClear();
},
child: Text(AppLocalizations.of(context)!.clearLogs)
),
if (width <= 500) IconButton(
onPressed: () {
Navigator.pop(context);
onClear();
},
icon: const Icon(Icons.delete_rounded),
tooltip: AppLocalizations.of(context)!.clearLogs,
),
Row(
children: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(AppLocalizations.of(context)!.cancel)
),
const SizedBox(width: 20),
TextButton(
onPressed: retentionTime != null
? () {
Navigator.pop(context);
onConfirm();
}
: null,
child: Text(
AppLocalizations.of(context)!.confirm,
style: TextStyle(
color: retentionTime != null
? Theme.of(context).colorScheme.primary
: Colors.grey
),
)
),
],
)
],
),
),
if (Platform.isIOS) const SizedBox(height: 16)
],
);
}
}
class ConfigLogsLoading extends StatelessWidget {
const ConfigLogsLoading({super.key});
@override
Widget build(BuildContext context) {
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
),
),
)
],
),
);
}
}
class ConfigLogsError extends StatelessWidget {
const ConfigLogsError({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.error,
color: Colors.red,
size: 50,
),
const SizedBox(height: 30),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text(
AppLocalizations.of(context)!.logSettingsNotLoaded,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 22,
color: Theme.of(context).colorScheme.onSurfaceVariant
),
),
)
],
);
}
}

View file

@ -1,187 +0,0 @@
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/logs/configuration/config_widgets.dart';
import 'package:adguard_home_manager/constants/enums.dart';
import 'package:adguard_home_manager/providers/servers_provider.dart';
class RetentionItem {
final String label;
final double value;
const RetentionItem({
required this.label,
required this.value,
});
}
class LogsConfigModal extends StatefulWidget {
final BuildContext context;
final void Function(Map<String, dynamic>) onConfirm;
final void Function() onClear;
final bool dialog;
final String serverVersion;
const LogsConfigModal({
super.key,
required this.context,
required this.onConfirm,
required this.onClear,
required this.dialog,
required this.serverVersion
});
@override
State<LogsConfigModal> createState() => _LogsConfigModalState();
}
class _LogsConfigModalState extends State<LogsConfigModal> {
bool generalSwitch = false;
bool anonymizeClientIp = false;
double? retentionTime;
List<RetentionItem> retentionItems = [];
LoadStatus loadStatus = LoadStatus.loading;
void loadData() async {
final serversProvider = Provider.of<ServersProvider>(context, listen: false);
final result = await serversProvider.apiClient2!.getQueryLogInfo();
if (mounted) {
if (result.successful == true) {
setState(() {
generalSwitch = result.content['enabled'];
anonymizeClientIp = result.content['anonymize_client_ip'];
retentionTime = result.content['interval'] != null
? double.parse(result.content['interval'].toString())
: null;
loadStatus = LoadStatus.loaded;
});
}
else {
setState(() => loadStatus = LoadStatus.error);
}
}
}
@override
void initState() {
retentionItems = [
RetentionItem(
label: AppLocalizations.of(widget.context)!.hours6,
value: 21600000
),
RetentionItem(
label: AppLocalizations.of(widget.context)!.hours24,
value: 86400000
),
RetentionItem(
label: AppLocalizations.of(widget.context)!.days7,
value: 604800000
),
RetentionItem(
label: AppLocalizations.of(widget.context)!.days30,
value: 2592000000
),
RetentionItem(
label: AppLocalizations.of(widget.context)!.days90,
value: 7776000000
),
];
loadData();
super.initState();
}
@override
Widget build(BuildContext context) {
if (widget.dialog == true) {
return Dialog(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 500
),
child: Builder(
builder: (context) {
switch (loadStatus) {
case LoadStatus.loading:
return const ConfigLogsLoading();
case LoadStatus.loaded:
return LogsConfigOptions(
generalSwitch: generalSwitch,
updateGeneralSwitch: (v) => setState(() => generalSwitch = v),
anonymizeClientIp: anonymizeClientIp,
updateAnonymizeClientIp: (v) => setState(() => anonymizeClientIp = v),
retentionItems: retentionItems,
retentionTime: retentionTime,
updateRetentionTime: (v) => setState(() => retentionTime = v),
onClear: () => widget.onClear(),
onConfirm: () => widget.onConfirm({
"enabled": generalSwitch,
"interval": retentionTime,
"anonymize_client_ip": anonymizeClientIp
})
);
case LoadStatus.error:
return const ConfigLogsError();
default:
return const SizedBox();
}
},
)
),
);
}
else {
return Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28),
topRight: Radius.circular(28)
),
color: Theme.of(context).dialogBackgroundColor
),
child: SafeArea(
child: Builder(
builder: (context) {
switch (loadStatus) {
case LoadStatus.loading:
return const ConfigLogsLoading();
case LoadStatus.loaded:
return LogsConfigOptions(
generalSwitch: generalSwitch,
updateGeneralSwitch: (v) => setState(() => generalSwitch = v),
anonymizeClientIp: anonymizeClientIp,
updateAnonymizeClientIp: (v) => setState(() => anonymizeClientIp = v),
retentionItems: retentionItems,
retentionTime: retentionTime,
updateRetentionTime: (v) => setState(() => retentionTime = v),
onClear: () => widget.onClear(),
onConfirm: () => widget.onConfirm({
"enabled": generalSwitch,
"interval": retentionTime,
"anonymize_client_ip": anonymizeClientIp
})
);
case LoadStatus.error:
return const ConfigLogsError();
default:
return const SizedBox();
}
},
),
)
);
}
}
}

View file

@ -4,10 +4,13 @@ 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_checkbox_list_tile.dart';
import 'package:adguard_home_manager/widgets/list_bottom_sheet.dart';
import 'package:adguard_home_manager/providers/clients_provider.dart';
import 'package:adguard_home_manager/providers/logs_provider.dart';
class ClientsModal extends StatefulWidget {
class ClientsModal extends StatelessWidget {
final List<String>? value;
final bool dialog;
@ -17,56 +20,90 @@ class ClientsModal extends StatefulWidget {
required this.dialog
});
@override
State<ClientsModal> createState() => _ClientsModalState();
}
class _ClientsModalState extends State<ClientsModal> {
List<String> selectedClients = [];
@override
void initState() {
setState(() => selectedClients = widget.value ?? []);
super.initState();
}
@override
Widget build(BuildContext context) {
final height = MediaQuery.of(context).size.height;
final clientsProvider = Provider.of<ClientsProvider>(context);
final logsProvider = Provider.of<LogsProvider>(context);
if (widget.dialog == true) {
if (dialog == true) {
return Dialog(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 500
),
child: _ModalContent(
selectedClients: selectedClients,
onClientsSelected: (v) => setState(() => selectedClients = v),
selectedClients: logsProvider.selectedClients,
onClientsSelected: (v) => logsProvider.setSelectedClients(v),
)
),
);
}
else {
return ConstrainedBox(
constraints: BoxConstraints(
maxHeight: height-50
),
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28),
topRight: Radius.circular(28)
return ListBottomSheet(
icon: Icons.smartphone_rounded,
title: AppLocalizations.of(context)!.clients,
children: [
Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Icon(
Icons.info_rounded,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(width: 16),
Flexible(
child: Text(AppLocalizations.of(context)!.selectClientsFiltersInfo)
)
],
),
),
color: Theme.of(context).dialogBackgroundColor
),
child: SafeArea(
child: _ModalContent(
selectedClients: selectedClients,
onClientsSelected: (v) => setState(() => selectedClients = v),
CustomCheckboxListTile(
padding: const EdgeInsets.only(
left: 24,
top: 8,
right: 12,
bottom: 8
),
value: logsProvider.selectedClients.length == clientsProvider.clients!.autoClients.length,
onChanged: (v) {
if (v == true) {
logsProvider.setSelectedClients(clientsProvider.clients!.autoClients.map((e) => e.ip).toList());
}
else {
logsProvider.setSelectedClients([]);
}
},
title: AppLocalizations.of(context)!.selectAll
),
ListView.builder(
shrinkWrap: true,
primary: false,
itemCount: clientsProvider.clients!.autoClients.length,
itemBuilder: (context, index) => _ListItem(
label: clientsProvider.clients!.autoClients[index].ip,
checkboxActive: logsProvider.selectedClients.contains(clientsProvider.clients!.autoClients[index].ip),
onChanged: (isSelected) {
if (isSelected == true) {
logsProvider.setSelectedClients([
...logsProvider.selectedClients,
clientsProvider.clients!.autoClients[index].ip
]);
}
else {
logsProvider.setSelectedClients(
logsProvider.selectedClients.where(
(item) => item != clientsProvider.clients!.autoClients[index].ip
).toList()
);
}
}
)
)
),
]
);
}
}
@ -86,97 +123,92 @@ class _ModalContent extends StatelessWidget {
final clientsProvider = Provider.of<ClientsProvider>(context);
final logsProvider = Provider.of<LogsProvider>(context);
void apply() async {
logsProvider.setSelectedClients(
selectedClients.isNotEmpty ? selectedClients : null
);
Navigator.pop(context);
}
void selectAll() {
onClientsSelected(
clientsProvider.clients!.autoClients.map((item) => item.ip).toList()
);
}
void unselectAll() {
onClientsSelected([]);
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Column(
children: [
Padding(
padding: const EdgeInsets.only(
top: 24,
bottom: 16,
),
child: Icon(
Icons.smartphone_rounded,
size: 24,
color: Theme.of(context).listTileTheme.iconColor
),
),
Text(
AppLocalizations.of(context)!.clients,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurface
),
),
const SizedBox(height: 16),
],
),
Flexible(
child: ListView.builder(
itemCount: clientsProvider.clients!.autoClients.length,
itemBuilder: (context, index) => _ListItem(
label: clientsProvider.clients!.autoClients[index].ip,
checkboxActive: selectedClients.contains(clientsProvider.clients!.autoClients[index].ip),
onChanged: (isSelected) {
if (isSelected == true) {
onClientsSelected([
...selectedClients,
clientsProvider.clients!.autoClients[index].ip
]);
}
else {
onClientsSelected(
selectedClients.where(
(item) => item != clientsProvider.clients!.autoClients[index].ip
).toList()
);
}
}
)
)
),
Padding(
padding: const EdgeInsets.all(24),
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(
onPressed: selectedClients.length == clientsProvider.clients!.autoClients.length
? () => unselectAll()
: () => selectAll(),
child: Text(
selectedClients.length == clientsProvider.clients!.autoClients.length
? AppLocalizations.of(context)!.unselectAll
: AppLocalizations.of(context)!.selectAll
)
CloseButton(
onPressed: () => Navigator.pop(context),
),
TextButton(
onPressed: apply,
child: Text(AppLocalizations.of(context)!.apply)
const SizedBox(width: 12),
Text(
AppLocalizations.of(context)!.clients,
style: const TextStyle(
fontSize: 22
),
)
],
),
),
Flexible(
child: ListView(
children: [
Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Icon(
Icons.info_rounded,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(width: 16),
Flexible(
child: Text(AppLocalizations.of(context)!.selectClientsFiltersInfo)
)
],
),
),
),
CustomCheckboxListTile(
padding: const EdgeInsets.only(
left: 24,
top: 8,
right: 12,
bottom: 8
),
value: logsProvider.selectedClients.length == clientsProvider.clients!.autoClients.length,
onChanged: (v) {
if (v == true) {
logsProvider.setSelectedClients(clientsProvider.clients!.autoClients.map((e) => e.ip).toList());
}
else {
logsProvider.setSelectedClients([]);
}
},
title: AppLocalizations.of(context)!.selectAll
),
ListView.builder(
primary: false,
shrinkWrap: true,
itemCount: clientsProvider.clients!.autoClients.length,
itemBuilder: (context, index) => _ListItem(
label: clientsProvider.clients!.autoClients[index].ip,
checkboxActive: selectedClients.contains(clientsProvider.clients!.autoClients[index].ip),
onChanged: (isSelected) {
if (isSelected == true) {
onClientsSelected([
...selectedClients,
clientsProvider.clients!.autoClients[index].ip
]);
}
else {
onClientsSelected(
selectedClients.where(
(item) => item != clientsProvider.clients!.autoClients[index].ip
).toList()
);
}
}
)
),
],
)
),
if (Platform.isIOS) const SizedBox(height: 16)
],
);
@ -203,9 +235,9 @@ class _ListItem extends StatelessWidget {
child: Padding(
padding: const EdgeInsets.only(
left: 24,
top: 8,
top: 4,
right: 12,
bottom: 8
bottom: 4
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,

View file

@ -150,6 +150,7 @@ class _FiltersList extends StatelessWidget {
dialog: false,
),
isScrollControlled: true,
useSafeArea: true,
backgroundColor: Colors.transparent
);
}
@ -225,8 +226,8 @@ class _FiltersList extends StatelessWidget {
Container(height: 16),
CustomListTile(
title: AppLocalizations.of(context)!.client,
subtitle: logsProvider.selectedClients != null
? "${logsProvider.selectedClients!.length} ${AppLocalizations.of(context)!.clientsSelected}"
subtitle: logsProvider.selectedClients.isNotEmpty
? "${logsProvider.selectedClients.length} ${AppLocalizations.of(context)!.clientsSelected}"
: AppLocalizations.of(context)!.all,
onTap: clientsProvider.loadStatus == LoadStatus.loaded
? openSelectClients

View file

@ -7,16 +7,10 @@ import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/logs/filters/logs_filters_modal.dart';
import 'package:adguard_home_manager/screens/logs/configuration/logs_config_modal.dart';
import 'package:adguard_home_manager/classes/process_modal.dart';
import 'package:adguard_home_manager/constants/enums.dart';
import 'package:adguard_home_manager/functions/desktop_mode.dart';
import 'package:adguard_home_manager/functions/snackbar.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/providers/servers_provider.dart';
import 'package:adguard_home_manager/providers/status_provider.dart';
import 'package:adguard_home_manager/providers/logs_provider.dart';
class LogsListAppBar extends StatelessWidget {
@ -32,61 +26,9 @@ class LogsListAppBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
final logsProvider = Provider.of<LogsProvider>(context);
final statusProvider = Provider.of<StatusProvider>(context);
final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
void updateConfig(Map<String, dynamic> data) async {
ProcessModal processModal = ProcessModal();
processModal.open(AppLocalizations.of(context)!.updatingSettings);
final result = await serversProvider.apiClient2!.updateQueryLogParameters(data: data);
processModal.close();
if (result.successful == true) {
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.logsConfigUpdated,
color: Colors.green
);
}
else {
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.logsConfigNotUpdated,
color: Colors.red
);
}
}
void clearQueries() async {
ProcessModal processModal = ProcessModal();
processModal.open(AppLocalizations.of(context)!.updatingSettings);
final result = await serversProvider.apiClient2!.clearLogs();
processModal.close();
if (result.successful == true) {
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.logsCleared,
color: Colors.green
);
}
else {
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.logsNotCleared,
color: Colors.red
);
}
}
void openFilersModal() {
if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) {
showDialog(
@ -143,40 +85,6 @@ class LogsListAppBar extends StatelessWidget {
tooltip: AppLocalizations.of(context)!.filters,
)
: const SizedBox(),
if (statusProvider.serverStatus != null) IconButton(
tooltip: AppLocalizations.of(context)!.settings,
onPressed: () => {
if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) {
showDialog(
context: context,
builder: (context) => LogsConfigModal(
context: context,
onConfirm: updateConfig,
onClear: clearQueries,
dialog: true,
serverVersion: statusProvider.serverStatus!.serverVersion,
),
barrierDismissible: false
)
}
else {
showModalBottomSheet(
context: context,
useRootNavigator: true,
builder: (context) => LogsConfigModal(
context: context,
onConfirm: updateConfig,
onClear: clearQueries,
dialog: false,
serverVersion: statusProvider.serverStatus!.serverVersion,
),
backgroundColor: Colors.transparent,
isScrollControlled: true
)
}
},
icon: const Icon(Icons.settings)
),
const SizedBox(width: 5),
],
bottom: logsProvider.appliedFilters.searchText != null || logsProvider.appliedFilters.selectedResultStatus != 'all' || logsProvider.appliedFilters.clients != null

View file

@ -1,194 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/models/app_log.dart';
class AppLogDetailsModal extends StatefulWidget {
final AppLog log;
const AppLogDetailsModal({
Key? key,
required this.log
}) : super(key: key);
@override
State<AppLogDetailsModal> createState() => _AppLogDetailsModalState();
}
class _AppLogDetailsModalState extends State<AppLogDetailsModal> {
String valueToShow = 'message';
String generateBody() {
switch (valueToShow) {
case 'message':
return widget.log.message;
case 'statusCode':
return widget.log.statusCode != null
? widget.log.statusCode.toString()
: "[NO STAUS CODE]";
case 'body':
return widget.log.resBody != null
? widget.log.resBody.toString()
: "[NO RESPONSE BODY]";
default:
return '';
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Column(
children: [
Icon(
Icons.description_rounded,
size: 24,
color: Theme.of(context).listTileTheme.iconColor,
),
const SizedBox(height: 16),
Text(
AppLocalizations.of(context)!.logDetails,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 24,
color: Theme.of(context).colorScheme.onSurface
),
)
],
),
scrollable: true,
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 20),
Row(
children: [
Material(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(15),
bottomLeft: Radius.circular(15)
),
color: Colors.transparent,
child: InkWell(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(15),
bottomLeft: Radius.circular(15)
),
onTap: () => setState(() => valueToShow = 'message'),
child: AnimatedContainer(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(15),
bottomLeft: Radius.circular(15)
),
border: Border.all(
color: Theme.of(context).colorScheme.primary
),
color: valueToShow == 'message'
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.primary.withOpacity(0.05)
),
child: Text(
"Message",
style: TextStyle(
color: valueToShow == 'message'
? Colors.white
: null
),
),
),
),
),
Material(
color: Colors.transparent,
child: InkWell(
onTap: () => setState(() => valueToShow = 'statusCode'),
child: AnimatedContainer(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
decoration: BoxDecoration(
border: Border(
top: BorderSide(
color: Theme.of(context).colorScheme.primary
),
bottom: BorderSide(
color: Theme.of(context).colorScheme.primary
),
),
color: valueToShow == 'statusCode'
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.primary.withOpacity(0.05)
),
child: Text(
"Status code",
style: TextStyle(
color: valueToShow == 'statusCode'
? Colors.white
: null
),
),
),
),
),
Material(
borderRadius: const BorderRadius.only(
topRight: Radius.circular(15),
bottomRight: Radius.circular(15)
),
color: Colors.transparent,
child: InkWell(
borderRadius: const BorderRadius.only(
topRight: Radius.circular(15),
bottomRight: Radius.circular(15)
),
onTap: () => setState(() => valueToShow = 'body'),
child: AnimatedContainer(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topRight: Radius.circular(15),
bottomRight: Radius.circular(15)
),
border: Border.all(
color: Theme.of(context).colorScheme.primary
),
color: valueToShow == 'body'
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.primary.withOpacity(0.05)
),
child: Text(
"Body",
style: TextStyle(
color: valueToShow == 'body'
? Colors.white
: null
),
),
),
),
),
],
),
const SizedBox(height: 20),
Text(generateBody())
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("Close")
)
],
);
}
}

View file

@ -1,81 +0,0 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:convert';
import 'package:adguard_home_manager/functions/copy_clipboard.dart';
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/settings/app_logs/app_log_details_modal.dart';
import 'package:adguard_home_manager/providers/app_config_provider.dart';
class AppLogs extends StatelessWidget {
const AppLogs({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final appConfigProvider = Provider.of<AppConfigProvider>(context);
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.logs),
actions: [
IconButton(
onPressed: appConfigProvider.logs.isNotEmpty
? () => copyToClipboard(
value: jsonEncode(appConfigProvider.logs.map((log) => log.toMap()).toList()),
successMessage: AppLocalizations.of(context)!.logsCopiedClipboard
)
: null,
icon: const Icon(Icons.share),
tooltip: AppLocalizations.of(context)!.copyLogsClipboard,
),
const SizedBox(width: 10),
],
),
body: appConfigProvider.logs.isNotEmpty
? ListView.builder(
padding: const EdgeInsets.only(top: 0),
itemCount: appConfigProvider.logs.length,
itemBuilder: (context, index) => ListTile(
title: Text(
appConfigProvider.logs[index].message,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.normal,
color: Theme.of(context).colorScheme.onSurface
),
),
subtitle: Text(
appConfigProvider.logs[index].dateTime.toString(),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: Theme.of(context).listTileTheme.textColor
),
),
trailing: Text(appConfigProvider.logs[index].type),
onTap: () => {
showDialog(
context: context,
builder: (context) => AppLogDetailsModal(
log: appConfigProvider.logs[index]
)
)
},
)
)
: Center(
child: Text(
AppLocalizations.of(context)!.noSavedLogs,
style: TextStyle(
fontSize: 24,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
)
);
}
}

View file

@ -1,23 +1,31 @@
import 'package:flutter/material.dart';
class ColorItem extends StatelessWidget {
final int index;
final int total;
final Color color;
final int numericValue;
final int? selectedValue;
final void Function(int) onChanged;
const ColorItem({
Key? key,
super.key,
required this.index,
required this.total,
required this.color,
required this.numericValue,
required this.selectedValue,
required this.onChanged
}) : super(key: key);
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(10),
padding: index == 0
? const EdgeInsets.only(top: 10, right: 10, bottom: 10)
: index == total-1
? const EdgeInsets.only(top: 10, bottom: 10, left: 10)
: const EdgeInsets.all(10),
child: Material(
borderRadius: BorderRadius.circular(50),
child: InkWell(

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';
@ -30,9 +32,9 @@ class CustomizationWidget extends StatefulWidget {
final AppConfigProvider appConfigProvider;
const CustomizationWidget({
Key? key,
super.key,
required this.appConfigProvider,
}) : super(key: key);
});
@override
State<CustomizationWidget> createState() => _CustomizationWidgetState();
@ -44,6 +46,8 @@ class _CustomizationWidgetState extends State<CustomizationWidget> {
int selectedColor = 0;
bool useThemeColorInsteadGreenRed = false;
final _colorsScrollController = ScrollController();
@override
void initState() {
selectedTheme = widget.appConfigProvider.selectedThemeNumber;
@ -125,83 +129,84 @@ class _CustomizationWidgetState extends State<CustomizationWidget> {
title: AppLocalizations.of(context)!.useDynamicTheme,
),
if (!(appConfigProvider.androidDeviceInfo != null && appConfigProvider.androidDeviceInfo!.version.sdkInt >= 31)) const SizedBox(height: 20),
if (dynamicColor == false) ...[
SizedBox(
width: MediaQuery.of(context).size.width,
height: 70,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: colors.length,
itemBuilder: (context, index) {
if (index == 0) {
return Row(
children: [
const SizedBox(width: 15),
ColorItem(
color: colors[index],
numericValue: index,
selectedValue: selectedColor,
onChanged: (value) {
setState(() => selectedColor = value);
appConfigProvider.setStaticColor(value);
if (dynamicColor == false) Padding(
padding: const EdgeInsets.only(bottom: 8, left: 16, right: 16),
child: Scrollbar(
controller: _colorsScrollController,
thumbVisibility: Platform.isMacOS || Platform.isLinux || Platform.isWindows,
interactive: Platform.isMacOS || Platform.isLinux || Platform.isWindows,
thickness: Platform.isMacOS || Platform.isLinux || Platform.isWindows ? 8 : 0,
child: Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: MediaQuery.of(context).size.width,
height: 70,
child: ListView.builder(
controller: _colorsScrollController,
scrollDirection: Axis.horizontal,
itemCount: colors.length,
padding: const EdgeInsets.all(0),
itemBuilder: (context, index) {
if (index == 0) {
return Row(
children: [
ColorItem(
index: index,
total: colors.length,
color: colors[index],
numericValue: index,
selectedValue: selectedColor,
onChanged: (value) {
setState(() => selectedColor = value);
appConfigProvider.setStaticColor(value);
}
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 10),
width: 1,
height: 60,
decoration: BoxDecoration(
color: Colors.grey,
borderRadius: BorderRadius.circular(1)
),
)
],
);
}
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 10),
width: 1,
height: 60,
decoration: BoxDecoration(
color: Colors.grey,
borderRadius: BorderRadius.circular(1)
),
)
],
);
}
else if (index == colors.length-1) {
return Row(
children: [
ColorItem(
color: colors[index],
numericValue: index,
selectedValue: selectedColor,
onChanged: (value) {
setState(() => selectedColor = value);
appConfigProvider.setStaticColor(value);
else {
return ColorItem(
index: index,
total: colors.length,
color: colors[index],
numericValue: index,
selectedValue: selectedColor,
onChanged: (value) {
setState(() => selectedColor = value);
appConfigProvider.setStaticColor(value);
}
);
}
},
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
colorTranslation(context, selectedColor),
style: TextStyle(
color: Theme.of(context).listTileTheme.iconColor,
fontSize: 16
),
const SizedBox(width: 15)
],
);
}
else {
return ColorItem(
color: colors[index],
numericValue: index,
selectedValue: selectedColor,
onChanged: (value) {
setState(() => selectedColor = value);
appConfigProvider.setStaticColor(value);
}
);
}
},
),
),
Padding(
padding: const EdgeInsets.only(
left: 25,
top: 10
),
child: Text(
colorTranslation(context, selectedColor),
style: TextStyle(
color: Theme.of(context).listTileTheme.iconColor,
fontSize: 16
),
)
],
),
),
)
],
),
),
CustomSwitchListTile(
value: useThemeColorInsteadGreenRed,
onChanged: (value) {

View file

@ -15,6 +15,7 @@ import 'package:adguard_home_manager/screens/settings/dhcp/dhcp_leases.dart';
import 'package:adguard_home_manager/screens/settings/dhcp/select_interface_modal.dart';
import 'package:adguard_home_manager/functions/desktop_mode.dart';
import 'package:adguard_home_manager/providers/status_provider.dart';
import 'package:adguard_home_manager/functions/snackbar.dart';
import 'package:adguard_home_manager/constants/enums.dart';
import 'package:adguard_home_manager/providers/dhcp_provider.dart';
@ -182,13 +183,15 @@ class _DhcpScreenState extends State<DhcpScreen> {
@override
void initState() {
if (mounted) loadDhcpStatus();
final statusProvider = Provider.of<StatusProvider>(context, listen: false);
if (mounted && statusProvider.serverStatus?.dhcpAvailable == true) loadDhcpStatus();
super.initState();
}
@override
Widget build(BuildContext context) {
final serversProvider = Provider.of<ServersProvider>(context);
final statusProvider = Provider.of<StatusProvider>(context);
final dhcpProvider = Provider.of<DhcpProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
@ -353,11 +356,7 @@ class _DhcpScreenState extends State<DhcpScreen> {
});
}
if (
dhcpProvider.loadStatus == LoadStatus.loaded &&
dhcpProvider.dhcp != null &&
dhcpProvider.dhcp!.dhcpAvailable == false
) {
if (statusProvider.serverStatus?.dhcpAvailable != true) {
return const DhcpNotAvailable();
}

View file

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/settings/dhcp/dhcp_interface_item.dart';
import 'package:adguard_home_manager/widgets/list_bottom_sheet.dart';
import 'package:adguard_home_manager/models/dhcp.dart';
@ -89,70 +90,20 @@ class SelectInterfaceModal extends StatelessWidget {
);
}
else {
return GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: DraggableScrollableSheet(
initialChildSize: 0.6,
minChildSize: 0.3,
maxChildSize: 1,
builder: (context, controller) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28),
topRight: Radius.circular(28),
),
),
child: Column(
children: [
Container(
margin: const EdgeInsets.all(16),
width: 36,
height: 4,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Colors.grey
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
children: [
Icon(
Icons.settings_ethernet_rounded,
size: 24,
color: Theme.of(context).listTileTheme.iconColor
),
const SizedBox(height: 16),
Text(
AppLocalizations.of(context)!.selectInterface,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 24,
color: Theme.of(context).colorScheme.onSurface
),
),
],
),
),
Expanded(
child: SafeArea(
child: ListView.builder(
controller: controller,
itemCount: interfaces.length,
itemBuilder: (context, index) => DhcpInterfaceItem(
networkInterface: interfaces[index],
onSelect: onSelect
)
),
)
),
],
),
);
},
),
return ListBottomSheet(
icon: Icons.settings_ethernet_rounded,
title: AppLocalizations.of(context)!.selectInterface,
children: [
ListView.builder(
primary: false,
shrinkWrap: true,
itemCount: interfaces.length,
itemBuilder: (context, index) => DhcpInterfaceItem(
networkInterface: interfaces[index],
onSelect: onSelect
)
),
]
);
}
}

View file

@ -206,28 +206,6 @@ class _GeneralSettingsState extends State<GeneralSettings> {
)
)
),
CustomListTile(
icon: Icons.donut_large_rounded,
title: AppLocalizations.of(context)!.showTopItemsChart,
subtitle: AppLocalizations.of(context)!.showTopItemsChartDescription,
trailing: Switch(
value: appConfigProvider.showTopItemsChart,
onChanged: (value) => updateSettings(
newStatus: value,
function: appConfigProvider.setShowTopItemsChart
),
),
onTap: () => updateSettings(
newStatus: !appConfigProvider.showTopItemsChart,
function: appConfigProvider.setShowTopItemsChart
),
padding: const EdgeInsets.only(
top: 10,
bottom: 10,
left: 16,
right: 10
)
),
SectionLabel(label: AppLocalizations.of(context)!.logs),
CustomListTile(
icon: Icons.timer_rounded,

View file

@ -0,0 +1,187 @@
import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/settings/logs_settings/logs_settings.dart';
import 'package:adguard_home_manager/widgets/section_label.dart';
import 'package:adguard_home_manager/widgets/master_switch.dart';
import 'package:adguard_home_manager/widgets/custom_checkbox_list_tile.dart';
class LogsConfigOptions extends StatelessWidget {
final bool generalSwitch;
final void Function(bool) updateGeneralSwitch;
final bool anonymizeClientIp;
final void Function(bool) updateAnonymizeClientIp;
final List<int> retentionItems;
final double? retentionTime;
final void Function(double?) updateRetentionTime;
final void Function() onClear;
final void Function() onConfirm;
final List<DomainListItemController> ignoredDomainsControllers;
final void Function(List<DomainListItemController>) updateIgnoredDomainsControllers;
const LogsConfigOptions({
super.key,
required this.generalSwitch,
required this.updateGeneralSwitch,
required this.anonymizeClientIp,
required this.updateAnonymizeClientIp,
required this.retentionItems,
required this.retentionTime,
required this.updateRetentionTime,
required this.onClear,
required this.onConfirm,
required this.ignoredDomainsControllers,
required this.updateIgnoredDomainsControllers
});
@override
Widget build(BuildContext context) {
const Uuid uuid = Uuid();
final List<String> dropdownItemTranslation = [
AppLocalizations.of(context)!.hours6,
AppLocalizations.of(context)!.hours24,
AppLocalizations.of(context)!.days7,
AppLocalizations.of(context)!.days30,
AppLocalizations.of(context)!.days90,
];
void validateDomain(String value, String id) {
final domainRegex = RegExp(r'^([a-z0-9|-]+\.)*[a-z0-9|-]+\.[a-z]+$');
bool error = false;
if (domainRegex.hasMatch(value)) {
error = false;
}
else {
error = true;
}
updateIgnoredDomainsControllers(
ignoredDomainsControllers.map((entry) {
if (entry.id != id) return entry;
return DomainListItemController(
id: id,
controller: entry.controller,
error: error
);
}).toList()
);
}
return ListView(
children: [
const SizedBox(height: 16),
MasterSwitch(
label: AppLocalizations.of(context)!.enableLog,
value: generalSwitch,
onChange: updateGeneralSwitch
),
const SizedBox(height: 16),
CustomCheckboxListTile(
value: anonymizeClientIp,
onChanged: (_) => updateAnonymizeClientIp(!anonymizeClientIp),
title: AppLocalizations.of(context)!.anonymizeClientIp,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: DropdownButtonFormField(
items: retentionItems.asMap().entries.map((item) => DropdownMenuItem(
value: item.value,
child: Text(dropdownItemTranslation[item.key]),
)).toList(),
value: retentionTime,
onChanged: (value) => updateRetentionTime(value as double),
decoration: InputDecoration(
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10)
)
),
label: Text(AppLocalizations.of(context)!.retentionTime)
),
borderRadius: BorderRadius.circular(20),
),
),
Padding(
padding: const EdgeInsets.only(top: 24, bottom: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SectionLabel(
label: AppLocalizations.of(context)!.ignoredDomains,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 0),
),
Padding(
padding: const EdgeInsets.only(right: 6),
child: IconButton(
onPressed: () => updateIgnoredDomainsControllers([
...ignoredDomainsControllers,
DomainListItemController(
id: uuid.v4(),
controller: TextEditingController(),
error: false
),
]),
icon: const Icon(Icons.add)
),
)
],
),
),
if (ignoredDomainsControllers.isNotEmpty) ...ignoredDomainsControllers.map((controller) => Padding(
padding: const EdgeInsets.only(
top: 12, bottom: 12, left: 16, right: 6
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: TextFormField(
controller: controller.controller,
onChanged: (v) => validateDomain(v, controller.id),
decoration: InputDecoration(
prefixIcon: const Icon(Icons.link_rounded),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10)
)
),
labelText: AppLocalizations.of(context)!.domain,
errorText: controller.error
? AppLocalizations.of(context)!.invalidDomain
: null
),
),
),
const SizedBox(width: 12),
Padding(
padding: controller.error
? const EdgeInsets.only(bottom: 24)
: const EdgeInsets.all(0),
child: IconButton(
onPressed: () => updateIgnoredDomainsControllers(
ignoredDomainsControllers.where((e) => e.id != controller.id).toList()
),
icon: const Icon(Icons.remove_circle_outline_outlined)
),
)
],
),
)),
if (ignoredDomainsControllers.isEmpty) Container(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Text(
AppLocalizations.of(context)!.noIgnoredDomainsAdded,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
],
);
}
}

View file

@ -0,0 +1,213 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:uuid/uuid.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/settings/logs_settings/config_widgets.dart';
import 'package:adguard_home_manager/widgets/load_status_widgets.dart';
import 'package:adguard_home_manager/models/querylog_config.dart';
import 'package:adguard_home_manager/classes/process_modal.dart';
import 'package:adguard_home_manager/functions/snackbar.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/servers_provider.dart';
class DomainListItemController {
final String id;
final TextEditingController controller;
bool error;
DomainListItemController({
required this.id,
required this.controller,
required this.error
});
}
class LogsSettings extends StatefulWidget {
const LogsSettings({super.key});
@override
State<LogsSettings> createState() => _LogsSettingsState();
}
class _LogsSettingsState extends State<LogsSettings> {
final Uuid uuid = const Uuid();
bool generalSwitch = false;
bool anonymizeClientIp = false;
double? retentionTime;
List<DomainListItemController> _ignoredDomainsControllers = [];
List<int> retentionItems = [
21600000,
86400000,
604800000,
2592000000,
7776000000
];
LoadStatus loadStatus = LoadStatus.loading;
void loadData() async {
final serversProvider = Provider.of<ServersProvider>(context, listen: false);
final result = await serversProvider.apiClient2!.getQueryLogInfo();
if (!mounted) return;
if (result.successful == true) {
final data = result.content as QueryLogConfig;
setState(() {
generalSwitch = data.enabled ?? false;
anonymizeClientIp = data.anonymizeClientIp ?? false;
retentionTime = data.interval != null
? double.parse(data.interval.toString())
: null;
if (data.ignored != null) {
_ignoredDomainsControllers = data.ignored!.map((e) => DomainListItemController(
id: uuid.v4(),
controller: TextEditingController(text: e),
error: false
)).toList();
}
loadStatus = LoadStatus.loaded;
});
}
else {
setState(() => loadStatus = LoadStatus.error);
}
}
@override
void initState() {
loadData();
super.initState();
}
@override
Widget build(BuildContext context) {
final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final validValues = _ignoredDomainsControllers.where(
(d) => d.controller.text == "" || d.error == true
).isEmpty;
void clearQueries() async {
ProcessModal processModal = ProcessModal();
processModal.open(AppLocalizations.of(context)!.updatingSettings);
final result = await serversProvider.apiClient2!.clearLogs();
processModal.close();
if (!mounted) return;
if (result.successful == true) {
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.logsCleared,
color: Colors.green
);
}
else {
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.logsNotCleared,
color: Colors.red
);
}
}
void updateConfig() async {
ProcessModal processModal = ProcessModal();
processModal.open(AppLocalizations.of(context)!.updatingSettings);
final result = await serversProvider.apiClient2!.updateQueryLogParameters(
data: {
"enabled": generalSwitch,
"interval": retentionTime,
"anonymize_client_ip": anonymizeClientIp,
"ignored": _ignoredDomainsControllers.map((e) => e.controller.text).toList()
}
);
processModal.close();
if (!mounted) return;
if (result.successful == true) {
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.logsConfigUpdated,
color: Colors.green
);
}
else {
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.logsConfigNotUpdated,
color: Colors.red
);
}
}
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.logsSettings),
actions: [
if (loadStatus == LoadStatus.loaded) IconButton(
onPressed: validValues ? () => updateConfig() : null,
icon: const Icon(Icons.save_rounded),
tooltip: AppLocalizations.of(context)!.save,
),
if (loadStatus == LoadStatus.loaded) PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
onTap: clearQueries,
child: Row(
children: [
const Icon(Icons.delete_rounded),
const SizedBox(width: 8),
Text(AppLocalizations.of(context)!.clearLogs),
],
)
)
],
),
const SizedBox(width: 8)
],
),
body: Builder(
builder: (context) {
switch (loadStatus) {
case LoadStatus.loading:
return LoadingData(text: AppLocalizations.of(context)!.loadingLogsSettings);
case LoadStatus.loaded:
return LogsConfigOptions(
generalSwitch: generalSwitch,
updateGeneralSwitch: (v) => setState(() => generalSwitch = v),
anonymizeClientIp: anonymizeClientIp,
updateAnonymizeClientIp: (v) => setState(() => anonymizeClientIp = v),
retentionItems: retentionItems,
retentionTime: retentionTime,
updateRetentionTime: (v) => setState(() => retentionTime = v),
onClear: clearQueries,
onConfirm: updateConfig,
ignoredDomainsControllers: _ignoredDomainsControllers,
updateIgnoredDomainsControllers: (v) => setState(() => _ignoredDomainsControllers = v),
);
case LoadStatus.error:
return ErrorLoadData(text: AppLocalizations.of(context)!.logSettingsNotLoaded,);
default:
return const SizedBox();
}
},
),
);
}
}

View file

@ -8,9 +8,11 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/settings/server_info/server_info.dart';
import 'package:adguard_home_manager/screens/settings/encryption/encryption.dart';
import 'package:adguard_home_manager/screens/settings/logs_settings/logs_settings.dart';
import 'package:adguard_home_manager/screens/settings/access_settings/access_settings.dart';
import 'package:adguard_home_manager/screens/settings/customization/customization.dart';
import 'package:adguard_home_manager/screens/settings/dhcp/dhcp.dart';
import 'package:adguard_home_manager/screens/settings/statistics_settings/statistics_settings.dart';
import 'package:adguard_home_manager/screens/settings/safe_search_settings.dart';
import 'package:adguard_home_manager/screens/settings/update_server/update.dart';
import 'package:adguard_home_manager/screens/settings/dns/dns.dart';
@ -99,198 +101,217 @@ class _SettingsWidgetState extends State<_SettingsWidget> {
appConfigProvider.setSelectedSettingsScreen(screen: null);
}
return Scaffold(
body: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar.large(
pinned: true,
floating: true,
centerTitle: false,
forceElevated: innerBoxIsScrolled,
surfaceTintColor: isDesktop(width) ? Colors.transparent : null,
title: Text(AppLocalizations.of(context)!.settings),
return ScaffoldMessenger(
key: widget.twoColumns ? GlobalKey() : null,
child: Scaffold(
body: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar.large(
pinned: true,
floating: true,
centerTitle: false,
forceElevated: innerBoxIsScrolled,
surfaceTintColor: isDesktop(width) ? Colors.transparent : null,
title: Text(AppLocalizations.of(context)!.settings),
)
)
)
],
body: SafeArea(
top: false,
bottom: false,
child: Builder(
builder: (context) => CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
SliverList.list(
children: [
if (
serversProvider.selectedServer != null &&
statusProvider.serverStatus != null &&
serversProvider.apiClient2 != null
) ...[
SectionLabel(label: AppLocalizations.of(context)!.serverSettings),
_SettingsTile(
icon: Icons.search_rounded,
title: AppLocalizations.of(context)!.safeSearch,
subtitle: AppLocalizations.of(context)!.safeSearchSettings,
thisItem: 0,
screenToNavigate: const SafeSearchSettingsScreen(),
twoColumns: widget.twoColumns,
),
_SettingsTile(
icon: Icons.lock_rounded,
title: AppLocalizations.of(context)!.accessSettings,
subtitle: AppLocalizations.of(context)!.accessSettingsDescription,
thisItem: 1,
screenToNavigate: const AccessSettings(),
twoColumns: widget.twoColumns,
),
_SettingsTile(
icon: Icons.install_desktop_rounded,
title: AppLocalizations.of(context)!.dhcpSettings,
subtitle: AppLocalizations.of(context)!.dhcpSettingsDescription,
thisItem: 2,
screenToNavigate: const DhcpScreen(),
twoColumns: widget.twoColumns,
),
_SettingsTile(
icon: Icons.dns_rounded,
title: AppLocalizations.of(context)!.dnsSettings,
subtitle: AppLocalizations.of(context)!.dnsSettingsDescription,
thisItem: 3,
screenToNavigate: DnsSettings(
splitView: widget.twoColumns,
],
body: SafeArea(
top: false,
bottom: false,
child: Builder(
builder: (context) => CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
SliverList.list(
children: [
if (
serversProvider.selectedServer != null &&
statusProvider.serverStatus != null &&
serversProvider.apiClient2 != null
) ...[
SectionLabel(label: AppLocalizations.of(context)!.serverSettings),
_SettingsTile(
icon: Icons.search_rounded,
title: AppLocalizations.of(context)!.safeSearch,
subtitle: AppLocalizations.of(context)!.safeSearchSettings,
thisItem: 0,
screenToNavigate: const SafeSearchSettingsScreen(),
twoColumns: widget.twoColumns,
),
_SettingsTile(
icon: Icons.list_alt_rounded,
title: AppLocalizations.of(context)!.logsSettings,
subtitle: AppLocalizations.of(context)!.logsSettingsDescription,
thisItem: 1,
screenToNavigate: const LogsSettings(),
twoColumns: widget.twoColumns,
),
_SettingsTile(
icon: Icons.analytics_rounded,
title: AppLocalizations.of(context)!.statisticsSettings,
subtitle: AppLocalizations.of(context)!.statisticsSettingsDescription,
thisItem: 2,
screenToNavigate: const StatisticsSettings(),
twoColumns: widget.twoColumns,
),
_SettingsTile(
icon: Icons.lock_rounded,
title: AppLocalizations.of(context)!.accessSettings,
subtitle: AppLocalizations.of(context)!.accessSettingsDescription,
thisItem: 3,
screenToNavigate: const AccessSettings(),
twoColumns: widget.twoColumns,
),
_SettingsTile(
icon: Icons.install_desktop_rounded,
title: AppLocalizations.of(context)!.dhcpSettings,
subtitle: AppLocalizations.of(context)!.dhcpSettingsDescription,
thisItem: 4,
screenToNavigate: const DhcpScreen(),
twoColumns: widget.twoColumns,
),
_SettingsTile(
icon: Icons.dns_rounded,
title: AppLocalizations.of(context)!.dnsSettings,
subtitle: AppLocalizations.of(context)!.dnsSettingsDescription,
thisItem: 5,
screenToNavigate: DnsSettings(
splitView: widget.twoColumns,
),
twoColumns: widget.twoColumns,
),
_SettingsTile(
icon: Icons.security_rounded,
title: AppLocalizations.of(context)!.encryptionSettings,
subtitle: AppLocalizations.of(context)!.encryptionSettingsDescription,
thisItem: 6,
screenToNavigate: const EncryptionSettings(),
twoColumns: widget.twoColumns,
),
_SettingsTile(
icon: Icons.route_rounded,
title: AppLocalizations.of(context)!.dnsRewrites,
subtitle: AppLocalizations.of(context)!.dnsRewritesDescription,
thisItem: 7,
screenToNavigate: const DnsRewritesScreen(),
twoColumns: widget.twoColumns,
),
if (serversProvider.updateAvailable.data != null) _SettingsTile(
icon: Icons.system_update_rounded,
title: AppLocalizations.of(context)!.updates,
subtitle: AppLocalizations.of(context)!.updatesDescription,
trailing: serversProvider.updateAvailable.data != null &&
serversProvider.updateAvailable.data!.canAutoupdate == true
? Container(
width: 10,
height: 10,
margin: const EdgeInsets.only(right: 12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Colors.red
),
)
: null,
thisItem: 8,
screenToNavigate: const UpdateScreen(),
twoColumns: widget.twoColumns,
),
_SettingsTile(
icon: Icons.info_rounded,
title: AppLocalizations.of(context)!.serverInformation,
subtitle: AppLocalizations.of(context)!.serverInformationDescription,
thisItem: 9,
screenToNavigate: const ServerInformation(),
twoColumns: widget.twoColumns,
),
],
SectionLabel(label: AppLocalizations.of(context)!.appSettings),
_SettingsTile(
icon: Icons.palette_rounded,
title: AppLocalizations.of(context)!.customization,
subtitle: AppLocalizations.of(context)!.customizationDescription,
thisItem: 10,
screenToNavigate: const Customization(),
twoColumns: widget.twoColumns,
),
_SettingsTile(
icon: Icons.security_rounded,
title: AppLocalizations.of(context)!.encryptionSettings,
subtitle: AppLocalizations.of(context)!.encryptionSettingsDescription,
thisItem: 4,
screenToNavigate: const EncryptionSettings(),
icon: Icons.storage_rounded,
title: AppLocalizations.of(context)!.servers,
subtitle: serversProvider.selectedServer != null
? statusProvider.serverStatus != null
? "${AppLocalizations.of(context)!.connectedTo} ${serversProvider.selectedServer!.name}"
: "${AppLocalizations.of(context)!.selectedServer} ${serversProvider.selectedServer!.name}"
: AppLocalizations.of(context)!.noServerSelected,
thisItem: 11,
screenToNavigate: const Servers(),
twoColumns: widget.twoColumns,
),
_SettingsTile(
icon: Icons.route_rounded,
title: AppLocalizations.of(context)!.dnsRewrites,
subtitle: AppLocalizations.of(context)!.dnsRewritesDescription,
thisItem: 5,
screenToNavigate: const DnsRewritesScreen(),
twoColumns: widget.twoColumns,
),
if (serversProvider.updateAvailable.data != null) _SettingsTile(
icon: Icons.system_update_rounded,
title: AppLocalizations.of(context)!.updates,
subtitle: AppLocalizations.of(context)!.updatesDescription,
trailing: serversProvider.updateAvailable.data != null &&
serversProvider.updateAvailable.data!.canAutoupdate == true
? Container(
width: 10,
height: 10,
margin: const EdgeInsets.only(right: 12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Colors.red
),
)
: null,
thisItem: 6,
screenToNavigate: const UpdateScreen(),
icon: Icons.settings,
title: AppLocalizations.of(context)!.generalSettings,
subtitle: AppLocalizations.of(context)!.generalSettingsDescription,
thisItem: 12,
screenToNavigate: GeneralSettings(splitView: widget.twoColumns),
twoColumns: widget.twoColumns,
),
_SettingsTile(
icon: Icons.info_rounded,
title: AppLocalizations.of(context)!.serverInformation,
subtitle: AppLocalizations.of(context)!.serverInformationDescription,
thisItem: 7,
screenToNavigate: const ServerInformation(),
icon: Icons.build_outlined,
title: AppLocalizations.of(context)!.advancedSettings,
subtitle: AppLocalizations.of(context)!.advancedSetupDescription,
thisItem: 13,
screenToNavigate: const AdvancedSettings(),
twoColumns: widget.twoColumns,
),
SectionLabel(label: AppLocalizations.of(context)!.aboutApp),
CustomListTile(
title: AppLocalizations.of(context)!.appVersion,
subtitle: appConfigProvider.getAppInfo!.version,
),
CustomListTile(
title: AppLocalizations.of(context)!.createdBy,
subtitle: Strings.createdBy,
),
Padding(
padding: const EdgeInsets.all(15),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
if (Platform.isAndroid) IconButton(
onPressed: () => openUrl(Urls.playStore),
icon: SvgPicture.asset(
'assets/resources/google-play.svg',
color: Theme.of(context).colorScheme.onSurfaceVariant,
width: 30,
height: 30,
),
tooltip: AppLocalizations.of(context)!.visitGooglePlay,
),
IconButton(
onPressed: () => openUrl(Urls.gitHub),
icon: SvgPicture.asset(
'assets/resources/github.svg',
color: Theme.of(context).colorScheme.onSurfaceVariant,
width: 30,
height: 30,
),
tooltip: AppLocalizations.of(context)!.gitHub,
),
],
),
)
],
SectionLabel(label: AppLocalizations.of(context)!.appSettings),
_SettingsTile(
icon: Icons.palette_rounded,
title: AppLocalizations.of(context)!.customization,
subtitle: AppLocalizations.of(context)!.customizationDescription,
thisItem: 8,
screenToNavigate: const Customization(),
twoColumns: widget.twoColumns,
),
_SettingsTile(
icon: Icons.storage_rounded,
title: AppLocalizations.of(context)!.servers,
subtitle: serversProvider.selectedServer != null
? statusProvider.serverStatus != null
? "${AppLocalizations.of(context)!.connectedTo} ${serversProvider.selectedServer!.name}"
: "${AppLocalizations.of(context)!.selectedServer} ${serversProvider.selectedServer!.name}"
: AppLocalizations.of(context)!.noServerSelected,
thisItem: 9,
screenToNavigate: const Servers(),
twoColumns: widget.twoColumns,
),
_SettingsTile(
icon: Icons.settings,
title: AppLocalizations.of(context)!.generalSettings,
subtitle: AppLocalizations.of(context)!.generalSettingsDescription,
thisItem: 10,
screenToNavigate: GeneralSettings(splitView: widget.twoColumns),
twoColumns: widget.twoColumns,
),
_SettingsTile(
icon: Icons.build_outlined,
title: AppLocalizations.of(context)!.advancedSettings,
subtitle: AppLocalizations.of(context)!.advancedSetupDescription,
thisItem: 11,
screenToNavigate: const AdvancedSettings(),
twoColumns: widget.twoColumns,
),
SectionLabel(label: AppLocalizations.of(context)!.aboutApp),
CustomListTile(
title: AppLocalizations.of(context)!.appVersion,
subtitle: appConfigProvider.getAppInfo!.version,
),
CustomListTile(
title: AppLocalizations.of(context)!.createdBy,
subtitle: Strings.createdBy,
),
Padding(
padding: const EdgeInsets.all(15),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
if (Platform.isAndroid) IconButton(
onPressed: () => openUrl(Urls.playStore),
icon: SvgPicture.asset(
'assets/resources/google-play.svg',
color: Theme.of(context).colorScheme.onSurfaceVariant,
width: 30,
height: 30,
),
tooltip: AppLocalizations.of(context)!.visitGooglePlay,
),
IconButton(
onPressed: () => openUrl(Urls.gitHub),
icon: SvgPicture.asset(
'assets/resources/github.svg',
color: Theme.of(context).colorScheme.onSurfaceVariant,
width: 30,
height: 30,
),
tooltip: AppLocalizations.of(context)!.gitHub,
),
],
),
)
],
)
],
)
)
],
)
),
),
),
)
)
),
);
}
}

View file

@ -0,0 +1,338 @@
import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/settings/logs_settings/logs_settings.dart';
import 'package:adguard_home_manager/widgets/load_status_widgets.dart';
import 'package:adguard_home_manager/widgets/section_label.dart';
import 'package:adguard_home_manager/widgets/master_switch.dart';
import 'package:adguard_home_manager/classes/process_modal.dart';
import 'package:adguard_home_manager/functions/snackbar.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/models/statistics_config.dart';
import 'package:adguard_home_manager/constants/enums.dart';
class StatisticsSettings extends StatefulWidget {
const StatisticsSettings({super.key});
@override
State<StatisticsSettings> createState() => _StatisticsSettingsState();
}
class _StatisticsSettingsState extends State<StatisticsSettings> {
final Uuid uuid = const Uuid();
LoadStatus _loadStatus = LoadStatus.loading;
bool _generalSwitch = false;
final List<String> _retentionItems = [
"custom",
"86400000",
"604800000",
"2592000000",
"7776000000"
];
final _customTimeController = TextEditingController();
String? _customTimeError;
String? _retentionTime;
List<DomainListItemController> _ignoredDomainsControllers = [];
void loadData() async {
final serversProvider = Provider.of<ServersProvider>(context, listen: false);
final result = await serversProvider.apiClient2!.getStatisticsConfig();
if (!mounted) return;
if (result.successful == true) {
final data = result.content as StatisticsConfig;
setState(() {
_generalSwitch = data.enabled ?? false;
if (_retentionItems.contains(data.interval.toString())) {
_retentionTime = data.interval.toString();
}
else if (data.interval != null) {
_retentionTime = "custom";
_customTimeController.text = Duration(milliseconds: data.interval!).inHours.toString();
}
if (data.ignored != null) {
_ignoredDomainsControllers = data.ignored!.map((e) => DomainListItemController(
id: uuid.v4(),
controller: TextEditingController(text: e),
error: false
)).toList();
}
_loadStatus = LoadStatus.loaded;
});
}
else {
setState(() => _loadStatus = LoadStatus.error);
}
}
@override
void initState() {
loadData();
super.initState();
}
@override
Widget build(BuildContext context) {
final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final List<String> dropdownItemTranslation = [
AppLocalizations.of(context)!.custom,
AppLocalizations.of(context)!.hours24,
AppLocalizations.of(context)!.days7,
AppLocalizations.of(context)!.days30,
AppLocalizations.of(context)!.days90,
];
void validateDomain(String value, String id) {
final domainRegex = RegExp(r'^([a-z0-9|-]+\.)*[a-z0-9|-]+\.[a-z]+$');
bool error = false;
if (domainRegex.hasMatch(value)) {
error = false;
}
else {
error = true;
}
setState(() {
_ignoredDomainsControllers = _ignoredDomainsControllers.map((entry) {
if (entry.id != id) return entry;
return DomainListItemController(
id: id,
controller: entry.controller,
error: error
);
}).toList();
});
}
void validateCustomTime(String v) {
try {
final regex = RegExp(r'^\d+$');
final parsed = int.parse(v);
if (!regex.hasMatch(v)) {
setState(() => _customTimeError = AppLocalizations.of(context)!.invalidTime);
}
else if (parsed < 1) {
setState(() => _customTimeError = AppLocalizations.of(context)!.notLess1Hour);
}
else {
setState(() => _customTimeError = null);
}
} catch (_) {
setState(() => _customTimeError = AppLocalizations.of(context)!.invalidTime);
}
}
void updateConfig() async {
ProcessModal processModal = ProcessModal();
processModal.open(AppLocalizations.of(context)!.updatingSettings);
final result = await serversProvider.apiClient2!.updateStatisticsSettings(
body: {
"enabled": _generalSwitch,
"interval": _retentionTime == "custom"
? Duration(hours: int.parse(_customTimeController.text)).inMilliseconds
: int.parse(_retentionTime!),
"ignored": _ignoredDomainsControllers.map((e) => e.controller.text).toList()
}
);
processModal.close();
if (!mounted) return;
if (result.successful == true) {
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.logsConfigUpdated,
color: Colors.green
);
}
else {
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.logsConfigNotUpdated,
color: Colors.red
);
}
}
final validValues = _ignoredDomainsControllers.where(
(d) => d.controller.text == "" || d.error == true
).isEmpty &&
(_retentionTime != "custom" ||
(_retentionTime == "custom" && _customTimeController.text != "" && _customTimeError == null));
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.statisticsSettings),
actions: [
IconButton(
onPressed: validValues ? () => updateConfig() : null,
icon: const Icon(Icons.save_rounded),
tooltip: AppLocalizations.of(context)!.save,
),
const SizedBox(width: 8)
],
),
body: Builder(
builder: (context) {
switch (_loadStatus) {
case LoadStatus.loading:
return LoadingData(text: AppLocalizations.of(context)!.loadingStatisticsSettings);
case LoadStatus.loaded:
return ListView(
children: [
const SizedBox(height: 8),
MasterSwitch(
label: AppLocalizations.of(context)!.enableLog,
value: _generalSwitch,
onChange: (v) => setState(() => _generalSwitch = v)
),
const SizedBox(height: 24),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: DropdownButtonFormField(
items: _retentionItems.asMap().entries.map((item) => DropdownMenuItem(
value: item.value,
child: Text(dropdownItemTranslation[item.key]),
)).toList(),
value: _retentionTime,
onChanged: (value) => setState(() {
if (value != null && value != "custom") {
_customTimeError = null;
_customTimeController.text = "";
};
_retentionTime = value;
}),
decoration: InputDecoration(
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10)
)
),
label: Text(AppLocalizations.of(context)!.retentionTime)
),
borderRadius: BorderRadius.circular(20),
),
),
if (_retentionTime == "custom") Padding(
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 24
),
child: TextFormField(
controller: _customTimeController,
onChanged: validateCustomTime,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.schedule_rounded),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10)
)
),
labelText: AppLocalizations.of(context)!.customTimeInHours,
errorText: _customTimeError
),
keyboardType: TextInputType.number,
),
),
Padding(
padding: const EdgeInsets.only(top: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SectionLabel(
label: AppLocalizations.of(context)!.ignoredDomains,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 0),
),
Padding(
padding: const EdgeInsets.only(right: 6),
child: IconButton(
onPressed: () => setState(() => _ignoredDomainsControllers = [
..._ignoredDomainsControllers,
DomainListItemController(
id: uuid.v4(),
controller: TextEditingController(),
error: false
),
]),
icon: const Icon(Icons.add),
tooltip: AppLocalizations.of(context)!.addDomain,
),
)
],
),
),
if (_ignoredDomainsControllers.isNotEmpty) ..._ignoredDomainsControllers.map((controller) => Padding(
padding: const EdgeInsets.only(
top: 12, bottom: 12, left: 16, right: 6
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: TextFormField(
controller: controller.controller,
onChanged: (v) => validateDomain(v, controller.id),
decoration: InputDecoration(
prefixIcon: const Icon(Icons.link_rounded),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10)
)
),
labelText: AppLocalizations.of(context)!.domain,
errorText: controller.error
? AppLocalizations.of(context)!.invalidDomain
: null
),
),
),
const SizedBox(width: 12),
Padding(
padding: controller.error
? const EdgeInsets.only(bottom: 24)
: const EdgeInsets.all(0),
child: IconButton(
onPressed: () => setState(() => _ignoredDomainsControllers = _ignoredDomainsControllers.where((e) => e.id != controller.id).toList()),
icon: const Icon(Icons.remove_circle_outline_outlined),
tooltip: AppLocalizations.of(context)!.removeDomain,
),
)
],
),
)),
if (_ignoredDomainsControllers.isEmpty) Container(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Text(
AppLocalizations.of(context)!.noIgnoredDomainsAdded,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
],
);
case LoadStatus.error:
return ErrorLoadData(text: AppLocalizations.of(context)!.statisticsSettingsLoadError);
default:
return const SizedBox();
}
},
),
);
}
}

View file

@ -3,6 +3,8 @@ import 'dart:convert';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:adguard_home_manager/models/blocked_services.dart';
import 'package:adguard_home_manager/models/querylog_config.dart';
import 'package:adguard_home_manager/models/statistics_config.dart';
import 'package:adguard_home_manager/models/dns_info.dart';
import 'package:adguard_home_manager/models/encryption.dart';
import 'package:adguard_home_manager/models/dhcp.dart';
@ -489,9 +491,6 @@ class ApiClientV2 {
return ApiResponse(
successful: true,
content: DhcpModel(
dhcpAvailable: jsonDecode(results[1].body!)['message'] != null
? false
: true,
networkInterfaces: interfaces,
dhcpStatus: jsonDecode(results[1].body!)['message'] != null
? null
@ -627,10 +626,19 @@ class ApiClientV2 {
Future<ApiResponse> getQueryLogInfo() async {
final result = await HttpRequestClient.get(urlPath: '/querylog/config', server: server);
if (result.successful) {
return ApiResponse(
successful: true,
content: jsonDecode(result.body!)
);
try {
return ApiResponse(
successful: true,
content: QueryLogConfig.fromJson(jsonDecode(result.body!))
);
} catch (e, stackTrace) {
Sentry.captureException(
e,
stackTrace: stackTrace,
hint: Hint.withMap({ "statusCode": result.statusCode.toString() })
);
return const ApiResponse(successful: false);
}
}
else {
return const ApiResponse(successful: false);
@ -649,7 +657,7 @@ class ApiClientV2 {
}
Future<ApiResponse> clearLogs() async {
final result = await HttpRequestClient.put(
final result = await HttpRequestClient.post(
urlPath: '/querylog_clear',
server: server,
body: {},
@ -874,4 +882,38 @@ class ApiClientV2 {
content: result.body != null ? jsonDecode(result.body!) : null
);
}
Future<ApiResponse> getStatisticsConfig() async {
final result = await HttpRequestClient.get(urlPath: '/stats/config', server: server);
if (result.successful) {
try {
return ApiResponse(
successful: true,
content: StatisticsConfig.fromJson(jsonDecode(result.body!))
);
} catch (e, stackTrace) {
Sentry.captureException(
e,
stackTrace: stackTrace,
hint: Hint.withMap({ "statusCode": result.statusCode.toString() })
);
return const ApiResponse(successful: false);
}
}
else {
return const ApiResponse(successful: false);
}
}
Future<ApiResponse> updateStatisticsSettings({
required Map<String, dynamic> body
}) async {
final result = await HttpRequestClient.put(
urlPath: '/stats/config/update',
server: server,
body: body
);
print(result.body);
return ApiResponse(successful: result.successful);
}
}

View file

@ -2,6 +2,8 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:adguard_home_manager/classes/http_client.dart';
import 'package:adguard_home_manager/models/server.dart';
@ -52,7 +54,8 @@ class ServerAuth {
return AuthStatus.timeoutException;
} on HandshakeException {
return AuthStatus.handshakeException;
} catch (e) {
} catch (e, stackTrace) {
Sentry.captureException(e, stackTrace: stackTrace);
return AuthStatus.unknown;
}
}
@ -75,7 +78,8 @@ class ServerAuth {
return AuthStatus.timeoutException;
} on HandshakeException {
return AuthStatus.handshakeException;
} catch (e) {
} catch (e, stackTrace) {
Sentry.captureException(e, stackTrace: stackTrace);
return AuthStatus.unknown;
}
}

View file

@ -127,9 +127,19 @@ Future<Map<String, dynamic>> loadDb(bool acceptsDynamicTheme) async {
});
}
Future upgradeDbToV11(Database db) async {
await db.execute("ALTER TABLE appConfig DROP COLUMN showTopItemsChart");
await db.transaction((txn) async{
await txn.rawQuery(
'SELECT * FROM appConfig',
);
});
}
Database db = await openDatabase(
'adguard_home_manager.db',
version: 10,
version: 11,
onCreate: (Database db, int version) async {
await db.execute(
"""
@ -165,8 +175,7 @@ Future<Map<String, dynamic>> loadDb(bool acceptsDynamicTheme) async {
combinedChart NUMERIC,
doNotRememberVersion TEXT,
hideServerAddress NUMERIC,
homeTopItemsOrder TEXT,
showTopItemsChart NUMERIC
homeTopItemsOrder TEXT
)
"""
);
@ -185,8 +194,7 @@ Future<Map<String, dynamic>> loadDb(bool acceptsDynamicTheme) async {
showIpLogs,
combinedChart,
hideServerAddress,
homeTopItemsOrder,
showTopItemsChart
homeTopItemsOrder
)
VALUES (
0,
@ -199,8 +207,7 @@ Future<Map<String, dynamic>> loadDb(bool acceptsDynamicTheme) async {
0,
0,
0,
'$homeTopItemsDefaultOrderString',
1
'$homeTopItemsDefaultOrderString'
)
"""
);
@ -216,6 +223,7 @@ Future<Map<String, dynamic>> loadDb(bool acceptsDynamicTheme) async {
await upgradeDbToV8(db);
await upgradeDbToV9(db);
await upgradeDbToV10(db);
await upgradeDbToV11(db);
}
if (oldVersion == 2) {
await upgradeDbToV3(db);
@ -226,6 +234,7 @@ Future<Map<String, dynamic>> loadDb(bool acceptsDynamicTheme) async {
await upgradeDbToV8(db);
await upgradeDbToV9(db);
await upgradeDbToV10(db);
await upgradeDbToV11(db);
}
if (oldVersion == 3) {
await upgradeDbToV4(db);
@ -235,6 +244,7 @@ Future<Map<String, dynamic>> loadDb(bool acceptsDynamicTheme) async {
await upgradeDbToV8(db);
await upgradeDbToV9(db);
await upgradeDbToV10(db);
await upgradeDbToV11(db);
}
if (oldVersion == 4) {
await upgradeDbToV5(db);
@ -243,6 +253,7 @@ Future<Map<String, dynamic>> loadDb(bool acceptsDynamicTheme) async {
await upgradeDbToV8(db);
await upgradeDbToV9(db);
await upgradeDbToV10(db);
await upgradeDbToV11(db);
}
if (oldVersion == 5) {
await upgradeDbToV6(db);
@ -250,24 +261,32 @@ Future<Map<String, dynamic>> loadDb(bool acceptsDynamicTheme) async {
await upgradeDbToV8(db);
await upgradeDbToV9(db);
await upgradeDbToV10(db);
await upgradeDbToV11(db);
}
if (oldVersion == 6) {
await upgradeDbToV7(db);
await upgradeDbToV8(db);
await upgradeDbToV9(db);
await upgradeDbToV10(db);
await upgradeDbToV11(db);
}
if (oldVersion == 7) {
await upgradeDbToV8(db);
await upgradeDbToV9(db);
await upgradeDbToV10(db);
await upgradeDbToV11(db);
}
if (oldVersion == 8) {
await upgradeDbToV9(db);
await upgradeDbToV10(db);
await upgradeDbToV11(db);
}
if (oldVersion == 9) {
await upgradeDbToV10(db);
await upgradeDbToV11(db);
}
if (oldVersion == 10) {
await upgradeDbToV11(db);
}
},
onOpen: (Database db) async {

View file

@ -1352,7 +1352,6 @@ class ApiClient {
return {
'result': 'success',
'data': DhcpModel(
dhcpAvailable: true,
networkInterfaces: interfaces,
dhcpStatus: DhcpStatus.fromJson(jsonDecode(result[1]['body']))
)

View file

@ -7,11 +7,11 @@ class CustomPieChart extends StatelessWidget {
final Duration? animationDuration;
const CustomPieChart({
Key? key,
super.key,
required this.data,
required this.colors,
this.animationDuration = const Duration(milliseconds: 800),
}) : super(key: key);
this.animationDuration = const Duration(milliseconds: 800),
});
@override
Widget build(BuildContext context) {

View file

@ -12,7 +12,7 @@ class CustomSettingsTile extends StatelessWidget {
final int? selectedItem;
const CustomSettingsTile({
Key? key,
super.key,
required this.title,
this.subtitle,
this.subtitleWidget,
@ -22,7 +22,7 @@ class CustomSettingsTile extends StatelessWidget {
this.padding,
required this.thisItem,
required this.selectedItem,
}) : super(key: key);
});
@override
Widget build(BuildContext context) {

View file

@ -0,0 +1,80 @@
import 'package:flutter/material.dart';
class ListBottomSheet extends StatelessWidget {
final IconData icon;
final String title;
final List<Widget> children;
const ListBottomSheet({
super.key,
required this.icon,
required this.title,
required this.children
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: DraggableScrollableSheet(
initialChildSize: 0.6,
minChildSize: 0.3,
maxChildSize: 1,
builder: (context, controller) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28),
topRight: Radius.circular(28),
),
),
child: SafeArea(
child: ListView(
controller: controller,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
margin: const EdgeInsets.all(16),
width: 36,
height: 4,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Colors.grey
),
),
],
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
children: [
Icon(
icon,
size: 24,
color: Theme.of(context).listTileTheme.iconColor
),
const SizedBox(height: 16),
Text(
title,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 24,
color: Theme.of(context).colorScheme.onSurface
),
),
],
),
),
...children
],
),
),
);
},
),
);
}
}

View file

@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
class LoadingData extends StatelessWidget {
final String text;
const LoadingData({
super.key,
required this.text,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(24),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 30),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text(
text,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 22,
color: Theme.of(context).colorScheme.onSurfaceVariant
),
),
)
],
),
),
);
}
}
class ErrorLoadData extends StatelessWidget {
final String text;
const ErrorLoadData({
super.key,
required this.text
});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(
Icons.error,
color: Colors.red,
size: 50,
),
const SizedBox(height: 30),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text(
text,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 22,
color: Theme.of(context).colorScheme.onSurfaceVariant
),
),
)
],
),
);
}
}

View file

@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
class MasterSwitch extends StatelessWidget {
final String label;
final bool value;
final void Function(bool) onChange;
final EdgeInsets? margin;
const MasterSwitch({
super.key,
required this.label,
required this.value,
required this.onChange,
this.margin
});
@override
Widget build(BuildContext context) {
return Padding(
padding: margin ?? const EdgeInsets.symmetric(horizontal: 16),
child: Material(
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(28),
child: InkWell(
onTap: () => onChange(!value),
borderRadius: BorderRadius.circular(28),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 8
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: const TextStyle(
fontSize: 18,
),
),
Switch(
value: value,
onChanged: onChange,
)
],
),
),
),
),
);
}
}

View file

@ -4,33 +4,30 @@ PODS:
- dynamic_color (0.0.2):
- FlutterMacOS
- FlutterMacOS (1.0.0)
- FMDB (2.7.5):
- FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5)
- package_info_plus (0.0.1):
- FlutterMacOS
- Sentry/HybridSDK (8.17.2):
- SentryPrivate (= 8.17.2)
- Sentry/HybridSDK (8.18.0):
- SentryPrivate (= 8.18.0)
- sentry_flutter (0.0.1):
- Flutter
- FlutterMacOS
- Sentry/HybridSDK (= 8.17.2)
- SentryPrivate (8.17.2)
- sqflite (0.0.2):
- Sentry/HybridSDK (= 8.18.0)
- SentryPrivate (8.18.0)
- sqflite (0.0.3):
- Flutter
- FlutterMacOS
- FMDB (>= 2.7.5)
- sqlite3 (3.44.0):
- sqlite3/common (= 3.44.0)
- sqlite3/common (3.44.0)
- sqlite3/fts5 (3.44.0):
- sqlite3 (3.45.0):
- sqlite3/common (= 3.45.0)
- sqlite3/common (3.45.0)
- sqlite3/fts5 (3.45.0):
- sqlite3/common
- sqlite3/perf-threadsafe (3.44.0):
- sqlite3/perf-threadsafe (3.45.0):
- sqlite3/common
- sqlite3/rtree (3.44.0):
- sqlite3/rtree (3.45.0):
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- FlutterMacOS
- sqlite3 (~> 3.44.0)
- sqlite3 (~> 3.45.0)
- sqlite3/fts5
- sqlite3/perf-threadsafe
- sqlite3/rtree
@ -45,14 +42,13 @@ DEPENDENCIES:
- FlutterMacOS (from `Flutter/ephemeral`)
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- sentry_flutter (from `Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos`)
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`)
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`)
- sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
- window_size (from `Flutter/ephemeral/.symlinks/plugins/window_size/macos`)
SPEC REPOS:
trunk:
- FMDB
- Sentry
- SentryPrivate
- sqlite3
@ -69,7 +65,7 @@ EXTERNAL SOURCES:
sentry_flutter:
:path: Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos
sqflite:
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin
sqlite3_flutter_libs:
:path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos
url_launcher_macos:
@ -81,14 +77,13 @@ SPEC CHECKSUMS:
device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f
dynamic_color: 2eaa27267de1ca20d879fbd6e01259773fb1670f
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce
Sentry: 64a9f9c3637af913adcf53deced05bbe452d1410
sentry_flutter: 57912cf425e09398bdf47f38842a1fcb9836f1be
SentryPrivate: 024c6fed507ac39ae98e6d087034160f942920d5
sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea
sqlite3: 6e2d4a4879854d0ec86b476bf3c3e30870bac273
sqlite3_flutter_libs: a25f3a0f522fdcd8fef6a4a50a3d681dd43d8dea
Sentry: 8984a4ffb2b9bd2894d74fb36e6f5833865bc18e
sentry_flutter: c87a0556eeb6cbf7f9f924d30e878bdedf22d364
SentryPrivate: 2f0c9ba4c3fc993f70eab6ca95673509561e0085
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
sqlite3: f307b6291c4db7b5086c38d6237446b98a738581
sqlite3_flutter_libs: 6b9913d8fbb718e5ebf23658aa6934a0fb509c0f
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95
window_size: 339dafa0b27a95a62a843042038fa6c3c48de195

View file

@ -213,15 +213,55 @@ packages:
dependency: "direct main"
description:
name: fl_chart
sha256: fe6fec7d85975a99c73b9515a69a6e291364accfa0e4a5b3ce6de814d74b9a1c
sha256: b5e2b0f13d93f8c532b5a2786bfb44580de1f50b927bf95813fa1af617e9caf8
url: "https://pub.dev"
source: hosted
version: "0.66.0"
version: "0.66.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_custom_tabs:
dependency: "direct main"
description:
name: flutter_custom_tabs
sha256: "961fe962ae55e9e41097c34e68c6d7d2de10e9a71034f83834919e31a891e728"
url: "https://pub.dev"
source: hosted
version: "2.0.0+1"
flutter_custom_tabs_android:
dependency: transitive
description:
name: flutter_custom_tabs_android
sha256: "5701a3e38117dfc59e5fa9e84a29eea9203e0062de7be56d1f775845c34456d9"
url: "https://pub.dev"
source: hosted
version: "2.0.0+1"
flutter_custom_tabs_ios:
dependency: transitive
description:
name: flutter_custom_tabs_ios
sha256: b29f687f7f366159d37347f888369fcd1259adac8ca6c7f07d356a80b091e08a
url: "https://pub.dev"
source: hosted
version: "2.0.0"
flutter_custom_tabs_platform_interface:
dependency: transitive
description:
name: flutter_custom_tabs_platform_interface
sha256: "13531a743486695d87b462b151801e11476b1b5a15274caec092420cc1943064"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
flutter_custom_tabs_web:
dependency: transitive
description:
name: flutter_custom_tabs_web
sha256: "3114b511d3942ed42c502cc57ad47eaef9622ac06dd036e2b21b19d8876f6498"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
flutter_displaymode:
dependency: "direct main"
description:
@ -279,10 +319,10 @@ packages:
dependency: "direct dev"
description:
name: flutter_native_splash
sha256: "9cdb5d9665dab5d098dc50feab74301c2c228cd02ca25c9b546ab572cebcd6af"
sha256: "558f10070f03ee71f850a78f7136ab239a67636a294a44a06b6b7345178edb1e"
url: "https://pub.dev"
source: hosted
version: "2.3.9"
version: "2.3.10"
flutter_reorderable_list:
dependency: "direct main"
description:
@ -313,14 +353,6 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_web_browser:
dependency: "direct main"
description:
name: flutter_web_browser
sha256: a5564b736253f745e147b8c4eff86de436324d081974cc1f16bff881134a400f
url: "https://pub.dev"
source: hosted
version: "0.17.1"
flutter_web_plugins:
dependency: transitive
description: flutter
@ -338,10 +370,10 @@ packages:
dependency: "direct main"
description:
name: http
sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139
sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba
url: "https://pub.dev"
source: hosted
version: "1.1.2"
version: "1.2.0"
http_parser:
dependency: transitive
description:
@ -402,10 +434,10 @@ packages:
dependency: "direct main"
description:
name: markdown
sha256: acf35edccc0463a9d7384e437c015a3535772e09714cf60e07eeef3a15870dcd
sha256: "1b134d9f8ff2da15cb298efe6cd8b7d2a78958c1b00384ebcbdf13fe340a6c90"
url: "https://pub.dev"
source: hosted
version: "7.1.1"
version: "7.2.1"
matcher:
dependency: transitive
description:
@ -530,18 +562,18 @@ packages:
dependency: transitive
description:
name: sentry
sha256: "89e426587b0879e53c46a0aae0eb312696d9d2d803ba14b252a65cc24b1416a2"
sha256: "5686ed515bb620dc52b4ae99a6586fe720d443591183cf1f620ec5d1f0eec100"
url: "https://pub.dev"
source: hosted
version: "7.14.0"
version: "7.15.0"
sentry_flutter:
dependency: "direct main"
description:
name: sentry_flutter
sha256: fd089ee4e75a927be037c56815a0a54af5a519f52b803a5ffecb589bb36e2401
sha256: "505dec3b6810562785d2c34ae871c73ff2cba6cf436c32c188f0464df226ba8f"
url: "https://pub.dev"
source: hosted
version: "7.14.0"
version: "7.15.0"
sky_engine:
dependency: transitive
description: flutter
@ -567,18 +599,18 @@ packages:
dependency: "direct main"
description:
name: sqflite
sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a"
sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6
url: "https://pub.dev"
source: hosted
version: "2.3.0"
version: "2.3.2"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
sha256: "76db4d324c8cbb16ca5b60ad2f3d25cec953107c93ae65aafa480d3e6fb69f14"
sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5"
url: "https://pub.dev"
source: hosted
version: "2.5.2-1"
version: "2.5.3"
sqflite_common_ffi:
dependency: "direct main"
description:
@ -599,10 +631,10 @@ packages:
dependency: "direct main"
description:
name: sqlite3_flutter_libs
sha256: "3e3583b77cf888a68eae2e49ee4f025f66b86623ef0d83c297c8d903daa14871"
sha256: "90963b515721d6a71e96f438175cf43c979493ed14822860a300b69694c74eb6"
url: "https://pub.dev"
source: hosted
version: "0.5.18"
version: "0.5.19+1"
stack_trace:
dependency: transitive
description:
@ -659,6 +691,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.1"
timezone:
dependency: "direct main"
description:
name: timezone
sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0"
url: "https://pub.dev"
source: hosted
version: "0.9.2"
typed_data:
dependency: transitive
description:
@ -679,10 +719,10 @@ packages:
dependency: "direct main"
description:
name: url_launcher
sha256: d25bb0ca00432a5e1ee40e69c36c85863addf7cc45e433769d61bed3fe81fd96
sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c
url: "https://pub.dev"
source: hosted
version: "6.2.3"
version: "6.2.4"
url_launcher_android:
dependency: transitive
description:

View file

@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 2.14.1+120
version: 2.15.0+125
environment:
sdk: '>=2.18.1 <3.0.0'
@ -49,7 +49,6 @@ dependencies:
uuid: ^4.2.1
expandable: ^5.0.1
fl_chart: ^0.66.0
flutter_web_browser: ^0.17.1
flutter_svg: ^2.0.9
percent_indicator: ^4.2.3
store_checker: ^1.4.0
@ -67,7 +66,6 @@ dependencies:
git:
url: https://github.com/JGeek00/flutter_split_view
ref: master-alt
url_launcher: ^6.2.2
contextmenu: ^3.0.0
async: ^2.11.0
sentry_flutter: ^7.13.2
@ -76,6 +74,9 @@ dependencies:
pie_chart: ^5.4.0
segmented_button_slide: ^1.0.4
http: ^1.1.2
timezone: ^0.9.2
flutter_custom_tabs: ^2.0.0+1
url_launcher: ^6.2.4
dev_dependencies:
flutter_test: