Merge branch 'beta'
|
@ -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>
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 21 KiB |
BIN
android/app/src/main/res/drawable-night-xxxhdpi/splash.png
Normal file → Executable file
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 13 KiB |
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
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 25 KiB |
|
@ -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>
|
|
@ -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>
|
Before Width: | Height: | Size: 3.6 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
Normal file
After Width: | Height: | Size: 874 B |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 2.4 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
Normal file
After Width: | Height: | Size: 650 B |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 5.1 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 1.1 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 8.1 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 1.6 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 12 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 7.3 KiB |
After Width: | Height: | Size: 2.1 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 7.3 KiB |
|
@ -1,4 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#ffffff</color>
|
||||
</resources>
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|
|
@ -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()
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -21,6 +21,6 @@
|
|||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>11.0</string>
|
||||
<string>12.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -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
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
);
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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
|
@ -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-порт."
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
|
|
27
lib/models/querylog_config.dart
Normal 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,
|
||||
};
|
||||
}
|
|
@ -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']
|
||||
);
|
||||
}
|
23
lib/models/statistics_config.dart
Normal 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,
|
||||
};
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
209
lib/screens/clients/client/blocking_schedule.dart
Normal 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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
325
lib/screens/clients/client/blocking_schedule_modal.dart
Normal 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)
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -39,6 +39,7 @@ class AddFiltersButton extends StatelessWidget {
|
|||
|
||||
processModal.close();
|
||||
|
||||
if (!context.mounted) return;
|
||||
if (result == true) {
|
||||
showSnacbkar(
|
||||
appConfigProvider: appConfigProvider,
|
||||
|
|
|
@ -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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -151,7 +151,8 @@ class HomeAppBar extends StatelessWidget {
|
|||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -227,6 +227,8 @@ class _HomeState extends State<Home> {
|
|||
),
|
||||
|
||||
TopItemsLists(order: appConfigProvider.homeTopItemsOrder),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
if (statusProvider.loadStatus == LoadStatus.error) SliverFillRemaining(
|
||||
|
|
|
@ -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
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
187
lib/screens/settings/logs_settings/config_widgets.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
213
lib/screens/settings/logs_settings/logs_settings.dart
Normal 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();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
],
|
||||
)
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -1352,7 +1352,6 @@ class ApiClient {
|
|||
return {
|
||||
'result': 'success',
|
||||
'data': DhcpModel(
|
||||
dhcpAvailable: true,
|
||||
networkInterfaces: interfaces,
|
||||
dhcpStatus: DhcpStatus.fromJson(jsonDecode(result[1]['body']))
|
||||
)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
80
lib/widgets/list_bottom_sheet.dart
Normal 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
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
76
lib/widgets/load_status_widgets.dart
Normal 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
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
52
lib/widgets/master_switch.dart
Normal 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,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
96
pubspec.lock
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|