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)
|
- [dynamic color](https://pub.dev/packages/dynamic_color)
|
||||||
- [device info](https://pub.dev/packages/device_info)
|
- [device info](https://pub.dev/packages/device_info)
|
||||||
- [fl chart](https://pub.dev/packages/fl_chart)
|
- [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)
|
- [flutter svg](https://pub.dev/packages/flutter_svg)
|
||||||
- [percent indicator](https://pub.dev/packages/percent_indicator)
|
- [percent indicator](https://pub.dev/packages/percent_indicator)
|
||||||
- [store checker](https://pub.dev/packages/store_checker)
|
- [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)
|
- [flutter reorderable list](https://pub.dev/packages/flutter_reorderable_list)
|
||||||
- [pie chart](https://pub.dev/packages/pie_chart)
|
- [pie chart](https://pub.dev/packages/pie_chart)
|
||||||
- [segmented button slide](https://pub.dev/packages/segmented_button_slide)
|
- [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>
|
<br>
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
|
plugins {
|
||||||
|
id "com.android.application"
|
||||||
|
id "kotlin-android"
|
||||||
|
id "dev.flutter.flutter-gradle-plugin"
|
||||||
|
}
|
||||||
|
|
||||||
def localProperties = new Properties()
|
def localProperties = new Properties()
|
||||||
def localPropertiesFile = rootProject.file('local.properties')
|
def localPropertiesFile = rootProject.file('local.properties')
|
||||||
if (localPropertiesFile.exists()) {
|
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')
|
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||||
if (flutterVersionCode == null) {
|
if (flutterVersionCode == null) {
|
||||||
flutterVersionCode = '1'
|
flutterVersionCode = '1'
|
||||||
|
@ -21,10 +22,6 @@ if (flutterVersionName == null) {
|
||||||
flutterVersionName = '1.0'
|
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 keystoreProperties = new Properties()
|
||||||
def keystorePropertiesFile = rootProject.file('key.properties')
|
def keystorePropertiesFile = rootProject.file('key.properties')
|
||||||
if (keystorePropertiesFile.exists()) {
|
if (keystorePropertiesFile.exists()) {
|
||||||
|
@ -80,5 +77,5 @@ flutter {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
|
||||||
<monochrome android:drawable="@mipmap/ic_launcher_monochrome" />
|
<monochrome android:drawable="@mipmap/ic_launcher_monochrome" />
|
||||||
</adaptive-icon>
|
</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 {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
|
|
|
@ -1,11 +1,26 @@
|
||||||
include ':app'
|
pluginManagement {
|
||||||
|
def flutterSdkPath = {
|
||||||
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
|
|
||||||
def properties = new Properties()
|
def properties = new Properties()
|
||||||
|
file("local.properties").withInputStream { properties.load(it) }
|
||||||
assert localPropertiesFile.exists()
|
|
||||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
|
||||||
|
|
||||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||||
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
return flutterSdkPath
|
||||||
|
}
|
||||||
|
settings.ext.flutterSdkPath = flutterSdkPath()
|
||||||
|
|
||||||
|
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
<key>CFBundleVersion</key>
|
||||||
<string>1.0</string>
|
<string>1.0</string>
|
||||||
<key>MinimumOSVersion</key>
|
<key>MinimumOSVersion</key>
|
||||||
<string>11.0</string>
|
<string>12.0</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# Uncomment this line to define a global platform for your project
|
# 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.
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
|
67
ios/Podfile.lock
Executable file → Normal file
|
@ -2,60 +2,53 @@ PODS:
|
||||||
- device_info_plus (0.0.1):
|
- device_info_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Flutter (1.0.0)
|
- Flutter (1.0.0)
|
||||||
|
- flutter_custom_tabs_ios (2.0.0):
|
||||||
|
- Flutter
|
||||||
- flutter_native_splash (0.0.1):
|
- flutter_native_splash (0.0.1):
|
||||||
- Flutter
|
- 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):
|
- package_info_plus (0.4.5):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Sentry/HybridSDK (8.15.2):
|
- Sentry/HybridSDK (8.18.0):
|
||||||
- SentryPrivate (= 8.15.2)
|
- SentryPrivate (= 8.18.0)
|
||||||
- sentry_flutter (0.0.1):
|
- sentry_flutter (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- Sentry/HybridSDK (= 8.15.2)
|
- Sentry/HybridSDK (= 8.18.0)
|
||||||
- SentryPrivate (8.15.2)
|
- SentryPrivate (8.18.0)
|
||||||
- sqflite (0.0.3):
|
- sqflite (0.0.3):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FMDB (>= 2.7.5)
|
- FlutterMacOS
|
||||||
- sqlite3 (3.44.0):
|
- sqlite3 (3.45.0):
|
||||||
- sqlite3/common (= 3.44.0)
|
- sqlite3/common (= 3.45.0)
|
||||||
- sqlite3/common (3.44.0)
|
- sqlite3/common (3.45.0)
|
||||||
- sqlite3/fts5 (3.44.0):
|
- sqlite3/fts5 (3.45.0):
|
||||||
- sqlite3/common
|
- sqlite3/common
|
||||||
- sqlite3/perf-threadsafe (3.44.0):
|
- sqlite3/perf-threadsafe (3.45.0):
|
||||||
- sqlite3/common
|
- sqlite3/common
|
||||||
- sqlite3/rtree (3.44.0):
|
- sqlite3/rtree (3.45.0):
|
||||||
- sqlite3/common
|
- sqlite3/common
|
||||||
- sqlite3_flutter_libs (0.0.1):
|
- sqlite3_flutter_libs (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- sqlite3 (~> 3.44.0)
|
- sqlite3 (~> 3.45.0)
|
||||||
- sqlite3/fts5
|
- sqlite3/fts5
|
||||||
- sqlite3/perf-threadsafe
|
- sqlite3/perf-threadsafe
|
||||||
- sqlite3/rtree
|
- sqlite3/rtree
|
||||||
- store_checker (0.0.1):
|
- store_checker (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- url_launcher_ios (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||||
- Flutter (from `Flutter`)
|
- 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_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`)
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/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`)
|
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`)
|
||||||
- store_checker (from `.symlinks/plugins/store_checker/ios`)
|
- store_checker (from `.symlinks/plugins/store_checker/ios`)
|
||||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
trunk:
|
trunk:
|
||||||
- FMDB
|
|
||||||
- Sentry
|
- Sentry
|
||||||
- SentryPrivate
|
- SentryPrivate
|
||||||
- sqlite3
|
- sqlite3
|
||||||
|
@ -65,39 +58,35 @@ EXTERNAL SOURCES:
|
||||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||||
Flutter:
|
Flutter:
|
||||||
:path: Flutter
|
:path: Flutter
|
||||||
|
flutter_custom_tabs_ios:
|
||||||
|
:path: ".symlinks/plugins/flutter_custom_tabs_ios/ios"
|
||||||
flutter_native_splash:
|
flutter_native_splash:
|
||||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||||
flutter_web_browser:
|
|
||||||
:path: ".symlinks/plugins/flutter_web_browser/ios"
|
|
||||||
package_info_plus:
|
package_info_plus:
|
||||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||||
sentry_flutter:
|
sentry_flutter:
|
||||||
:path: ".symlinks/plugins/sentry_flutter/ios"
|
:path: ".symlinks/plugins/sentry_flutter/ios"
|
||||||
sqflite:
|
sqflite:
|
||||||
:path: ".symlinks/plugins/sqflite/ios"
|
:path: ".symlinks/plugins/sqflite/darwin"
|
||||||
sqlite3_flutter_libs:
|
sqlite3_flutter_libs:
|
||||||
:path: ".symlinks/plugins/sqlite3_flutter_libs/ios"
|
:path: ".symlinks/plugins/sqlite3_flutter_libs/ios"
|
||||||
store_checker:
|
store_checker:
|
||||||
:path: ".symlinks/plugins/store_checker/ios"
|
:path: ".symlinks/plugins/store_checker/ios"
|
||||||
url_launcher_ios:
|
|
||||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
|
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
|
||||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||||
|
flutter_custom_tabs_ios: 62439c843b2691aae516fd50119a01eb9755fff7
|
||||||
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
|
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
|
||||||
flutter_web_browser: 7bccaafbb0c5b8862afe7bcd158f15557109f61f
|
|
||||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
|
||||||
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
|
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
|
||||||
Sentry: 6f5742b4c47c17c9adcf265f6f328cf4a0ed1923
|
Sentry: 8984a4ffb2b9bd2894d74fb36e6f5833865bc18e
|
||||||
sentry_flutter: 2c309a1d4b45e59d02cfa15795705687f1e2081b
|
sentry_flutter: c87a0556eeb6cbf7f9f924d30e878bdedf22d364
|
||||||
SentryPrivate: b2f7996f37781080f04a946eb4e377ff63c64195
|
SentryPrivate: 2f0c9ba4c3fc993f70eab6ca95673509561e0085
|
||||||
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
|
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||||
sqlite3: 6e2d4a4879854d0ec86b476bf3c3e30870bac273
|
sqlite3: f307b6291c4db7b5086c38d6237446b98a738581
|
||||||
sqlite3_flutter_libs: eb769059df0356dc52ddda040f09cacc9391a7cf
|
sqlite3_flutter_libs: aeb4d37509853dfa79d9b59386a2dac5dd079428
|
||||||
store_checker: 359c5051d9ec30ff0a8fa39eb5ec9df021bb745d
|
store_checker: 359c5051d9ec30ff0a8fa39eb5ec9df021bb745d
|
||||||
url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b
|
|
||||||
|
|
||||||
PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3
|
PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011
|
||||||
|
|
||||||
COCOAPODS: 1.14.3
|
COCOAPODS: 1.14.3
|
||||||
|
|
|
@ -1,30 +1,33 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter_web_browser/flutter_web_browser.dart';
|
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart' as flutter_custom_tabs;
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart' as url_launcher;
|
||||||
|
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||||
|
|
||||||
void openUrl(String url) async {
|
void openUrl(String url) async {
|
||||||
if (Platform.isAndroid || Platform.isIOS) {
|
if (Platform.isAndroid || Platform.isIOS) {
|
||||||
FlutterWebBrowser.openWebPage(
|
try {
|
||||||
url: url,
|
await flutter_custom_tabs.launchUrl(
|
||||||
customTabsOptions: const CustomTabsOptions(
|
Uri.parse(url),
|
||||||
instantAppsEnabled: true,
|
customTabsOptions: const flutter_custom_tabs.CustomTabsOptions(
|
||||||
|
shareState: flutter_custom_tabs.CustomTabsShareState.browserDefault,
|
||||||
|
urlBarHidingEnabled: true,
|
||||||
showTitle: true,
|
showTitle: true,
|
||||||
urlBarHidingEnabled: false,
|
|
||||||
),
|
),
|
||||||
safariVCOptions: const SafariViewControllerOptions(
|
safariVCOptions: const flutter_custom_tabs.SafariViewControllerOptions(
|
||||||
barCollapsingEnabled: true,
|
barCollapsingEnabled: true,
|
||||||
dismissButtonStyle: SafariViewControllerDismissButtonStyle.close,
|
dismissButtonStyle: flutter_custom_tabs.SafariViewControllerDismissButtonStyle.close,
|
||||||
modalPresentationCapturesStatusBarAppearance: true,
|
),
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
Sentry.captureException(e, stackTrace: stackTrace);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
final uri = Uri.parse(url);
|
try {
|
||||||
if (await canLaunchUrl(uri)) {
|
url_launcher.launchUrl(Uri.parse(url));
|
||||||
await launchUrl(uri);
|
} catch (e, stackTrace) {
|
||||||
} else {
|
Sentry.captureException(e, stackTrace: stackTrace);
|
||||||
throw 'Could not launch $url';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -9,10 +9,12 @@ void showSnacbkar({
|
||||||
required AppConfigProvider appConfigProvider,
|
required AppConfigProvider appConfigProvider,
|
||||||
required String label,
|
required String label,
|
||||||
required Color color,
|
required Color color,
|
||||||
Color? labelColor
|
Color? labelColor,
|
||||||
|
GlobalKey<ScaffoldMessengerState>? key,
|
||||||
}) async {
|
}) async {
|
||||||
|
final GlobalKey<ScaffoldMessengerState> scaffoldKey = key ?? scaffoldMessengerKey;
|
||||||
if (appConfigProvider.showingSnackbar == true) {
|
if (appConfigProvider.showingSnackbar == true) {
|
||||||
scaffoldMessengerKey.currentState?.clearSnackBars();
|
scaffoldKey.currentState?.clearSnackBars();
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
}
|
}
|
||||||
appConfigProvider.setShowingSnackbar(true);
|
appConfigProvider.setShowingSnackbar(true);
|
||||||
|
@ -26,7 +28,7 @@ void showSnacbkar({
|
||||||
),
|
),
|
||||||
backgroundColor: color,
|
backgroundColor: color,
|
||||||
);
|
);
|
||||||
scaffoldMessengerKey.currentState?.showSnackBar(snackBar).closed.then(
|
scaffoldKey.currentState?.showSnackBar(snackBar).closed.then(
|
||||||
(value) => appConfigProvider.setShowingSnackbar(false)
|
(value) => appConfigProvider.setShowingSnackbar(false)
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -24,7 +24,7 @@
|
||||||
"invalidUsernamePassword": "Invalid username or password",
|
"invalidUsernamePassword": "Invalid username or password",
|
||||||
"tooManyAttempts": "Too many attempts. Try again later.",
|
"tooManyAttempts": "Too many attempts. Try again later.",
|
||||||
"cantReachServer": "Can't reach server. Check connection data.",
|
"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",
|
"unknownError": "Unknown error",
|
||||||
"connectionNotCreated": "Connection couldn't be created",
|
"connectionNotCreated": "Connection couldn't be created",
|
||||||
"connecting": "Connecting...",
|
"connecting": "Connecting...",
|
||||||
|
@ -719,12 +719,44 @@
|
||||||
"unblockClient": "Unblock client",
|
"unblockClient": "Unblock client",
|
||||||
"blockingClient": "Blocking client...",
|
"blockingClient": "Blocking client...",
|
||||||
"unblockingClient": "Unblocking client...",
|
"unblockingClient": "Unblocking client...",
|
||||||
"upstreamDnsCacheConfiguration": "Configuración de la caché DNS upstream",
|
"upstreamDnsCacheConfiguration": "DNS upstream cache configuration",
|
||||||
"enableDnsCachingClient": "Enable DNS caching for this client",
|
"enableDnsCachingClient": "Enable DNS caching for this client",
|
||||||
"dnsCacheSize": "DNS cache size",
|
"dnsCacheSize": "DNS cache size",
|
||||||
"nameInvalid": "Name is required",
|
"nameInvalid": "Name is required",
|
||||||
"oneIdentifierRequired": "At least one identifier is required",
|
"oneIdentifierRequired": "At least one identifier is required",
|
||||||
"dnsCacheNumber": "DNS cache size must be a number",
|
"dnsCacheNumber": "DNS cache size must be a number",
|
||||||
"errors": "Errors",
|
"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.",
|
"invalidUsernamePassword": "Usuario o contraseña no válidos.",
|
||||||
"tooManyAttempts": "Demasiados intentos. Prueba de nuevo más tarde.",
|
"tooManyAttempts": "Demasiados intentos. Prueba de nuevo más tarde.",
|
||||||
"cantReachServer": "No se puede alcanzar el servidor. Comprueba los datos de conexión.",
|
"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",
|
"unknownError": "Error desconocido",
|
||||||
"connectionNotCreated": "No se pudo crear la conexión",
|
"connectionNotCreated": "No se pudo crear la conexión",
|
||||||
"connecting": "Conectando...",
|
"connecting": "Conectando...",
|
||||||
|
@ -726,5 +726,37 @@
|
||||||
"oneIdentifierRequired": "Se require al menos un identificador",
|
"oneIdentifierRequired": "Se require al menos un identificador",
|
||||||
"dnsCacheNumber": "El tamaño de caché de DNS debe ser un número",
|
"dnsCacheNumber": "El tamaño de caché de DNS debe ser un número",
|
||||||
"errors": "Errores",
|
"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-порт."
|
||||||
|
}
|
|
@ -234,7 +234,8 @@ class _MainState extends State<Main> {
|
||||||
Locale('zh', ''),
|
Locale('zh', ''),
|
||||||
Locale('zh', 'CN'),
|
Locale('zh', 'CN'),
|
||||||
Locale('pl', ''),
|
Locale('pl', ''),
|
||||||
Locale('tr', '')
|
Locale('tr', ''),
|
||||||
|
Locale('ru', '')
|
||||||
],
|
],
|
||||||
scaffoldMessengerKey: scaffoldMessengerKey,
|
scaffoldMessengerKey: scaffoldMessengerKey,
|
||||||
navigatorKey: globalNavigatorKey,
|
navigatorKey: globalNavigatorKey,
|
||||||
|
|
|
@ -91,6 +91,7 @@ class Client {
|
||||||
final bool? ignoreStatistics;
|
final bool? ignoreStatistics;
|
||||||
final bool? upstreamsCacheEnabled;
|
final bool? upstreamsCacheEnabled;
|
||||||
final int? upstreamsCacheSize;
|
final int? upstreamsCacheSize;
|
||||||
|
final BlockedServicesSchedule? blockedServicesSchedule;
|
||||||
|
|
||||||
Client({
|
Client({
|
||||||
required this.name,
|
required this.name,
|
||||||
|
@ -108,6 +109,7 @@ class Client {
|
||||||
required this.ignoreStatistics,
|
required this.ignoreStatistics,
|
||||||
required this.upstreamsCacheEnabled,
|
required this.upstreamsCacheEnabled,
|
||||||
required this.upstreamsCacheSize,
|
required this.upstreamsCacheSize,
|
||||||
|
required this.blockedServicesSchedule,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Client.fromJson(Map<String, dynamic> json) => Client(
|
factory Client.fromJson(Map<String, dynamic> json) => Client(
|
||||||
|
@ -127,7 +129,8 @@ class Client {
|
||||||
ignoreQuerylog: json["ignore_querylog"],
|
ignoreQuerylog: json["ignore_querylog"],
|
||||||
ignoreStatistics: json["ignore_statistics"],
|
ignoreStatistics: json["ignore_statistics"],
|
||||||
upstreamsCacheEnabled: json["upstreams_cache_enabled"],
|
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() => {
|
Map<String, dynamic> toJson() => {
|
||||||
|
@ -145,6 +148,71 @@ class Client {
|
||||||
"ignore_querylog": ignoreQuerylog,
|
"ignore_querylog": ignoreQuerylog,
|
||||||
"ignore_statistics": ignoreStatistics,
|
"ignore_statistics": ignoreStatistics,
|
||||||
"upstreams_cache_enabled": upstreamsCacheEnabled,
|
"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';
|
import 'dart:convert';
|
||||||
class DhcpModel {
|
class DhcpModel {
|
||||||
bool dhcpAvailable;
|
|
||||||
List<NetworkInterface> networkInterfaces;
|
List<NetworkInterface> networkInterfaces;
|
||||||
DhcpStatus? dhcpStatus;
|
DhcpStatus? dhcpStatus;
|
||||||
|
|
||||||
DhcpModel({
|
DhcpModel({
|
||||||
required this.dhcpAvailable,
|
|
||||||
required this.networkInterfaces,
|
required this.networkInterfaces,
|
||||||
required this.dhcpStatus,
|
required this.dhcpStatus,
|
||||||
});
|
});
|
||||||
|
@ -83,8 +81,8 @@ class DhcpStatus {
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
"interface_name": interfaceName,
|
"interface_name": interfaceName,
|
||||||
"v4": v4 != null ? v4!.toJson() : null,
|
"v4": v4?.toJson(),
|
||||||
"v6": v6 != null ? v6!.toJson() : null,
|
"v6": v6?.toJson(),
|
||||||
"leases": List<Lease>.from(leases.map((x) => x)),
|
"leases": List<Lease>.from(leases.map((x) => x)),
|
||||||
"static_leases": List<Lease>.from(staticLeases.map((x) => x)),
|
"static_leases": List<Lease>.from(staticLeases.map((x) => x)),
|
||||||
"enabled": enabled,
|
"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? safeSearchPixabay;
|
||||||
bool? safeSearchYandex;
|
bool? safeSearchYandex;
|
||||||
bool? safeSearchYoutube;
|
bool? safeSearchYoutube;
|
||||||
|
bool dhcpAvailable;
|
||||||
|
|
||||||
ServerStatus({
|
ServerStatus({
|
||||||
required this.stats,
|
required this.stats,
|
||||||
|
@ -39,7 +40,8 @@ class ServerStatus {
|
||||||
required this.safeSearchDuckduckgo,
|
required this.safeSearchDuckduckgo,
|
||||||
required this.safeSearchPixabay,
|
required this.safeSearchPixabay,
|
||||||
required this.safeSearchYandex,
|
required this.safeSearchYandex,
|
||||||
required this.safeSearchYoutube
|
required this.safeSearchYoutube,
|
||||||
|
required this.dhcpAvailable,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory ServerStatus.fromJson(Map<String, dynamic> json) => ServerStatus(
|
factory ServerStatus.fromJson(Map<String, dynamic> json) => ServerStatus(
|
||||||
|
@ -64,5 +66,6 @@ class ServerStatus {
|
||||||
safeSearchPixabay: json['safeSearch']['pixabay'],
|
safeSearchPixabay: json['safeSearch']['pixabay'],
|
||||||
safeSearchYandex: json['safeSearch']['yandex'],
|
safeSearchYandex: json['safeSearch']['yandex'],
|
||||||
safeSearchYoutube: json['safeSearch']['youtube'],
|
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 _combinedChartHome = 0;
|
||||||
|
|
||||||
int _showTopItemsChart = 0;
|
|
||||||
|
|
||||||
String? _doNotRememberVersion;
|
String? _doNotRememberVersion;
|
||||||
|
|
||||||
GitHubRelease? _appUpdatesAvailable;
|
GitHubRelease? _appUpdatesAvailable;
|
||||||
|
@ -170,10 +168,6 @@ class AppConfigProvider with ChangeNotifier {
|
||||||
return _hideServerAddress == 1 ? true : false;
|
return _hideServerAddress == 1 ? true : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get showTopItemsChart {
|
|
||||||
return _showTopItemsChart == 1 ? true : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setDbInstance(Database db) {
|
void setDbInstance(Database db) {
|
||||||
_dbInstance = 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 {
|
Future<bool> setDoNotRememberVersion(String value) async {
|
||||||
final updated = await updateConfigQuery(
|
final updated = await updateConfigQuery(
|
||||||
db: _dbInstance!,
|
db: _dbInstance!,
|
||||||
|
@ -446,7 +423,6 @@ class AppConfigProvider with ChangeNotifier {
|
||||||
_showIpLogs = dbData['showIpLogs'] ?? 0;
|
_showIpLogs = dbData['showIpLogs'] ?? 0;
|
||||||
_combinedChartHome = dbData['combinedChart'] ?? 0;
|
_combinedChartHome = dbData['combinedChart'] ?? 0;
|
||||||
_hideServerAddress = dbData['hideServerAddress'];
|
_hideServerAddress = dbData['hideServerAddress'];
|
||||||
_showTopItemsChart = dbData['showTopItemsChart'];
|
|
||||||
if (dbData['homeTopItemsOrder'] != null) {
|
if (dbData['homeTopItemsOrder'] != null) {
|
||||||
try {
|
try {
|
||||||
final itemsOrder = List<HomeTopItems>.from(
|
final itemsOrder = List<HomeTopItems>.from(
|
||||||
|
|
|
@ -20,7 +20,7 @@ class LogsProvider with ChangeNotifier {
|
||||||
DateTime? _logsOlderThan;
|
DateTime? _logsOlderThan;
|
||||||
String _selectedResultStatus = 'all';
|
String _selectedResultStatus = 'all';
|
||||||
String? _searchText;
|
String? _searchText;
|
||||||
List<String>? _selectedClients;
|
List<String> _selectedClients = [];
|
||||||
|
|
||||||
int _logsQuantity = 100;
|
int _logsQuantity = 100;
|
||||||
int _offset = 0;
|
int _offset = 0;
|
||||||
|
@ -65,7 +65,7 @@ class LogsProvider with ChangeNotifier {
|
||||||
return _offset;
|
return _offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String>? get selectedClients {
|
List<String> get selectedClients {
|
||||||
return _selectedClients;
|
return _selectedClients;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,7 +131,7 @@ class LogsProvider with ChangeNotifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
void setSelectedClients(List<String>? clients) {
|
void setSelectedClients(List<String>? clients) {
|
||||||
_selectedClients = clients;
|
_selectedClients = clients ?? [];
|
||||||
notifyListeners();
|
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/settings_tile.dart';
|
||||||
import 'package:adguard_home_manager/screens/clients/client/tags_section.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/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_switch_list_tile.dart';
|
||||||
import 'package:adguard_home_manager/widgets/custom_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 TextEditingController dnsCacheField;
|
||||||
final String? dnsCacheError;
|
final String? dnsCacheError;
|
||||||
final void Function(String?) updateDnsCacheError;
|
final void Function(String?) updateDnsCacheError;
|
||||||
|
final BlockedServicesSchedule blockedServicesSchedule;
|
||||||
|
final void Function(BlockedServicesSchedule) setBlockedServicesSchedule;
|
||||||
|
|
||||||
const ClientForm({
|
const ClientForm({
|
||||||
super.key,
|
super.key,
|
||||||
|
@ -90,6 +93,8 @@ class ClientForm extends StatelessWidget {
|
||||||
required this.dnsCacheField,
|
required this.dnsCacheField,
|
||||||
required this.dnsCacheError,
|
required this.dnsCacheError,
|
||||||
required this.updateDnsCacheError,
|
required this.updateDnsCacheError,
|
||||||
|
required this.blockedServicesSchedule,
|
||||||
|
required this.setBlockedServicesSchedule,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -282,6 +287,11 @@ class ClientForm extends StatelessWidget {
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
BlockingSchedule(
|
||||||
|
blockedServicesSchedule: blockedServicesSchedule,
|
||||||
|
setBlockedServicesSchedule: setBlockedServicesSchedule,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,6 +82,8 @@ class _ClientScreenState extends State<ClientScreen> {
|
||||||
final _dnsCacheField = TextEditingController();
|
final _dnsCacheField = TextEditingController();
|
||||||
String? _dnsCacheError;
|
String? _dnsCacheError;
|
||||||
|
|
||||||
|
BlockedServicesSchedule _blockedServicesSchedule = BlockedServicesSchedule();
|
||||||
|
|
||||||
// VALIDATIONS
|
// VALIDATIONS
|
||||||
bool _nameValid = true;
|
bool _nameValid = true;
|
||||||
bool _identifiersValid = true;
|
bool _identifiersValid = true;
|
||||||
|
@ -140,6 +142,9 @@ class _ClientScreenState extends State<ClientScreen> {
|
||||||
_dnsCacheField.text = widget.client!.upstreamsCacheSize != null
|
_dnsCacheField.text = widget.client!.upstreamsCacheSize != null
|
||||||
? widget.client!.upstreamsCacheSize.toString()
|
? widget.client!.upstreamsCacheSize.toString()
|
||||||
: "";
|
: "";
|
||||||
|
if (widget.client!.blockedServicesSchedule != null) {
|
||||||
|
_blockedServicesSchedule = widget.client!.blockedServicesSchedule!;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
@ -166,7 +171,8 @@ class _ClientScreenState extends State<ClientScreen> {
|
||||||
upstreamsCacheEnabled: _enableDnsCache,
|
upstreamsCacheEnabled: _enableDnsCache,
|
||||||
upstreamsCacheSize: _dnsCacheField.text != ""
|
upstreamsCacheSize: _dnsCacheField.text != ""
|
||||||
? int.parse(_dnsCacheField.text)
|
? int.parse(_dnsCacheField.text)
|
||||||
: null
|
: null,
|
||||||
|
blockedServicesSchedule: _blockedServicesSchedule
|
||||||
);
|
);
|
||||||
widget.onConfirm(client);
|
widget.onConfirm(client);
|
||||||
}
|
}
|
||||||
|
@ -268,7 +274,9 @@ class _ClientScreenState extends State<ClientScreen> {
|
||||||
updateEnableDnsCache: (v) => setState(() => _enableDnsCache = v),
|
updateEnableDnsCache: (v) => setState(() => _enableDnsCache = v),
|
||||||
dnsCacheField: _dnsCacheField,
|
dnsCacheField: _dnsCacheField,
|
||||||
dnsCacheError: _dnsCacheError,
|
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),
|
updateEnableDnsCache: (v) => setState(() => _enableDnsCache = v),
|
||||||
dnsCacheField: _dnsCacheField,
|
dnsCacheField: _dnsCacheField,
|
||||||
dnsCacheError: _dnsCacheError,
|
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();
|
processModal.close();
|
||||||
|
|
||||||
|
if (!context.mounted) return;
|
||||||
if (result == true) {
|
if (result == true) {
|
||||||
showSnacbkar(
|
showSnacbkar(
|
||||||
appConfigProvider: appConfigProvider,
|
appConfigProvider: appConfigProvider,
|
||||||
|
|
|
@ -64,8 +64,6 @@ class _ListDetailsScreenState extends State<ListDetailsScreen> {
|
||||||
final filteringProvider = Provider.of<FilteringProvider>(context);
|
final filteringProvider = Provider.of<FilteringProvider>(context);
|
||||||
final appConfigProvider = Provider.of<AppConfigProvider>(context);
|
final appConfigProvider = Provider.of<AppConfigProvider>(context);
|
||||||
|
|
||||||
final width = MediaQuery.of(context).size.width;
|
|
||||||
|
|
||||||
Filter? list;
|
Filter? list;
|
||||||
try {
|
try {
|
||||||
list = filteringProvider.filtering != null
|
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) {
|
if (widget.dialog == true) {
|
||||||
return Dialog(
|
return Dialog(
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
|
@ -330,7 +157,11 @@ class _ListDetailsScreenState extends State<ListDetailsScreen> {
|
||||||
? AppLocalizations.of(context)!.disableList
|
? AppLocalizations.of(context)!.disableList
|
||||||
: AppLocalizations.of(context)!.enableList,
|
: 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
|
child: list != null
|
||||||
? SingleChildScrollView(
|
? SingleChildScrollView(
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
children: content(),
|
children: [
|
||||||
|
_Content(
|
||||||
|
isDialog: widget.dialog,
|
||||||
|
list: list,
|
||||||
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Center(
|
: Center(
|
||||||
|
@ -361,17 +197,27 @@ class _ListDetailsScreenState extends State<ListDetailsScreen> {
|
||||||
return Dialog.fullscreen(
|
return Dialog.fullscreen(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: CloseButton(
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
),
|
|
||||||
title: Text(AppLocalizations.of(context)!.listDetails),
|
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(
|
body: SafeArea(
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
if (list != null) ListView(
|
if (list != null) ListView(
|
||||||
children: content(),
|
children: [
|
||||||
|
_Content(
|
||||||
|
isDialog: widget.dialog,
|
||||||
|
list: list,
|
||||||
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
if (list == null) Center(
|
if (list == null) Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
|
@ -411,3 +257,203 @@ 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,32 +188,26 @@ class _FiltersState extends State<Filters> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void openListDetails(Filter filter, String type) {
|
void openListDetails(Filter filter, String type) {
|
||||||
showGeneralDialog(
|
if (width > 900) {
|
||||||
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
barrierColor: !(width > 900 || !(Platform.isAndroid | Platform.isIOS))
|
builder: (context) => ListDetailsScreen(
|
||||||
?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,
|
listId: filter.id,
|
||||||
type: type,
|
type: type,
|
||||||
dialog: width > 900 || !(Platform.isAndroid | Platform.isIOS),
|
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() {
|
List<Widget> actions() {
|
||||||
if (filteringProvider.loadStatus == LoadStatus.loaded) {
|
if (filteringProvider.loadStatus == LoadStatus.loaded) {
|
||||||
|
|
|
@ -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),
|
TopItemsLists(order: appConfigProvider.homeTopItemsOrder),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (statusProvider.loadStatus == LoadStatus.error) SliverFillRemaining(
|
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/constants/enums.dart';
|
||||||
import 'package:adguard_home_manager/providers/status_provider.dart';
|
import 'package:adguard_home_manager/providers/status_provider.dart';
|
||||||
|
|
||||||
class RowItem extends StatefulWidget {
|
class RowItem extends StatelessWidget {
|
||||||
final HomeTopItems type;
|
final HomeTopItems type;
|
||||||
final Color chartColor;
|
final Color chartColor;
|
||||||
final String domain;
|
final String domain;
|
||||||
|
@ -32,61 +32,14 @@ class RowItem extends StatefulWidget {
|
||||||
this.unit,
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final statusProvider = Provider.of<StatusProvider>(context);
|
final statusProvider = Provider.of<StatusProvider>(context);
|
||||||
|
|
||||||
String? name;
|
String? name;
|
||||||
if (widget.clients == true) {
|
if (clients == true) {
|
||||||
try {
|
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) {
|
} catch (e) {
|
||||||
// ---- //
|
// ---- //
|
||||||
}
|
}
|
||||||
|
@ -95,9 +48,9 @@ class _RowItemState extends State<RowItem> with TickerProviderStateMixin {
|
||||||
return Material(
|
return Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: OptionsMenu(
|
child: OptionsMenu(
|
||||||
value: widget.domain,
|
value: domain,
|
||||||
options: widget.options,
|
options: options,
|
||||||
onTap: widget.onTapEntry,
|
onTap: onTapEntry,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 20,
|
horizontal: 20,
|
||||||
|
@ -109,18 +62,13 @@ class _RowItemState extends State<RowItem> with TickerProviderStateMixin {
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
SizeTransition(
|
if (showColor == true) Container(
|
||||||
axisAlignment: 1.0,
|
|
||||||
sizeFactor: animation,
|
|
||||||
axis: Axis.horizontal,
|
|
||||||
child: Container(
|
|
||||||
margin: const EdgeInsets.only(right: 16),
|
margin: const EdgeInsets.only(right: 16),
|
||||||
width: 12,
|
width: 12,
|
||||||
height: 12,
|
height: 12,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(30),
|
borderRadius: BorderRadius.circular(30),
|
||||||
color: widget.chartColor
|
color: chartColor
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
@ -128,7 +76,7 @@ class _RowItemState extends State<RowItem> with TickerProviderStateMixin {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
widget.domain,
|
domain,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
@ -154,7 +102,7 @@ class _RowItemState extends State<RowItem> with TickerProviderStateMixin {
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Text(
|
Text(
|
||||||
widget.number,
|
number,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).colorScheme.onSurface
|
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 List<Map<String, dynamic>> items;
|
||||||
final bool showColor;
|
final bool showColor;
|
||||||
|
|
||||||
|
@ -177,63 +125,13 @@ class OthersRowItem extends StatefulWidget {
|
||||||
required this.showColor,
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (widget.items.length <= 5) {
|
if (items.length <= 5) {
|
||||||
return const SizedBox();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
|
|
||||||
return SizeTransition(
|
return Padding(
|
||||||
axisAlignment: 1.0,
|
|
||||||
sizeFactor: animation,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 20,
|
horizontal: 20,
|
||||||
vertical: 8
|
vertical: 8
|
||||||
|
@ -244,7 +142,7 @@ class _OthersRowItemState extends State<OthersRowItem> with SingleTickerProvider
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
if (showColor == true) Container(
|
||||||
margin: const EdgeInsets.only(right: 16),
|
margin: const EdgeInsets.only(right: 16),
|
||||||
width: 12,
|
width: 12,
|
||||||
height: 12,
|
height: 12,
|
||||||
|
@ -274,7 +172,7 @@ class _OthersRowItemState extends State<OthersRowItem> with SingleTickerProvider
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Text(
|
Text(
|
||||||
List<int>.from(
|
List<int>.from(
|
||||||
widget.items.sublist(5, widget.items.length).map((e) => e.values.first.toInt())
|
items.sublist(5, items.length).map((e) => e.values.first.toInt())
|
||||||
).reduce((a, b) => a + b).toString(),
|
).reduce((a, b) => a + b).toString(),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).colorScheme.onSurface
|
color: Theme.of(context).colorScheme.onSurface
|
||||||
|
@ -282,7 +180,6 @@ class _OthersRowItemState extends State<OthersRowItem> with SingleTickerProvider
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -3,18 +3,17 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
import 'package:adguard_home_manager/screens/home/top_items/row_item.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/screens/home/top_items/top_items_screen.dart';
|
||||||
import 'package:adguard_home_manager/widgets/custom_pie_chart.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/models/menu_option.dart';
|
||||||
import 'package:adguard_home_manager/constants/enums.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 HomeTopItems type;
|
||||||
final String label;
|
final String label;
|
||||||
final List<Map<String, dynamic>> data;
|
final List<Map<String, dynamic>> data;
|
||||||
|
@ -37,12 +36,7 @@ class TopItemsSection extends StatefulWidget {
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<TopItemsSection> createState() => _TopItemsState();
|
Widget build(BuildContext context) {
|
||||||
}
|
|
||||||
|
|
||||||
class _TopItemsState extends State<TopItemsSection> {
|
|
||||||
bool _showChart = true;
|
|
||||||
|
|
||||||
final colors = [
|
final colors = [
|
||||||
Colors.red,
|
Colors.red,
|
||||||
Colors.green,
|
Colors.green,
|
||||||
|
@ -52,29 +46,21 @@ class _TopItemsState extends State<TopItemsSection> {
|
||||||
Colors.grey
|
Colors.grey
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
_showChart = Provider.of<AppConfigProvider>(context, listen: false).showTopItemsChart;
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final width = MediaQuery.of(context).size.width;
|
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 = {};
|
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 = {
|
||||||
...values,
|
...values,
|
||||||
element.keys.first: element.values.first.toDouble()
|
element.keys.first: element.values.first.toDouble()
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
if (widget.data.length > 5) {
|
if (data.length > 5) {
|
||||||
final int rest = List<int>.from(
|
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);
|
).reduce((a, b) => a + b);
|
||||||
values = {
|
values = {
|
||||||
...values,
|
...values,
|
||||||
|
@ -84,25 +70,37 @@ class _TopItemsState extends State<TopItemsSection> {
|
||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
final Widget noItems = Padding(
|
List<Map<String, dynamic>> lineData() {
|
||||||
padding: const EdgeInsets.only(
|
List<Map<String, dynamic>> values = [];
|
||||||
bottom: 20,
|
data.sublist(0, data.length > 5 ? 5 : data.length).forEach((element) {
|
||||||
top: 10
|
values.add({
|
||||||
),
|
"label": element.keys.first,
|
||||||
child: Text(
|
"value": element.values.first.toDouble()
|
||||||
AppLocalizations.of(context)!.noItems,
|
});
|
||||||
style: TextStyle(
|
});
|
||||||
fontSize: 16,
|
if (data.length > 5) {
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
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(
|
return SizedBox(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
if (widget.data.isEmpty) noItems,
|
if (data.isEmpty) _NoData(label: label),
|
||||||
if (widget.data.isNotEmpty && width > 700) Padding(
|
if (data.isNotEmpty && width > 700) Padding(
|
||||||
padding: EdgeInsets.only(bottom: withChart == false ? 16 : 0),
|
padding: EdgeInsets.only(bottom: withChart == false ? 16 : 0),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
@ -116,7 +114,7 @@ class _TopItemsState extends State<TopItemsSection> {
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: CustomPieChart(
|
child: CustomPieChart(
|
||||||
data: chartData(),
|
data: ringData(),
|
||||||
colors: colors
|
colors: colors
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
@ -132,7 +130,7 @@ class _TopItemsState extends State<TopItemsSection> {
|
||||||
bottom: 16
|
bottom: 16
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
widget.label,
|
label,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w500
|
fontWeight: FontWeight.w500
|
||||||
|
@ -141,16 +139,16 @@ class _TopItemsState extends State<TopItemsSection> {
|
||||||
),
|
),
|
||||||
_ItemsList(
|
_ItemsList(
|
||||||
colors: colors,
|
colors: colors,
|
||||||
data: widget.data,
|
data: data,
|
||||||
clients: widget.type == HomeTopItems.recurrentClients,
|
clients: type == HomeTopItems.recurrentClients,
|
||||||
type: widget.type,
|
type: type,
|
||||||
showChart: withChart == true ? _showChart : false,
|
showChart: withChart,
|
||||||
buildValue: widget.buildValue,
|
buildValue: buildValue,
|
||||||
menuOptions: widget.menuOptions,
|
menuOptions: menuOptions,
|
||||||
onTapEntry: widget.onTapEntry,
|
onTapEntry: onTapEntry,
|
||||||
),
|
),
|
||||||
if (withChart == true) OthersRowItem(
|
if (withChart == true) OthersRowItem(
|
||||||
items: widget.data,
|
items: data,
|
||||||
showColor: true,
|
showColor: true,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
@ -159,28 +157,9 @@ class _TopItemsState extends State<TopItemsSection> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (widget.data.isNotEmpty && width <= 700) Builder(
|
if (data.isNotEmpty && width <= 700) ...[
|
||||||
builder: (context) {
|
Text(
|
||||||
if (widget.withChart == true) {
|
label,
|
||||||
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,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
|
@ -188,99 +167,49 @@ class _TopItemsState extends State<TopItemsSection> {
|
||||||
color: Theme.of(context).colorScheme.onSurface
|
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(
|
)).toList()
|
||||||
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(
|
||||||
padding: const EdgeInsets.only(top: 8),
|
padding: const EdgeInsets.only(top: 8),
|
||||||
child: _ItemsList(
|
child: _ItemsList(
|
||||||
colors: colors,
|
colors: colors,
|
||||||
data: widget.data,
|
data: data,
|
||||||
clients: widget.type == HomeTopItems.recurrentClients,
|
clients: type == HomeTopItems.recurrentClients,
|
||||||
type: widget.type,
|
type: type,
|
||||||
showChart: _showChart,
|
showChart: withChart,
|
||||||
buildValue: widget.buildValue,
|
buildValue: buildValue,
|
||||||
menuOptions: widget.menuOptions,
|
menuOptions: menuOptions,
|
||||||
onTapEntry: widget.onTapEntry,
|
onTapEntry: 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
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
OthersRowItem(
|
||||||
|
items: data,
|
||||||
|
showColor: withChart,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
),
|
|
||||||
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),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
|
|
||||||
if (widget.data.length > 5) ...[
|
if (data.length > 5) ...[
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(right: 20),
|
padding: const EdgeInsets.only(right: 20),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
@ -288,37 +217,37 @@ class _TopItemsState extends State<TopItemsSection> {
|
||||||
children: [
|
children: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => {
|
onPressed: () => {
|
||||||
showGeneralDialog(
|
if (width > 700) {
|
||||||
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
barrierColor: !(width > 700 || !(Platform.isAndroid | Platform.isIOS))
|
builder: (context) => TopItemsScreen(
|
||||||
?Colors.transparent
|
type: type,
|
||||||
: Colors.black54,
|
title: label,
|
||||||
transitionBuilder: (context, anim1, anim2, child) {
|
isClient: type == HomeTopItems.recurrentClients,
|
||||||
return SlideTransition(
|
data: data,
|
||||||
position: Tween(
|
withProgressBar: withProgressBar,
|
||||||
begin: const Offset(0, 1),
|
buildValue: buildValue,
|
||||||
end: const Offset(0, 0)
|
options: menuOptions,
|
||||||
).animate(
|
onTapEntry: onTapEntry,
|
||||||
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)),
|
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(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
@ -382,3 +311,37 @@ class _ItemsList extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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:provider/provider.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
import 'package:adguard_home_manager/widgets/custom_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/clients_provider.dart';
|
||||||
import 'package:adguard_home_manager/providers/logs_provider.dart';
|
import 'package:adguard_home_manager/providers/logs_provider.dart';
|
||||||
|
|
||||||
class ClientsModal extends StatefulWidget {
|
class ClientsModal extends StatelessWidget {
|
||||||
final List<String>? value;
|
final List<String>? value;
|
||||||
final bool dialog;
|
final bool dialog;
|
||||||
|
|
||||||
|
@ -17,56 +20,90 @@ class ClientsModal extends StatefulWidget {
|
||||||
required this.dialog
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
return Dialog(
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(
|
constraints: const BoxConstraints(
|
||||||
maxWidth: 500
|
maxWidth: 500
|
||||||
),
|
),
|
||||||
child: _ModalContent(
|
child: _ModalContent(
|
||||||
selectedClients: selectedClients,
|
selectedClients: logsProvider.selectedClients,
|
||||||
onClientsSelected: (v) => setState(() => selectedClients = v),
|
onClientsSelected: (v) => logsProvider.setSelectedClients(v),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return ConstrainedBox(
|
return ListBottomSheet(
|
||||||
constraints: BoxConstraints(
|
icon: Icons.smartphone_rounded,
|
||||||
maxHeight: height-50
|
title: AppLocalizations.of(context)!.clients,
|
||||||
),
|
children: [
|
||||||
child: Container(
|
Card(
|
||||||
decoration: BoxDecoration(
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
borderRadius: const BorderRadius.only(
|
child: Padding(
|
||||||
topLeft: Radius.circular(28),
|
padding: const EdgeInsets.all(16),
|
||||||
topRight: Radius.circular(28)
|
child: Row(
|
||||||
),
|
children: [
|
||||||
color: Theme.of(context).dialogBackgroundColor
|
Icon(
|
||||||
),
|
Icons.info_rounded,
|
||||||
child: SafeArea(
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
child: _ModalContent(
|
|
||||||
selectedClients: selectedClients,
|
|
||||||
onClientsSelected: (v) => setState(() => selectedClients = v),
|
|
||||||
),
|
),
|
||||||
|
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(
|
||||||
|
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,53 +123,68 @@ class _ModalContent extends StatelessWidget {
|
||||||
final clientsProvider = Provider.of<ClientsProvider>(context);
|
final clientsProvider = Provider.of<ClientsProvider>(context);
|
||||||
final logsProvider = Provider.of<LogsProvider>(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(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.all(16),
|
||||||
top: 24,
|
child: Row(
|
||||||
bottom: 16,
|
children: [
|
||||||
),
|
CloseButton(
|
||||||
child: Icon(
|
onPressed: () => Navigator.pop(context),
|
||||||
Icons.smartphone_rounded,
|
|
||||||
size: 24,
|
|
||||||
color: Theme.of(context).listTileTheme.iconColor
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context)!.clients,
|
AppLocalizations.of(context)!.clients,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 24,
|
fontSize: 22
|
||||||
fontWeight: FontWeight.w400,
|
|
||||||
color: Theme.of(context).colorScheme.onSurface
|
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
const SizedBox(height: 16),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: ListView.builder(
|
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,
|
itemCount: clientsProvider.clients!.autoClients.length,
|
||||||
itemBuilder: (context, index) => _ListItem(
|
itemBuilder: (context, index) => _ListItem(
|
||||||
label: clientsProvider.clients!.autoClients[index].ip,
|
label: clientsProvider.clients!.autoClients[index].ip,
|
||||||
|
@ -153,29 +205,9 @@ class _ModalContent extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
|
||||||
),
|
),
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(24),
|
|
||||||
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
|
|
||||||
)
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: apply,
|
|
||||||
child: Text(AppLocalizations.of(context)!.apply)
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
),
|
)
|
||||||
),
|
),
|
||||||
if (Platform.isIOS) const SizedBox(height: 16)
|
if (Platform.isIOS) const SizedBox(height: 16)
|
||||||
],
|
],
|
||||||
|
@ -203,9 +235,9 @@ class _ListItem extends StatelessWidget {
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
left: 24,
|
left: 24,
|
||||||
top: 8,
|
top: 4,
|
||||||
right: 12,
|
right: 12,
|
||||||
bottom: 8
|
bottom: 4
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
|
|
@ -150,6 +150,7 @@ class _FiltersList extends StatelessWidget {
|
||||||
dialog: false,
|
dialog: false,
|
||||||
),
|
),
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
|
useSafeArea: true,
|
||||||
backgroundColor: Colors.transparent
|
backgroundColor: Colors.transparent
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -225,8 +226,8 @@ class _FiltersList extends StatelessWidget {
|
||||||
Container(height: 16),
|
Container(height: 16),
|
||||||
CustomListTile(
|
CustomListTile(
|
||||||
title: AppLocalizations.of(context)!.client,
|
title: AppLocalizations.of(context)!.client,
|
||||||
subtitle: logsProvider.selectedClients != null
|
subtitle: logsProvider.selectedClients.isNotEmpty
|
||||||
? "${logsProvider.selectedClients!.length} ${AppLocalizations.of(context)!.clientsSelected}"
|
? "${logsProvider.selectedClients.length} ${AppLocalizations.of(context)!.clientsSelected}"
|
||||||
: AppLocalizations.of(context)!.all,
|
: AppLocalizations.of(context)!.all,
|
||||||
onTap: clientsProvider.loadStatus == LoadStatus.loaded
|
onTap: clientsProvider.loadStatus == LoadStatus.loaded
|
||||||
? openSelectClients
|
? openSelectClients
|
||||||
|
|
|
@ -7,16 +7,10 @@ import 'package:provider/provider.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
import 'package:adguard_home_manager/screens/logs/filters/logs_filters_modal.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/constants/enums.dart';
|
||||||
import 'package:adguard_home_manager/functions/desktop_mode.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/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';
|
import 'package:adguard_home_manager/providers/logs_provider.dart';
|
||||||
|
|
||||||
class LogsListAppBar extends StatelessWidget {
|
class LogsListAppBar extends StatelessWidget {
|
||||||
|
@ -32,61 +26,9 @@ class LogsListAppBar extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final logsProvider = Provider.of<LogsProvider>(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;
|
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() {
|
void openFilersModal() {
|
||||||
if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) {
|
if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) {
|
||||||
showDialog(
|
showDialog(
|
||||||
|
@ -143,40 +85,6 @@ class LogsListAppBar extends StatelessWidget {
|
||||||
tooltip: AppLocalizations.of(context)!.filters,
|
tooltip: AppLocalizations.of(context)!.filters,
|
||||||
)
|
)
|
||||||
: const SizedBox(),
|
: 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),
|
const SizedBox(width: 5),
|
||||||
],
|
],
|
||||||
bottom: logsProvider.appliedFilters.searchText != null || logsProvider.appliedFilters.selectedResultStatus != 'all' || logsProvider.appliedFilters.clients != null
|
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';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class ColorItem extends StatelessWidget {
|
class ColorItem extends StatelessWidget {
|
||||||
|
final int index;
|
||||||
|
final int total;
|
||||||
final Color color;
|
final Color color;
|
||||||
final int numericValue;
|
final int numericValue;
|
||||||
final int? selectedValue;
|
final int? selectedValue;
|
||||||
final void Function(int) onChanged;
|
final void Function(int) onChanged;
|
||||||
|
|
||||||
const ColorItem({
|
const ColorItem({
|
||||||
Key? key,
|
super.key,
|
||||||
|
required this.index,
|
||||||
|
required this.total,
|
||||||
required this.color,
|
required this.color,
|
||||||
required this.numericValue,
|
required this.numericValue,
|
||||||
required this.selectedValue,
|
required this.selectedValue,
|
||||||
required this.onChanged
|
required this.onChanged
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
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(
|
child: Material(
|
||||||
borderRadius: BorderRadius.circular(50),
|
borderRadius: BorderRadius.circular(50),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
@ -30,9 +32,9 @@ class CustomizationWidget extends StatefulWidget {
|
||||||
final AppConfigProvider appConfigProvider;
|
final AppConfigProvider appConfigProvider;
|
||||||
|
|
||||||
const CustomizationWidget({
|
const CustomizationWidget({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.appConfigProvider,
|
required this.appConfigProvider,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<CustomizationWidget> createState() => _CustomizationWidgetState();
|
State<CustomizationWidget> createState() => _CustomizationWidgetState();
|
||||||
|
@ -44,6 +46,8 @@ class _CustomizationWidgetState extends State<CustomizationWidget> {
|
||||||
int selectedColor = 0;
|
int selectedColor = 0;
|
||||||
bool useThemeColorInsteadGreenRed = false;
|
bool useThemeColorInsteadGreenRed = false;
|
||||||
|
|
||||||
|
final _colorsScrollController = ScrollController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
selectedTheme = widget.appConfigProvider.selectedThemeNumber;
|
selectedTheme = widget.appConfigProvider.selectedThemeNumber;
|
||||||
|
@ -125,19 +129,33 @@ class _CustomizationWidgetState extends State<CustomizationWidget> {
|
||||||
title: AppLocalizations.of(context)!.useDynamicTheme,
|
title: AppLocalizations.of(context)!.useDynamicTheme,
|
||||||
),
|
),
|
||||||
if (!(appConfigProvider.androidDeviceInfo != null && appConfigProvider.androidDeviceInfo!.version.sdkInt >= 31)) const SizedBox(height: 20),
|
if (!(appConfigProvider.androidDeviceInfo != null && appConfigProvider.androidDeviceInfo!.version.sdkInt >= 31)) const SizedBox(height: 20),
|
||||||
if (dynamicColor == false) ...[
|
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(
|
SizedBox(
|
||||||
width: MediaQuery.of(context).size.width,
|
width: MediaQuery.of(context).size.width,
|
||||||
height: 70,
|
height: 70,
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
|
controller: _colorsScrollController,
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemCount: colors.length,
|
itemCount: colors.length,
|
||||||
|
padding: const EdgeInsets.all(0),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (index == 0) {
|
if (index == 0) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(width: 15),
|
|
||||||
ColorItem(
|
ColorItem(
|
||||||
|
index: index,
|
||||||
|
total: colors.length,
|
||||||
color: colors[index],
|
color: colors[index],
|
||||||
numericValue: index,
|
numericValue: index,
|
||||||
selectedValue: selectedColor,
|
selectedValue: selectedColor,
|
||||||
|
@ -158,24 +176,10 @@ class _CustomizationWidgetState extends State<CustomizationWidget> {
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
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);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
const SizedBox(width: 15)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else {
|
else {
|
||||||
return ColorItem(
|
return ColorItem(
|
||||||
|
index: index,
|
||||||
|
total: colors.length,
|
||||||
color: colors[index],
|
color: colors[index],
|
||||||
numericValue: index,
|
numericValue: index,
|
||||||
selectedValue: selectedColor,
|
selectedValue: selectedColor,
|
||||||
|
@ -189,10 +193,7 @@ class _CustomizationWidgetState extends State<CustomizationWidget> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
left: 25,
|
|
||||||
top: 10
|
|
||||||
),
|
|
||||||
child: Text(
|
child: Text(
|
||||||
colorTranslation(context, selectedColor),
|
colorTranslation(context, selectedColor),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
@ -202,6 +203,10 @@ class _CustomizationWidgetState extends State<CustomizationWidget> {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
CustomSwitchListTile(
|
CustomSwitchListTile(
|
||||||
value: useThemeColorInsteadGreenRed,
|
value: useThemeColorInsteadGreenRed,
|
||||||
onChanged: (value) {
|
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/screens/settings/dhcp/select_interface_modal.dart';
|
||||||
|
|
||||||
import 'package:adguard_home_manager/functions/desktop_mode.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/functions/snackbar.dart';
|
||||||
import 'package:adguard_home_manager/constants/enums.dart';
|
import 'package:adguard_home_manager/constants/enums.dart';
|
||||||
import 'package:adguard_home_manager/providers/dhcp_provider.dart';
|
import 'package:adguard_home_manager/providers/dhcp_provider.dart';
|
||||||
|
@ -182,13 +183,15 @@ class _DhcpScreenState extends State<DhcpScreen> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
if (mounted) loadDhcpStatus();
|
final statusProvider = Provider.of<StatusProvider>(context, listen: false);
|
||||||
|
if (mounted && statusProvider.serverStatus?.dhcpAvailable == true) loadDhcpStatus();
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final serversProvider = Provider.of<ServersProvider>(context);
|
final serversProvider = Provider.of<ServersProvider>(context);
|
||||||
|
final statusProvider = Provider.of<StatusProvider>(context);
|
||||||
final dhcpProvider = Provider.of<DhcpProvider>(context);
|
final dhcpProvider = Provider.of<DhcpProvider>(context);
|
||||||
final appConfigProvider = Provider.of<AppConfigProvider>(context);
|
final appConfigProvider = Provider.of<AppConfigProvider>(context);
|
||||||
|
|
||||||
|
@ -353,11 +356,7 @@ class _DhcpScreenState extends State<DhcpScreen> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (statusProvider.serverStatus?.dhcpAvailable != true) {
|
||||||
dhcpProvider.loadStatus == LoadStatus.loaded &&
|
|
||||||
dhcpProvider.dhcp != null &&
|
|
||||||
dhcpProvider.dhcp!.dhcpAvailable == false
|
|
||||||
) {
|
|
||||||
return const DhcpNotAvailable();
|
return const DhcpNotAvailable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
import 'package:adguard_home_manager/screens/settings/dhcp/dhcp_interface_item.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';
|
import 'package:adguard_home_manager/models/dhcp.dart';
|
||||||
|
|
||||||
|
@ -89,70 +90,20 @@ class SelectInterfaceModal extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return GestureDetector(
|
return ListBottomSheet(
|
||||||
onTap: () => Navigator.of(context).pop(),
|
icon: Icons.settings_ethernet_rounded,
|
||||||
child: DraggableScrollableSheet(
|
title: AppLocalizations.of(context)!.selectInterface,
|
||||||
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: [
|
children: [
|
||||||
Container(
|
ListView.builder(
|
||||||
margin: const EdgeInsets.all(16),
|
primary: false,
|
||||||
width: 36,
|
shrinkWrap: true,
|
||||||
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,
|
itemCount: interfaces.length,
|
||||||
itemBuilder: (context, index) => DhcpInterfaceItem(
|
itemBuilder: (context, index) => DhcpInterfaceItem(
|
||||||
networkInterface: interfaces[index],
|
networkInterface: interfaces[index],
|
||||||
onSelect: onSelect
|
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),
|
SectionLabel(label: AppLocalizations.of(context)!.logs),
|
||||||
CustomListTile(
|
CustomListTile(
|
||||||
icon: Icons.timer_rounded,
|
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/server_info/server_info.dart';
|
||||||
import 'package:adguard_home_manager/screens/settings/encryption/encryption.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/access_settings/access_settings.dart';
|
||||||
import 'package:adguard_home_manager/screens/settings/customization/customization.dart';
|
import 'package:adguard_home_manager/screens/settings/customization/customization.dart';
|
||||||
import 'package:adguard_home_manager/screens/settings/dhcp/dhcp.dart';
|
import 'package:adguard_home_manager/screens/settings/dhcp/dhcp.dart';
|
||||||
|
import 'package:adguard_home_manager/screens/settings/statistics_settings/statistics_settings.dart';
|
||||||
import 'package:adguard_home_manager/screens/settings/safe_search_settings.dart';
|
import 'package:adguard_home_manager/screens/settings/safe_search_settings.dart';
|
||||||
import 'package:adguard_home_manager/screens/settings/update_server/update.dart';
|
import 'package:adguard_home_manager/screens/settings/update_server/update.dart';
|
||||||
import 'package:adguard_home_manager/screens/settings/dns/dns.dart';
|
import 'package:adguard_home_manager/screens/settings/dns/dns.dart';
|
||||||
|
@ -99,7 +101,9 @@ class _SettingsWidgetState extends State<_SettingsWidget> {
|
||||||
appConfigProvider.setSelectedSettingsScreen(screen: null);
|
appConfigProvider.setSelectedSettingsScreen(screen: null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return ScaffoldMessenger(
|
||||||
|
key: widget.twoColumns ? GlobalKey() : null,
|
||||||
|
child: Scaffold(
|
||||||
body: NestedScrollView(
|
body: NestedScrollView(
|
||||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||||
SliverOverlapAbsorber(
|
SliverOverlapAbsorber(
|
||||||
|
@ -139,11 +143,27 @@ class _SettingsWidgetState extends State<_SettingsWidget> {
|
||||||
screenToNavigate: const SafeSearchSettingsScreen(),
|
screenToNavigate: const SafeSearchSettingsScreen(),
|
||||||
twoColumns: widget.twoColumns,
|
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(
|
_SettingsTile(
|
||||||
icon: Icons.lock_rounded,
|
icon: Icons.lock_rounded,
|
||||||
title: AppLocalizations.of(context)!.accessSettings,
|
title: AppLocalizations.of(context)!.accessSettings,
|
||||||
subtitle: AppLocalizations.of(context)!.accessSettingsDescription,
|
subtitle: AppLocalizations.of(context)!.accessSettingsDescription,
|
||||||
thisItem: 1,
|
thisItem: 3,
|
||||||
screenToNavigate: const AccessSettings(),
|
screenToNavigate: const AccessSettings(),
|
||||||
twoColumns: widget.twoColumns,
|
twoColumns: widget.twoColumns,
|
||||||
),
|
),
|
||||||
|
@ -151,7 +171,7 @@ class _SettingsWidgetState extends State<_SettingsWidget> {
|
||||||
icon: Icons.install_desktop_rounded,
|
icon: Icons.install_desktop_rounded,
|
||||||
title: AppLocalizations.of(context)!.dhcpSettings,
|
title: AppLocalizations.of(context)!.dhcpSettings,
|
||||||
subtitle: AppLocalizations.of(context)!.dhcpSettingsDescription,
|
subtitle: AppLocalizations.of(context)!.dhcpSettingsDescription,
|
||||||
thisItem: 2,
|
thisItem: 4,
|
||||||
screenToNavigate: const DhcpScreen(),
|
screenToNavigate: const DhcpScreen(),
|
||||||
twoColumns: widget.twoColumns,
|
twoColumns: widget.twoColumns,
|
||||||
),
|
),
|
||||||
|
@ -159,7 +179,7 @@ class _SettingsWidgetState extends State<_SettingsWidget> {
|
||||||
icon: Icons.dns_rounded,
|
icon: Icons.dns_rounded,
|
||||||
title: AppLocalizations.of(context)!.dnsSettings,
|
title: AppLocalizations.of(context)!.dnsSettings,
|
||||||
subtitle: AppLocalizations.of(context)!.dnsSettingsDescription,
|
subtitle: AppLocalizations.of(context)!.dnsSettingsDescription,
|
||||||
thisItem: 3,
|
thisItem: 5,
|
||||||
screenToNavigate: DnsSettings(
|
screenToNavigate: DnsSettings(
|
||||||
splitView: widget.twoColumns,
|
splitView: widget.twoColumns,
|
||||||
),
|
),
|
||||||
|
@ -169,7 +189,7 @@ class _SettingsWidgetState extends State<_SettingsWidget> {
|
||||||
icon: Icons.security_rounded,
|
icon: Icons.security_rounded,
|
||||||
title: AppLocalizations.of(context)!.encryptionSettings,
|
title: AppLocalizations.of(context)!.encryptionSettings,
|
||||||
subtitle: AppLocalizations.of(context)!.encryptionSettingsDescription,
|
subtitle: AppLocalizations.of(context)!.encryptionSettingsDescription,
|
||||||
thisItem: 4,
|
thisItem: 6,
|
||||||
screenToNavigate: const EncryptionSettings(),
|
screenToNavigate: const EncryptionSettings(),
|
||||||
twoColumns: widget.twoColumns,
|
twoColumns: widget.twoColumns,
|
||||||
),
|
),
|
||||||
|
@ -177,7 +197,7 @@ class _SettingsWidgetState extends State<_SettingsWidget> {
|
||||||
icon: Icons.route_rounded,
|
icon: Icons.route_rounded,
|
||||||
title: AppLocalizations.of(context)!.dnsRewrites,
|
title: AppLocalizations.of(context)!.dnsRewrites,
|
||||||
subtitle: AppLocalizations.of(context)!.dnsRewritesDescription,
|
subtitle: AppLocalizations.of(context)!.dnsRewritesDescription,
|
||||||
thisItem: 5,
|
thisItem: 7,
|
||||||
screenToNavigate: const DnsRewritesScreen(),
|
screenToNavigate: const DnsRewritesScreen(),
|
||||||
twoColumns: widget.twoColumns,
|
twoColumns: widget.twoColumns,
|
||||||
),
|
),
|
||||||
|
@ -197,7 +217,7 @@ class _SettingsWidgetState extends State<_SettingsWidget> {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
thisItem: 6,
|
thisItem: 8,
|
||||||
screenToNavigate: const UpdateScreen(),
|
screenToNavigate: const UpdateScreen(),
|
||||||
twoColumns: widget.twoColumns,
|
twoColumns: widget.twoColumns,
|
||||||
),
|
),
|
||||||
|
@ -205,7 +225,7 @@ class _SettingsWidgetState extends State<_SettingsWidget> {
|
||||||
icon: Icons.info_rounded,
|
icon: Icons.info_rounded,
|
||||||
title: AppLocalizations.of(context)!.serverInformation,
|
title: AppLocalizations.of(context)!.serverInformation,
|
||||||
subtitle: AppLocalizations.of(context)!.serverInformationDescription,
|
subtitle: AppLocalizations.of(context)!.serverInformationDescription,
|
||||||
thisItem: 7,
|
thisItem: 9,
|
||||||
screenToNavigate: const ServerInformation(),
|
screenToNavigate: const ServerInformation(),
|
||||||
twoColumns: widget.twoColumns,
|
twoColumns: widget.twoColumns,
|
||||||
),
|
),
|
||||||
|
@ -215,7 +235,7 @@ class _SettingsWidgetState extends State<_SettingsWidget> {
|
||||||
icon: Icons.palette_rounded,
|
icon: Icons.palette_rounded,
|
||||||
title: AppLocalizations.of(context)!.customization,
|
title: AppLocalizations.of(context)!.customization,
|
||||||
subtitle: AppLocalizations.of(context)!.customizationDescription,
|
subtitle: AppLocalizations.of(context)!.customizationDescription,
|
||||||
thisItem: 8,
|
thisItem: 10,
|
||||||
screenToNavigate: const Customization(),
|
screenToNavigate: const Customization(),
|
||||||
twoColumns: widget.twoColumns,
|
twoColumns: widget.twoColumns,
|
||||||
),
|
),
|
||||||
|
@ -227,7 +247,7 @@ class _SettingsWidgetState extends State<_SettingsWidget> {
|
||||||
? "${AppLocalizations.of(context)!.connectedTo} ${serversProvider.selectedServer!.name}"
|
? "${AppLocalizations.of(context)!.connectedTo} ${serversProvider.selectedServer!.name}"
|
||||||
: "${AppLocalizations.of(context)!.selectedServer} ${serversProvider.selectedServer!.name}"
|
: "${AppLocalizations.of(context)!.selectedServer} ${serversProvider.selectedServer!.name}"
|
||||||
: AppLocalizations.of(context)!.noServerSelected,
|
: AppLocalizations.of(context)!.noServerSelected,
|
||||||
thisItem: 9,
|
thisItem: 11,
|
||||||
screenToNavigate: const Servers(),
|
screenToNavigate: const Servers(),
|
||||||
twoColumns: widget.twoColumns,
|
twoColumns: widget.twoColumns,
|
||||||
),
|
),
|
||||||
|
@ -235,7 +255,7 @@ class _SettingsWidgetState extends State<_SettingsWidget> {
|
||||||
icon: Icons.settings,
|
icon: Icons.settings,
|
||||||
title: AppLocalizations.of(context)!.generalSettings,
|
title: AppLocalizations.of(context)!.generalSettings,
|
||||||
subtitle: AppLocalizations.of(context)!.generalSettingsDescription,
|
subtitle: AppLocalizations.of(context)!.generalSettingsDescription,
|
||||||
thisItem: 10,
|
thisItem: 12,
|
||||||
screenToNavigate: GeneralSettings(splitView: widget.twoColumns),
|
screenToNavigate: GeneralSettings(splitView: widget.twoColumns),
|
||||||
twoColumns: widget.twoColumns,
|
twoColumns: widget.twoColumns,
|
||||||
),
|
),
|
||||||
|
@ -243,7 +263,7 @@ class _SettingsWidgetState extends State<_SettingsWidget> {
|
||||||
icon: Icons.build_outlined,
|
icon: Icons.build_outlined,
|
||||||
title: AppLocalizations.of(context)!.advancedSettings,
|
title: AppLocalizations.of(context)!.advancedSettings,
|
||||||
subtitle: AppLocalizations.of(context)!.advancedSetupDescription,
|
subtitle: AppLocalizations.of(context)!.advancedSetupDescription,
|
||||||
thisItem: 11,
|
thisItem: 13,
|
||||||
screenToNavigate: const AdvancedSettings(),
|
screenToNavigate: const AdvancedSettings(),
|
||||||
twoColumns: widget.twoColumns,
|
twoColumns: widget.twoColumns,
|
||||||
),
|
),
|
||||||
|
@ -291,6 +311,7 @@ class _SettingsWidgetState extends State<_SettingsWidget> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:sentry_flutter/sentry_flutter.dart';
|
||||||
|
|
||||||
import 'package:adguard_home_manager/models/blocked_services.dart';
|
import 'package:adguard_home_manager/models/blocked_services.dart';
|
||||||
|
import 'package:adguard_home_manager/models/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/dns_info.dart';
|
||||||
import 'package:adguard_home_manager/models/encryption.dart';
|
import 'package:adguard_home_manager/models/encryption.dart';
|
||||||
import 'package:adguard_home_manager/models/dhcp.dart';
|
import 'package:adguard_home_manager/models/dhcp.dart';
|
||||||
|
@ -489,9 +491,6 @@ class ApiClientV2 {
|
||||||
return ApiResponse(
|
return ApiResponse(
|
||||||
successful: true,
|
successful: true,
|
||||||
content: DhcpModel(
|
content: DhcpModel(
|
||||||
dhcpAvailable: jsonDecode(results[1].body!)['message'] != null
|
|
||||||
? false
|
|
||||||
: true,
|
|
||||||
networkInterfaces: interfaces,
|
networkInterfaces: interfaces,
|
||||||
dhcpStatus: jsonDecode(results[1].body!)['message'] != null
|
dhcpStatus: jsonDecode(results[1].body!)['message'] != null
|
||||||
? null
|
? null
|
||||||
|
@ -627,10 +626,19 @@ class ApiClientV2 {
|
||||||
Future<ApiResponse> getQueryLogInfo() async {
|
Future<ApiResponse> getQueryLogInfo() async {
|
||||||
final result = await HttpRequestClient.get(urlPath: '/querylog/config', server: server);
|
final result = await HttpRequestClient.get(urlPath: '/querylog/config', server: server);
|
||||||
if (result.successful) {
|
if (result.successful) {
|
||||||
|
try {
|
||||||
return ApiResponse(
|
return ApiResponse(
|
||||||
successful: true,
|
successful: true,
|
||||||
content: jsonDecode(result.body!)
|
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 {
|
else {
|
||||||
return const ApiResponse(successful: false);
|
return const ApiResponse(successful: false);
|
||||||
|
@ -649,7 +657,7 @@ class ApiClientV2 {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ApiResponse> clearLogs() async {
|
Future<ApiResponse> clearLogs() async {
|
||||||
final result = await HttpRequestClient.put(
|
final result = await HttpRequestClient.post(
|
||||||
urlPath: '/querylog_clear',
|
urlPath: '/querylog_clear',
|
||||||
server: server,
|
server: server,
|
||||||
body: {},
|
body: {},
|
||||||
|
@ -874,4 +882,38 @@ class ApiClientV2 {
|
||||||
content: result.body != null ? jsonDecode(result.body!) : null
|
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:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||||
|
|
||||||
import 'package:adguard_home_manager/classes/http_client.dart';
|
import 'package:adguard_home_manager/classes/http_client.dart';
|
||||||
import 'package:adguard_home_manager/models/server.dart';
|
import 'package:adguard_home_manager/models/server.dart';
|
||||||
|
|
||||||
|
@ -52,7 +54,8 @@ class ServerAuth {
|
||||||
return AuthStatus.timeoutException;
|
return AuthStatus.timeoutException;
|
||||||
} on HandshakeException {
|
} on HandshakeException {
|
||||||
return AuthStatus.handshakeException;
|
return AuthStatus.handshakeException;
|
||||||
} catch (e) {
|
} catch (e, stackTrace) {
|
||||||
|
Sentry.captureException(e, stackTrace: stackTrace);
|
||||||
return AuthStatus.unknown;
|
return AuthStatus.unknown;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,7 +78,8 @@ class ServerAuth {
|
||||||
return AuthStatus.timeoutException;
|
return AuthStatus.timeoutException;
|
||||||
} on HandshakeException {
|
} on HandshakeException {
|
||||||
return AuthStatus.handshakeException;
|
return AuthStatus.handshakeException;
|
||||||
} catch (e) {
|
} catch (e, stackTrace) {
|
||||||
|
Sentry.captureException(e, stackTrace: stackTrace);
|
||||||
return AuthStatus.unknown;
|
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(
|
Database db = await openDatabase(
|
||||||
'adguard_home_manager.db',
|
'adguard_home_manager.db',
|
||||||
version: 10,
|
version: 11,
|
||||||
onCreate: (Database db, int version) async {
|
onCreate: (Database db, int version) async {
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
|
@ -165,8 +175,7 @@ Future<Map<String, dynamic>> loadDb(bool acceptsDynamicTheme) async {
|
||||||
combinedChart NUMERIC,
|
combinedChart NUMERIC,
|
||||||
doNotRememberVersion TEXT,
|
doNotRememberVersion TEXT,
|
||||||
hideServerAddress NUMERIC,
|
hideServerAddress NUMERIC,
|
||||||
homeTopItemsOrder TEXT,
|
homeTopItemsOrder TEXT
|
||||||
showTopItemsChart NUMERIC
|
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
);
|
);
|
||||||
|
@ -185,8 +194,7 @@ Future<Map<String, dynamic>> loadDb(bool acceptsDynamicTheme) async {
|
||||||
showIpLogs,
|
showIpLogs,
|
||||||
combinedChart,
|
combinedChart,
|
||||||
hideServerAddress,
|
hideServerAddress,
|
||||||
homeTopItemsOrder,
|
homeTopItemsOrder
|
||||||
showTopItemsChart
|
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
0,
|
0,
|
||||||
|
@ -199,8 +207,7 @@ Future<Map<String, dynamic>> loadDb(bool acceptsDynamicTheme) async {
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
'$homeTopItemsDefaultOrderString',
|
'$homeTopItemsDefaultOrderString'
|
||||||
1
|
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
);
|
);
|
||||||
|
@ -216,6 +223,7 @@ Future<Map<String, dynamic>> loadDb(bool acceptsDynamicTheme) async {
|
||||||
await upgradeDbToV8(db);
|
await upgradeDbToV8(db);
|
||||||
await upgradeDbToV9(db);
|
await upgradeDbToV9(db);
|
||||||
await upgradeDbToV10(db);
|
await upgradeDbToV10(db);
|
||||||
|
await upgradeDbToV11(db);
|
||||||
}
|
}
|
||||||
if (oldVersion == 2) {
|
if (oldVersion == 2) {
|
||||||
await upgradeDbToV3(db);
|
await upgradeDbToV3(db);
|
||||||
|
@ -226,6 +234,7 @@ Future<Map<String, dynamic>> loadDb(bool acceptsDynamicTheme) async {
|
||||||
await upgradeDbToV8(db);
|
await upgradeDbToV8(db);
|
||||||
await upgradeDbToV9(db);
|
await upgradeDbToV9(db);
|
||||||
await upgradeDbToV10(db);
|
await upgradeDbToV10(db);
|
||||||
|
await upgradeDbToV11(db);
|
||||||
}
|
}
|
||||||
if (oldVersion == 3) {
|
if (oldVersion == 3) {
|
||||||
await upgradeDbToV4(db);
|
await upgradeDbToV4(db);
|
||||||
|
@ -235,6 +244,7 @@ Future<Map<String, dynamic>> loadDb(bool acceptsDynamicTheme) async {
|
||||||
await upgradeDbToV8(db);
|
await upgradeDbToV8(db);
|
||||||
await upgradeDbToV9(db);
|
await upgradeDbToV9(db);
|
||||||
await upgradeDbToV10(db);
|
await upgradeDbToV10(db);
|
||||||
|
await upgradeDbToV11(db);
|
||||||
}
|
}
|
||||||
if (oldVersion == 4) {
|
if (oldVersion == 4) {
|
||||||
await upgradeDbToV5(db);
|
await upgradeDbToV5(db);
|
||||||
|
@ -243,6 +253,7 @@ Future<Map<String, dynamic>> loadDb(bool acceptsDynamicTheme) async {
|
||||||
await upgradeDbToV8(db);
|
await upgradeDbToV8(db);
|
||||||
await upgradeDbToV9(db);
|
await upgradeDbToV9(db);
|
||||||
await upgradeDbToV10(db);
|
await upgradeDbToV10(db);
|
||||||
|
await upgradeDbToV11(db);
|
||||||
}
|
}
|
||||||
if (oldVersion == 5) {
|
if (oldVersion == 5) {
|
||||||
await upgradeDbToV6(db);
|
await upgradeDbToV6(db);
|
||||||
|
@ -250,24 +261,32 @@ Future<Map<String, dynamic>> loadDb(bool acceptsDynamicTheme) async {
|
||||||
await upgradeDbToV8(db);
|
await upgradeDbToV8(db);
|
||||||
await upgradeDbToV9(db);
|
await upgradeDbToV9(db);
|
||||||
await upgradeDbToV10(db);
|
await upgradeDbToV10(db);
|
||||||
|
await upgradeDbToV11(db);
|
||||||
}
|
}
|
||||||
if (oldVersion == 6) {
|
if (oldVersion == 6) {
|
||||||
await upgradeDbToV7(db);
|
await upgradeDbToV7(db);
|
||||||
await upgradeDbToV8(db);
|
await upgradeDbToV8(db);
|
||||||
await upgradeDbToV9(db);
|
await upgradeDbToV9(db);
|
||||||
await upgradeDbToV10(db);
|
await upgradeDbToV10(db);
|
||||||
|
await upgradeDbToV11(db);
|
||||||
}
|
}
|
||||||
if (oldVersion == 7) {
|
if (oldVersion == 7) {
|
||||||
await upgradeDbToV8(db);
|
await upgradeDbToV8(db);
|
||||||
await upgradeDbToV9(db);
|
await upgradeDbToV9(db);
|
||||||
await upgradeDbToV10(db);
|
await upgradeDbToV10(db);
|
||||||
|
await upgradeDbToV11(db);
|
||||||
}
|
}
|
||||||
if (oldVersion == 8) {
|
if (oldVersion == 8) {
|
||||||
await upgradeDbToV9(db);
|
await upgradeDbToV9(db);
|
||||||
await upgradeDbToV10(db);
|
await upgradeDbToV10(db);
|
||||||
|
await upgradeDbToV11(db);
|
||||||
}
|
}
|
||||||
if (oldVersion == 9) {
|
if (oldVersion == 9) {
|
||||||
await upgradeDbToV10(db);
|
await upgradeDbToV10(db);
|
||||||
|
await upgradeDbToV11(db);
|
||||||
|
}
|
||||||
|
if (oldVersion == 10) {
|
||||||
|
await upgradeDbToV11(db);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onOpen: (Database db) async {
|
onOpen: (Database db) async {
|
||||||
|
|
|
@ -1352,7 +1352,6 @@ class ApiClient {
|
||||||
return {
|
return {
|
||||||
'result': 'success',
|
'result': 'success',
|
||||||
'data': DhcpModel(
|
'data': DhcpModel(
|
||||||
dhcpAvailable: true,
|
|
||||||
networkInterfaces: interfaces,
|
networkInterfaces: interfaces,
|
||||||
dhcpStatus: DhcpStatus.fromJson(jsonDecode(result[1]['body']))
|
dhcpStatus: DhcpStatus.fromJson(jsonDecode(result[1]['body']))
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,11 +7,11 @@ class CustomPieChart extends StatelessWidget {
|
||||||
final Duration? animationDuration;
|
final Duration? animationDuration;
|
||||||
|
|
||||||
const CustomPieChart({
|
const CustomPieChart({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.data,
|
required this.data,
|
||||||
required this.colors,
|
required this.colors,
|
||||||
this.animationDuration = const Duration(milliseconds: 800),
|
this.animationDuration = const Duration(milliseconds: 800),
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
|
@ -12,7 +12,7 @@ class CustomSettingsTile extends StatelessWidget {
|
||||||
final int? selectedItem;
|
final int? selectedItem;
|
||||||
|
|
||||||
const CustomSettingsTile({
|
const CustomSettingsTile({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.title,
|
required this.title,
|
||||||
this.subtitle,
|
this.subtitle,
|
||||||
this.subtitleWidget,
|
this.subtitleWidget,
|
||||||
|
@ -22,7 +22,7 @@ class CustomSettingsTile extends StatelessWidget {
|
||||||
this.padding,
|
this.padding,
|
||||||
required this.thisItem,
|
required this.thisItem,
|
||||||
required this.selectedItem,
|
required this.selectedItem,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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):
|
- dynamic_color (0.0.2):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- FlutterMacOS (1.0.0)
|
- FlutterMacOS (1.0.0)
|
||||||
- FMDB (2.7.5):
|
|
||||||
- FMDB/standard (= 2.7.5)
|
|
||||||
- FMDB/standard (2.7.5)
|
|
||||||
- package_info_plus (0.0.1):
|
- package_info_plus (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- Sentry/HybridSDK (8.17.2):
|
- Sentry/HybridSDK (8.18.0):
|
||||||
- SentryPrivate (= 8.17.2)
|
- SentryPrivate (= 8.18.0)
|
||||||
- sentry_flutter (0.0.1):
|
- sentry_flutter (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- Sentry/HybridSDK (= 8.17.2)
|
- Sentry/HybridSDK (= 8.18.0)
|
||||||
- SentryPrivate (8.17.2)
|
- SentryPrivate (8.18.0)
|
||||||
- sqflite (0.0.2):
|
- sqflite (0.0.3):
|
||||||
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- FMDB (>= 2.7.5)
|
- sqlite3 (3.45.0):
|
||||||
- sqlite3 (3.44.0):
|
- sqlite3/common (= 3.45.0)
|
||||||
- sqlite3/common (= 3.44.0)
|
- sqlite3/common (3.45.0)
|
||||||
- sqlite3/common (3.44.0)
|
- sqlite3/fts5 (3.45.0):
|
||||||
- sqlite3/fts5 (3.44.0):
|
|
||||||
- sqlite3/common
|
- sqlite3/common
|
||||||
- sqlite3/perf-threadsafe (3.44.0):
|
- sqlite3/perf-threadsafe (3.45.0):
|
||||||
- sqlite3/common
|
- sqlite3/common
|
||||||
- sqlite3/rtree (3.44.0):
|
- sqlite3/rtree (3.45.0):
|
||||||
- sqlite3/common
|
- sqlite3/common
|
||||||
- sqlite3_flutter_libs (0.0.1):
|
- sqlite3_flutter_libs (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- sqlite3 (~> 3.44.0)
|
- sqlite3 (~> 3.45.0)
|
||||||
- sqlite3/fts5
|
- sqlite3/fts5
|
||||||
- sqlite3/perf-threadsafe
|
- sqlite3/perf-threadsafe
|
||||||
- sqlite3/rtree
|
- sqlite3/rtree
|
||||||
|
@ -45,14 +42,13 @@ DEPENDENCIES:
|
||||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||||
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
||||||
- sentry_flutter (from `Flutter/ephemeral/.symlinks/plugins/sentry_flutter/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`)
|
- sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos`)
|
||||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||||
- window_size (from `Flutter/ephemeral/.symlinks/plugins/window_size/macos`)
|
- window_size (from `Flutter/ephemeral/.symlinks/plugins/window_size/macos`)
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
trunk:
|
trunk:
|
||||||
- FMDB
|
|
||||||
- Sentry
|
- Sentry
|
||||||
- SentryPrivate
|
- SentryPrivate
|
||||||
- sqlite3
|
- sqlite3
|
||||||
|
@ -69,7 +65,7 @@ EXTERNAL SOURCES:
|
||||||
sentry_flutter:
|
sentry_flutter:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos
|
||||||
sqflite:
|
sqflite:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin
|
||||||
sqlite3_flutter_libs:
|
sqlite3_flutter_libs:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos
|
||||||
url_launcher_macos:
|
url_launcher_macos:
|
||||||
|
@ -81,14 +77,13 @@ SPEC CHECKSUMS:
|
||||||
device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f
|
device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f
|
||||||
dynamic_color: 2eaa27267de1ca20d879fbd6e01259773fb1670f
|
dynamic_color: 2eaa27267de1ca20d879fbd6e01259773fb1670f
|
||||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
|
||||||
package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce
|
package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce
|
||||||
Sentry: 64a9f9c3637af913adcf53deced05bbe452d1410
|
Sentry: 8984a4ffb2b9bd2894d74fb36e6f5833865bc18e
|
||||||
sentry_flutter: 57912cf425e09398bdf47f38842a1fcb9836f1be
|
sentry_flutter: c87a0556eeb6cbf7f9f924d30e878bdedf22d364
|
||||||
SentryPrivate: 024c6fed507ac39ae98e6d087034160f942920d5
|
SentryPrivate: 2f0c9ba4c3fc993f70eab6ca95673509561e0085
|
||||||
sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea
|
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||||
sqlite3: 6e2d4a4879854d0ec86b476bf3c3e30870bac273
|
sqlite3: f307b6291c4db7b5086c38d6237446b98a738581
|
||||||
sqlite3_flutter_libs: a25f3a0f522fdcd8fef6a4a50a3d681dd43d8dea
|
sqlite3_flutter_libs: 6b9913d8fbb718e5ebf23658aa6934a0fb509c0f
|
||||||
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95
|
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95
|
||||||
window_size: 339dafa0b27a95a62a843042038fa6c3c48de195
|
window_size: 339dafa0b27a95a62a843042038fa6c3c48de195
|
||||||
|
|
||||||
|
|
96
pubspec.lock
|
@ -213,15 +213,55 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: fl_chart
|
name: fl_chart
|
||||||
sha256: fe6fec7d85975a99c73b9515a69a6e291364accfa0e4a5b3ce6de814d74b9a1c
|
sha256: b5e2b0f13d93f8c532b5a2786bfb44580de1f50b927bf95813fa1af617e9caf8
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.66.0"
|
version: "0.66.1"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
flutter_displaymode:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -279,10 +319,10 @@ packages:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: flutter_native_splash
|
name: flutter_native_splash
|
||||||
sha256: "9cdb5d9665dab5d098dc50feab74301c2c228cd02ca25c9b546ab572cebcd6af"
|
sha256: "558f10070f03ee71f850a78f7136ab239a67636a294a44a06b6b7345178edb1e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.9"
|
version: "2.3.10"
|
||||||
flutter_reorderable_list:
|
flutter_reorderable_list:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -313,14 +353,6 @@ packages:
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
flutter_web_plugins:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
|
@ -338,10 +370,10 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: http
|
name: http
|
||||||
sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139
|
sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
version: "1.2.0"
|
||||||
http_parser:
|
http_parser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -402,10 +434,10 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: markdown
|
name: markdown
|
||||||
sha256: acf35edccc0463a9d7384e437c015a3535772e09714cf60e07eeef3a15870dcd
|
sha256: "1b134d9f8ff2da15cb298efe6cd8b7d2a78958c1b00384ebcbdf13fe340a6c90"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.1.1"
|
version: "7.2.1"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -530,18 +562,18 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: sentry
|
name: sentry
|
||||||
sha256: "89e426587b0879e53c46a0aae0eb312696d9d2d803ba14b252a65cc24b1416a2"
|
sha256: "5686ed515bb620dc52b4ae99a6586fe720d443591183cf1f620ec5d1f0eec100"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.14.0"
|
version: "7.15.0"
|
||||||
sentry_flutter:
|
sentry_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: sentry_flutter
|
name: sentry_flutter
|
||||||
sha256: fd089ee4e75a927be037c56815a0a54af5a519f52b803a5ffecb589bb36e2401
|
sha256: "505dec3b6810562785d2c34ae871c73ff2cba6cf436c32c188f0464df226ba8f"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.14.0"
|
version: "7.15.0"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
|
@ -567,18 +599,18 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: sqflite
|
name: sqflite
|
||||||
sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a"
|
sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.2"
|
||||||
sqflite_common:
|
sqflite_common:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: sqflite_common
|
name: sqflite_common
|
||||||
sha256: "76db4d324c8cbb16ca5b60ad2f3d25cec953107c93ae65aafa480d3e6fb69f14"
|
sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.2-1"
|
version: "2.5.3"
|
||||||
sqflite_common_ffi:
|
sqflite_common_ffi:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -599,10 +631,10 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: sqlite3_flutter_libs
|
name: sqlite3_flutter_libs
|
||||||
sha256: "3e3583b77cf888a68eae2e49ee4f025f66b86623ef0d83c297c8d903daa14871"
|
sha256: "90963b515721d6a71e96f438175cf43c979493ed14822860a300b69694c74eb6"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.18"
|
version: "0.5.19+1"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -659,6 +691,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.1"
|
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:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -679,10 +719,10 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: url_launcher
|
name: url_launcher
|
||||||
sha256: d25bb0ca00432a5e1ee40e69c36c85863addf7cc45e433769d61bed3fe81fd96
|
sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.2.3"
|
version: "6.2.4"
|
||||||
url_launcher_android:
|
url_launcher_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
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
|
# 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
|
# 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.
|
# 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:
|
environment:
|
||||||
sdk: '>=2.18.1 <3.0.0'
|
sdk: '>=2.18.1 <3.0.0'
|
||||||
|
@ -49,7 +49,6 @@ dependencies:
|
||||||
uuid: ^4.2.1
|
uuid: ^4.2.1
|
||||||
expandable: ^5.0.1
|
expandable: ^5.0.1
|
||||||
fl_chart: ^0.66.0
|
fl_chart: ^0.66.0
|
||||||
flutter_web_browser: ^0.17.1
|
|
||||||
flutter_svg: ^2.0.9
|
flutter_svg: ^2.0.9
|
||||||
percent_indicator: ^4.2.3
|
percent_indicator: ^4.2.3
|
||||||
store_checker: ^1.4.0
|
store_checker: ^1.4.0
|
||||||
|
@ -67,7 +66,6 @@ dependencies:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/JGeek00/flutter_split_view
|
url: https://github.com/JGeek00/flutter_split_view
|
||||||
ref: master-alt
|
ref: master-alt
|
||||||
url_launcher: ^6.2.2
|
|
||||||
contextmenu: ^3.0.0
|
contextmenu: ^3.0.0
|
||||||
async: ^2.11.0
|
async: ^2.11.0
|
||||||
sentry_flutter: ^7.13.2
|
sentry_flutter: ^7.13.2
|
||||||
|
@ -76,6 +74,9 @@ dependencies:
|
||||||
pie_chart: ^5.4.0
|
pie_chart: ^5.4.0
|
||||||
segmented_button_slide: ^1.0.4
|
segmented_button_slide: ^1.0.4
|
||||||
http: ^1.1.2
|
http: ^1.1.2
|
||||||
|
timezone: ^0.9.2
|
||||||
|
flutter_custom_tabs: ^2.0.0+1
|
||||||
|
url_launcher: ^6.2.4
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|