diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..db332bb --- /dev/null +++ b/.env.sample @@ -0,0 +1,2 @@ +SENTRY_DSN= +ENABLE_SENTRY=false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..cf174cb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,51 @@ +name: Report a bug +description: | + You detected something wrong on the application. +labels: ["bug"] +body: + - type: textarea + id: steps + attributes: + label: Steps to reproduce + description: Steps to reproduce the problem you are running into. + placeholder: | + 1. ... + 2. ... + 3. ... + validations: + required: true + - type: textarea + id: expected-results + attributes: + label: Expected results + description: What is expected to happen. + validations: + required: true + - type: textarea + id: actual-results + attributes: + label: Actual results + description: What is actually happening. + validations: + required: true + - type: textarea + id: screenshots + attributes: + label: Screenshots or Video + description: | + Upload any screenshots or video of the bug. + value: | + Screenshots or video demonstration + validations: + required: true + - type: textarea + id: app-os-details + attributes: + label: App and OS details + description: | + Some details about the app version and the OS where you are running the application. + value: | + - App version: (eg: v2.16.0) + - Device OS version: (eg: Android 14) + validations: + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request_agh.yml b/.github/ISSUE_TEMPLATE/feature_request_agh.yml new file mode 100644 index 0000000..55972db --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request_agh.yml @@ -0,0 +1,32 @@ +name: Request a new feature that has been added to AdGuard Home +description: | + The AdGuard Home team added recently a new feature that you want on the app. +labels: ["agh feature request"] +body: + - type: textarea + id: description + attributes: + label: Description + description: Describe the feature that you want on the app. + placeholder: | + Write the details here... + validations: + required: true + - type: textarea + id: screenshots + attributes: + label: Screenshots + description: Attach some screenshots of where that new feature is located on the web administration panel. + placeholder: | + Screenshots here. + validations: + required: true + - type: textarea + id: version-introduced + attributes: + label: Version where feature was introduced + description: Version of the AdGuard Home server where this feature was introduced. + placeholder: | + - Version: (eg: v0.107.44) + validations: + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request_app.yml b/.github/ISSUE_TEMPLATE/feature_request_app.yml new file mode 100644 index 0000000..3cf08e6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request_app.yml @@ -0,0 +1,20 @@ +name: Request a new feature or an improvement for the app itself +description: | + You want a new feature for the application, or an improvement for an existing one. +labels: ["app feature request"] +body: + - type: markdown + id: important-info + attributes: + value: | + ℹ️ IMPORTANT INFO ℹ️ + Please note that if the functionality is related to data representation, it may not be implemented, as AdGuard Home Manager depends on the capabilities of the AdGuard Home API. + - type: textarea + id: description + attributes: + label: Description + description: Describe the feature that you want on the app. + placeholder: | + Write the details here... + validations: + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml new file mode 100644 index 0000000..805783f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -0,0 +1,19 @@ +name: You have a question about the app +description: | + You want ask something that's not related with a feature request or a bug. +labels: ["question"] +body: + - type: markdown + id: important-info + attributes: + value: | + Having problems while adding a connection to a server? Check out [this guide](https://github.com/JGeek00/adguard-home-manager/wiki/Create-a-connection). + - type: textarea + id: question + attributes: + label: Question + description: Write your question here giving the maximum detail possible. + placeholder: | + Write your question here... + validations: + required: true \ No newline at end of file diff --git a/.gitignore b/.gitignore index 24476c5..e9d1504 100644 --- a/.gitignore +++ b/.gitignore @@ -5,11 +5,16 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ +# Env +.env + # IntelliJ related *.iml *.ipr @@ -42,3 +47,9 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + +/debian/packages + +untranslated.json + +android/app/.cxx diff --git a/.metadata b/.metadata index e27f5cc..656c923 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled. version: - revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + revision: f72efea43c3013323d1b95cff571f3c1caa37583 channel: stable project_type: app @@ -13,26 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - - platform: android - create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - - platform: ios - create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + create_revision: f72efea43c3013323d1b95cff571f3c1caa37583 + base_revision: f72efea43c3013323d1b95cff571f3c1caa37583 - platform: linux - create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - - platform: macos - create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - - platform: web - create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - - platform: windows - create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + create_revision: f72efea43c3013323d1b95cff571f3c1caa37583 + base_revision: f72efea43c3013323d1b95cff571f3c1caa37583 # User provided section diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..fab9821 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,20 @@ +{ + "editor.formatOnSave": false, + "editor.formatOnPaste": false, + "editor.formatOnType": false, + "editor.defaultFormatter": "Dart-Code.flutter", + "dart.lineLength": 120, + "[dart]": { + "editor.rulers": [ + 120 + ], + "editor.defaultFormatter": "Dart-Code.dart-code", + "editor.formatOnSave": false, + "editor.formatOnPaste": false, + "editor.formatOnType": false + }, + "cSpell.ignorePaths": [ + "/pubspec.yaml", + "/.github/workflows" + ], +} \ No newline at end of file diff --git a/README.md b/README.md index aabbe0f..1dcbc67 100644 --- a/README.md +++ b/README.md @@ -30,13 +30,82 @@

▶ See the clients list and create a custom configuration for a client.

▶ Configure the allowed devices, DHCP, DNS or rewrites among others.

▶ Material You interface with dynamic theming (only Android 12+).

+

▶ Full desktop support with builds for macOS, Linux and Windows.

+

▶ Responsive UI adapted for landscape displays.

## Privacy policy -AdGuard Home Manager does not collect any personal user data. All data stored into the application is kept on the user's device. +Check the privacy policy [here](https://github.com/JGeek00/adguard-home-manager/wiki/Privacy-policy). ## Disclaimer This is an unofficial application. The AdGuard Home team and the development of the AdGuard Home software is not related in any way with this application. +## Recommended configuration and lists +On [this repository](https://github.com/juanico10/Pihole_list) you can find a recommended configuration for AdGuard Home and some lists. Thanks to [juanico10](https://github.com/juanico10). + +## Donations +If you like the project and you want to contribute with the development, you can [become a sponsor on GitHub](https://github.com/sponsors/JGeek00), or you can donate using PayPal. + +
+ + Donate with PayPal + +
+ +## Generate production build + + ## Third party libraries - [provider](https://pub.dev/packages/provider) - [sqflite](https://pub.dev/packages/sqflite) @@ -44,7 +113,6 @@ This is an unofficial application. The AdGuard Home team and the development of - [expandable](https://pub.dev/packages/expandable) - [package info plus](https://pub.dev/packages/package_info_plus) - [flutter phoenix](https://pub.dev/packages/flutter_phoenix) -- [flutter displaymode](https://pub.dev/packages/flutter_displaymode) - [flutter launcher icons](https://pub.dev/packages/flutter_launcher_icons) - [flutter native splash](https://pub.dev/packages/flutter_native_splash) - [intl](https://pub.dev/packages/intl) @@ -52,12 +120,28 @@ This is an unofficial application. The AdGuard Home team and the development of - [dynamic color](https://pub.dev/packages/dynamic_color) - [device info](https://pub.dev/packages/device_info) - [fl chart](https://pub.dev/packages/fl_chart) -- [flutter web browser](https://pub.dev/packages/flutter_web_browser) - [flutter svg](https://pub.dev/packages/flutter_svg) -- [bottom sheet](https://pub.dev/packages/bottom_sheet) - [percent indicator](https://pub.dev/packages/percent_indicator) -- [store checker](https://pub.dev/packages/store_checker) +- [flutter markdown](https://pub.dev/packages/flutter_markdown) +- [markdown](https://pub.dev/packages/markdown) +- [html](https://pub.dev/packages/html) +- [flutter html](https://pub.dev/packages/flutter_html) +- [sqlite3 flutter libs](https://pub.dev/packages/sqlite3_flutter_libs) +- [sqflite common ffi](https://pub.dev/packages/sqflite_common_ffi) +- [window size](https://github.com/google/flutter-desktop-embedding) +- [flutter split view](https://github.com/JGeek00/flutter_split_view) (forked from [here](https://pub.dev/packages/flutter_split_view)) +- [async](https://pub.dev/packages/async) +- [sentry flutter](https://pub.dev/packages/sentry_flutter) +- [flutter dotenv](https://pub.dev/packages/flutter_dotenv) +- [flutter reorderable list](https://pub.dev/packages/flutter_reorderable_list) +- [pie chart](https://pub.dev/packages/pie_chart) +- [segmented button slide](https://pub.dev/packages/segmented_button_slide) +- [timezone](https://pub.dev/packages/timezone) +- [url launcher](https://pub.dev/packages/url_launcher) +- [flutter custom tabs](https://pub.dev/packages/flutter_custom_tabs) +- [shared preferences](https://pub.dev/packages/shared_preferences) +- [window manager](https://pub.dev/packages/window_manager)
-##### Created by JGeek00 \ No newline at end of file +##### Created by JGeek00 diff --git a/android/app/build.gradle b/android/app/build.gradle index d93673c..ab6291c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,3 +1,9 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) { } } -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' @@ -21,13 +22,16 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" +def keystoreProperties = new Properties() +def keystorePropertiesFile = rootProject.file('key.properties') +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +} android { - compileSdkVersion 33 - ndkVersion flutter.ndkVersion + namespace "com.jgeek00.adguard_home_manager" + compileSdkVersion 35 + ndkVersion "26.1.10909125" compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -43,21 +47,25 @@ android { } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.jgeek00.adguard_home_manager" - // You can update the following values to match your application needs. - // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. minSdkVersion 26 - targetSdkVersion 33 + targetSdkVersion 35 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } + signingConfigs { + release { + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] + storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null + storePassword keystoreProperties['storePassword'] + } + } + buildTypes { release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug + signingConfig signingConfigs.release } } } @@ -67,5 +75,5 @@ flutter { } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.20" } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index dbe31f9..603fecc 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -15,7 +15,8 @@ android:label="AdGuard Home Manager" android:name="${applicationName}" android:requestLegacyExternalStorage="true" - android:icon="@mipmap/ic_launcher"> + android:icon="@mipmap/ic_launcher" + android:enableOnBackInvokedCallback="true"> - - + + - + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 620121e..0000000 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..ee4fa4d Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..4d52a73 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..ee4fa4d Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 4b33d20..0000000 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..2ae3642 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..8a39f50 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..2ae3642 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 3128163..0000000 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..1c206bb Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..e490246 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1c206bb Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 7eb67dc..0000000 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aefe0b6 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..3599460 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..aefe0b6 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 7eadefb..0000000 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..68173f9 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..7d57305 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..68173f9 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml deleted file mode 100644 index ab98328..0000000 --- a/android/app/src/main/res/values/colors.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - #ffffff - \ No newline at end of file diff --git a/android/app/src/main/res/values/ic_launcher_background.xml b/android/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..c5d5899 --- /dev/null +++ b/android/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index 61a47e7..bc157bd 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,16 +1,3 @@ -buildscript { - ext.kotlin_version = '1.6.10' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.2.2' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - allprojects { repositories { google() @@ -26,6 +13,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index cb24abd..19cfad9 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/android/key.properties.sample b/android/key.properties.sample new file mode 100644 index 0000000..b51dc2c --- /dev/null +++ b/android/key.properties.sample @@ -0,0 +1,4 @@ +storePassword= # keystore password # +keyPassword= # keystore key password # +keyAlias= # key alias # +storeFile= # ./keystore-file-name.jks # \ No newline at end of file diff --git a/android/settings.gradle b/android/settings.gradle index 44e62bc..e8481f5 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,11 +1,26 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + } + settings.ext.flutterSdkPath = flutterSdkPath() -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() + includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version '8.6.0' apply false + id "org.jetbrains.kotlin.android" version "1.8.20" apply false +} + +include ":app" diff --git a/assets/icon/icon-circle-256.png b/assets/icon/icon-circle-256.png new file mode 100644 index 0000000..517e26a Binary files /dev/null and b/assets/icon/icon-circle-256.png differ diff --git a/assets/icon/icon-circle.ico b/assets/icon/icon-circle.ico new file mode 100644 index 0000000..e4da1bd Binary files /dev/null and b/assets/icon/icon-circle.ico differ diff --git a/assets/icon/icon-circle.png b/assets/icon/icon-circle.png new file mode 100644 index 0000000..5b39396 Binary files /dev/null and b/assets/icon/icon-circle.png differ diff --git a/assets/icon/icon-macos.png b/assets/icon/icon-macos.png new file mode 100755 index 0000000..c201d70 Binary files /dev/null and b/assets/icon/icon-macos.png differ diff --git a/assets/screenshots/linux/clients.png b/assets/screenshots/linux/clients.png new file mode 100644 index 0000000..5804d46 Binary files /dev/null and b/assets/screenshots/linux/clients.png differ diff --git a/assets/screenshots/linux/home.png b/assets/screenshots/linux/home.png new file mode 100644 index 0000000..6666046 Binary files /dev/null and b/assets/screenshots/linux/home.png differ diff --git a/assets/screenshots/linux/logs.png b/assets/screenshots/linux/logs.png new file mode 100644 index 0000000..2cf184f Binary files /dev/null and b/assets/screenshots/linux/logs.png differ diff --git a/debian/compile_deb.txt b/debian/compile_deb.txt new file mode 100644 index 0000000..4a18706 --- /dev/null +++ b/debian/compile_deb.txt @@ -0,0 +1 @@ +https://pub.dev/packages/flutter_to_debian \ No newline at end of file diff --git a/debian/debian.yaml b/debian/debian.yaml new file mode 100644 index 0000000..43a90f9 --- /dev/null +++ b/debian/debian.yaml @@ -0,0 +1,14 @@ +flutter_app: + command: AdGuardHomeManager + arch: x64 + parent: /usr/local/lib + +control: + Package: AdGuardHomeManager + Version: 2.20.1 + Architecture: amd64 + Essential: no + Priority: optional + Depends: + Maintainer: JGeek00 + Description: AdGuard Home control app \ No newline at end of file diff --git a/debian/gui/adguard-home-manager.desktop b/debian/gui/adguard-home-manager.desktop new file mode 100644 index 0000000..a011591 --- /dev/null +++ b/debian/gui/adguard-home-manager.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Name=AdGuard Home Manager +Comment=Manage your AdGuard Home server +Exec=adguard-home-manager +Icon=${SNAP}/meta/gui/adguard-home-manager.png +Terminal=false +Type=Application +Categories=Utilities; diff --git a/debian/gui/adguard-home-manager.png b/debian/gui/adguard-home-manager.png new file mode 100644 index 0000000..5b39396 Binary files /dev/null and b/debian/gui/adguard-home-manager.png differ diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..7e7e7f6 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1 @@ +extensions: diff --git a/ios/.gitignore b/ios/.gitignore old mode 100644 new mode 100755 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist old mode 100644 new mode 100755 index 9625e10..7c56964 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 11.0 + 12.0 diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig old mode 100644 new mode 100755 diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig old mode 100644 new mode 100755 diff --git a/ios/Podfile b/ios/Podfile old mode 100644 new mode 100755 index 88359b2..279576f --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '11.0' +# platform :ios, '12.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 9a238fd..2c2d827 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2,60 +2,91 @@ PODS: - device_info_plus (0.0.1): - Flutter - Flutter (1.0.0) + - flutter_custom_tabs_ios (2.0.0): + - Flutter - flutter_native_splash (0.0.1): - Flutter - - flutter_web_browser (0.17.1): - - Flutter - - FMDB (2.7.5): - - FMDB/standard (= 2.7.5) - - FMDB/standard (2.7.5) - package_info_plus (0.4.5): - Flutter - - sqflite (0.0.2): + - Sentry/HybridSDK (8.18.0): + - SentryPrivate (= 8.18.0) + - sentry_flutter (0.0.1): - Flutter - - FMDB (>= 2.7.5) + - FlutterMacOS + - Sentry/HybridSDK (= 8.18.0) + - SentryPrivate (8.18.0) + - sqflite (0.0.3): + - Flutter + - FlutterMacOS + - sqlite3 (3.45.0): + - sqlite3/common (= 3.45.0) + - sqlite3/common (3.45.0) + - sqlite3/fts5 (3.45.0): + - sqlite3/common + - sqlite3/perf-threadsafe (3.45.0): + - sqlite3/common + - sqlite3/rtree (3.45.0): + - sqlite3/common + - sqlite3_flutter_libs (0.0.1): + - Flutter + - sqlite3 (~> 3.45.0) + - sqlite3/fts5 + - sqlite3/perf-threadsafe + - sqlite3/rtree - store_checker (0.0.1): - Flutter DEPENDENCIES: - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - Flutter (from `Flutter`) + - flutter_custom_tabs_ios (from `.symlinks/plugins/flutter_custom_tabs_ios/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - - flutter_web_browser (from `.symlinks/plugins/flutter_web_browser/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - - sqflite (from `.symlinks/plugins/sqflite/ios`) + - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`) + - sqflite (from `.symlinks/plugins/sqflite/darwin`) + - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`) - store_checker (from `.symlinks/plugins/store_checker/ios`) SPEC REPOS: trunk: - - FMDB + - Sentry + - SentryPrivate + - sqlite3 EXTERNAL SOURCES: device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" Flutter: :path: Flutter + flutter_custom_tabs_ios: + :path: ".symlinks/plugins/flutter_custom_tabs_ios/ios" flutter_native_splash: :path: ".symlinks/plugins/flutter_native_splash/ios" - flutter_web_browser: - :path: ".symlinks/plugins/flutter_web_browser/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" + sentry_flutter: + :path: ".symlinks/plugins/sentry_flutter/ios" sqflite: - :path: ".symlinks/plugins/sqflite/ios" + :path: ".symlinks/plugins/sqflite/darwin" + sqlite3_flutter_libs: + :path: ".symlinks/plugins/sqlite3_flutter_libs/ios" store_checker: :path: ".symlinks/plugins/store_checker/ios" SPEC CHECKSUMS: - device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed - Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + flutter_custom_tabs_ios: 62439c843b2691aae516fd50119a01eb9755fff7 flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef - flutter_web_browser: 7bccaafbb0c5b8862afe7bcd158f15557109f61f - FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a - package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e - sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 + package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 + Sentry: 8984a4ffb2b9bd2894d74fb36e6f5833865bc18e + sentry_flutter: c87a0556eeb6cbf7f9f924d30e878bdedf22d364 + SentryPrivate: 2f0c9ba4c3fc993f70eab6ca95673509561e0085 + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec + sqlite3: f307b6291c4db7b5086c38d6237446b98a738581 + sqlite3_flutter_libs: aeb4d37509853dfa79d9b59386a2dac5dd079428 store_checker: 359c5051d9ec30ff0a8fa39eb5ec9df021bb745d -PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 +PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011 -COCOAPODS: 1.11.2 +COCOAPODS: 1.14.3 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj old mode 100644 new mode 100755 index 972c5ab..00edeae --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -68,7 +68,6 @@ 65533F0C0783FDE34AE79B0A /* Pods-Runner.release.xcconfig */, 69C2CC4A6DE17506FC5C0F13 /* Pods-Runner.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -156,7 +155,8 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -217,10 +217,12 @@ }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -231,6 +233,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -322,6 +325,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -332,6 +336,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -340,7 +345,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -358,15 +363,22 @@ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 38Z3B9TJTR; ENABLE_BITCODE = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "AdGuard Home"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.adguardHomeManager; + PRODUCT_BUNDLE_IDENTIFIER = com.jgeek00.adguardHomeManager; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; @@ -394,6 +406,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -404,6 +417,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -418,7 +432,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -449,6 +463,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -459,6 +474,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -467,7 +483,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -487,16 +503,23 @@ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 38Z3B9TJTR; ENABLE_BITCODE = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "AdGuard Home"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.adguardHomeManager; + PRODUCT_BUNDLE_IDENTIFIER = com.jgeek00.adguardHomeManager; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -510,15 +533,22 @@ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 38Z3B9TJTR; ENABLE_BITCODE = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "AdGuard Home"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.adguardHomeManager; + PRODUCT_BUNDLE_IDENTIFIER = com.jgeek00.adguardHomeManager; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata old mode 100644 new mode 100755 diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist old mode 100644 new mode 100755 diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings old mode 100644 new mode 100755 diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme old mode 100644 new mode 100755 index c87d15a..a6b826d --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - AdGuard Home Manager - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - adguard_home_manager - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - - UIStatusBarHidden - - + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + AdGuard Home + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + adguardHomeManager + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIStatusBarHidden + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h old mode 100644 new mode 100755 diff --git a/l10n.yaml b/l10n.yaml index 4e6692e..764d005 100644 --- a/l10n.yaml +++ b/l10n.yaml @@ -1,3 +1,4 @@ arb-dir: lib/l10n template-arb-file: app_en.arb -output-localization-file: app_localizations.dart \ No newline at end of file +output-localization-file: app_localizations.dart +untranslated-messages-file: untranslated.json \ No newline at end of file diff --git a/lib/base.dart b/lib/base.dart deleted file mode 100644 index 327b6ca..0000000 --- a/lib/base.dart +++ /dev/null @@ -1,144 +0,0 @@ -// ignore_for_file: use_build_context_synchronously, depend_on_referenced_packages - -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:animations/animations.dart'; -import 'package:flutter_web_browser/flutter_web_browser.dart'; -import 'package:provider/provider.dart'; -import 'package:store_checker/store_checker.dart'; -import 'package:flutter/services.dart'; - -import 'package:adguard_home_manager/widgets/bottom_nav_bar.dart'; -import 'package:adguard_home_manager/widgets/update_modal.dart'; - -import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/models/github_release.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; -import 'package:adguard_home_manager/models/app_screen.dart'; -import 'package:adguard_home_manager/config/app_screens.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; - -class Base extends StatefulWidget { - final AppConfigProvider appConfigProvider; - - const Base({ - Key? key, - required this.appConfigProvider, - }) : super(key: key); - - @override - State createState() => _BaseState(); -} - -class _BaseState extends State with WidgetsBindingObserver { - int selectedScreen = 0; - - bool updateExists(String appVersion, String gitHubVersion) { - final List appVersionSplit = List.from(appVersion.split('.').map((e) => int.parse(e))); - final List gitHubVersionSplit = List.from(gitHubVersion.split('.').map((e) => int.parse(e))); - - if (gitHubVersionSplit[0] > appVersionSplit[0]) { - return true; - } - else if (gitHubVersionSplit[0] == appVersionSplit[0] && gitHubVersionSplit[1] > appVersionSplit[1]) { - return true; - } - else if (gitHubVersionSplit[0] == appVersionSplit[0] && gitHubVersionSplit[1] == appVersionSplit[1] && gitHubVersionSplit[2] > appVersionSplit[2]) { - return true; - } - else { - return false; - } - } - - Future checkInstallationSource() async { - Source installationSource = await StoreChecker.getSource; - if (installationSource != Source.IS_INSTALLED_FROM_PLAY_STORE) { - final result = await checkAppUpdatesGitHub(); - if (result['result'] == 'success') { - if (updateExists(widget.appConfigProvider.getAppInfo!.version, result['body'].tagName)) { - return result['body']; - } - } - } - return null; - } - - void download(String link, String version) async { - FlutterWebBrowser.openWebPage( - url: link, - customTabsOptions: const CustomTabsOptions( - instantAppsEnabled: true, - showTitle: true, - urlBarHidingEnabled: false, - ), - safariVCOptions: const SafariViewControllerOptions( - barCollapsingEnabled: true, - dismissButtonStyle: SafariViewControllerDismissButtonStyle.close, - modalPresentationCapturesStatusBarAppearance: true, - ) - ); - } - - @override - void initState() { - WidgetsBinding.instance.addObserver(this); - - super.initState(); - - WidgetsBinding.instance.addPostFrameCallback((_) async { - final result = await checkInstallationSource(); - - if (result != null && widget.appConfigProvider.doNotRememberVersion != result.tagName) { - await showDialog( - context: context, - builder: (context) => UpdateModal( - gitHubRelease: result, - onDownload: download, - ), - ); - } - }); - } - - @override - Widget build(BuildContext context) { - final serversProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - - List screens = serversProvider.selectedServer != null - ? screensServerConnected - : screensSelectServer; - - return AnnotatedRegion( - value: SystemUiOverlayStyle( - statusBarColor: Colors.transparent, - statusBarBrightness: Theme.of(context).brightness == Brightness.light - ? Brightness.light - : Brightness.dark, - statusBarIconBrightness: Theme.of(context).brightness == Brightness.light - ? Brightness.dark - : Brightness.light, - systemNavigationBarColor: Theme.of(context).scaffoldBackgroundColor, - systemNavigationBarIconBrightness: Theme.of(context).brightness == Brightness.light - ? Brightness.dark - : Brightness.light, - ), - child: Scaffold( - body: PageTransitionSwitcher( - duration: const Duration(milliseconds: 200), - transitionBuilder: ( - (child, primaryAnimation, secondaryAnimation) => FadeThroughTransition( - animation: primaryAnimation, - secondaryAnimation: secondaryAnimation, - child: child, - ) - ), - child: screens[appConfigProvider.selectedScreen].body, - ), - bottomNavigationBar: const BottomNavBar(), - ) - ); - } -} \ No newline at end of file diff --git a/lib/classes/http_client.dart b/lib/classes/http_client.dart new file mode 100644 index 0000000..d64e1d1 --- /dev/null +++ b/lib/classes/http_client.dart @@ -0,0 +1,216 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:adguard_home_manager/models/server.dart'; + +enum ExceptionType { socket, timeout, handshake, http, unknown } + +class HttpResponse { + final bool successful; + final String? body; + final int? statusCode; + final ExceptionType? exception; + + const HttpResponse({ + required this.successful, + required this.body, + required this.statusCode, + this.exception, + }); +} + +String getConnectionString({ + required Server server, + required String urlPath, +}) { + return "${server.connectionMethod}://${server.domain}${server.port != null ? ':${server.port}' : ""}${server.path ?? ""}/control$urlPath"; +} + +class HttpRequestClient { + static Future get({ + required String urlPath, + required Server server, + int timeout = 10, + }) async{ + final String connectionString = getConnectionString(server: server, urlPath: urlPath); + try { + HttpClient httpClient = HttpClient(); + HttpClientRequest request = await httpClient.getUrl(Uri.parse(connectionString)); + if (server.authToken != null) { + request.headers.set('Authorization', 'Basic ${server.authToken}'); + } + HttpClientResponse response = await request.close().timeout( + Duration(seconds: timeout) + ); + String reply = await response.transform(utf8.decoder).join(); + httpClient.close(); + return HttpResponse( + successful: response.statusCode >= 400 ? false : true, + body: reply, + statusCode: response.statusCode + ); + } on SocketException { + return const HttpResponse( + successful: false, + body: null, + statusCode: null, + exception: ExceptionType.socket + ); + } on TimeoutException { + return const HttpResponse( + successful: false, + body: null, + statusCode: null, + exception: ExceptionType.timeout + ); + } on HandshakeException { + return const HttpResponse( + successful: false, + body: null, + statusCode: null, + exception: ExceptionType.handshake + ); + } on HttpException { + return const HttpResponse( + successful: false, + body: null, + statusCode: null, + exception: ExceptionType.http + ); + } catch (e) { + return const HttpResponse( + successful: false, + body: null, + statusCode: null, + exception: ExceptionType.unknown + ); + } + } + + static Future post({ + required String urlPath, + required Server server, + dynamic body, + int timeout = 10, + }) async{ + final String connectionString = getConnectionString(server: server, urlPath: urlPath); + try { + HttpClient httpClient = HttpClient(); + HttpClientRequest request = await httpClient.postUrl(Uri.parse(connectionString)); + if (server.authToken != null) { + request.headers.set('Authorization', 'Basic ${server.authToken}'); + } + request.headers.set('content-type', 'application/json'); + request.add(utf8.encode(json.encode(body))); + HttpClientResponse response = await request.close().timeout( + Duration(seconds: timeout) + ); + String reply = await response.transform(utf8.decoder).join(); + httpClient.close(); + return HttpResponse( + successful: response.statusCode >= 400 ? false : true, + body: reply, + statusCode: response.statusCode + ); + } on SocketException { + return const HttpResponse( + successful: false, + body: null, + statusCode: null, + exception: ExceptionType.socket + ); + } on TimeoutException { + return const HttpResponse( + successful: false, + body: null, + statusCode: null, + exception: ExceptionType.timeout + ); + } on HttpException { + return const HttpResponse( + successful: false, + body: null, + statusCode: null, + exception: ExceptionType.http + ); + } on HandshakeException { + return const HttpResponse( + successful: false, + body: null, + statusCode: null, + exception: ExceptionType.handshake + ); + } catch (e) { + return const HttpResponse( + successful: false, + body: null, + statusCode: null, + exception: ExceptionType.unknown + ); + } + } + + static Future put({ + required String urlPath, + required Server server, + dynamic body, + int timeout = 10, + }) async{ + final String connectionString = getConnectionString(server: server, urlPath: urlPath); + try { + HttpClient httpClient = HttpClient(); + HttpClientRequest request = await httpClient.putUrl(Uri.parse(connectionString)); + if (server.authToken != null) { + request.headers.set('Authorization', 'Basic ${server.authToken}'); + } + request.headers.set('content-type', 'application/json'); + request.add(utf8.encode(json.encode(body))); + HttpClientResponse response = await request.close().timeout( + Duration(seconds: timeout) + ); + String reply = await response.transform(utf8.decoder).join(); + httpClient.close(); + return HttpResponse( + successful: response.statusCode >= 400 ? false : true, + body: reply, + statusCode: response.statusCode + ); + } on SocketException { + return const HttpResponse( + successful: false, + body: null, + statusCode: null, + exception: ExceptionType.socket + ); + } on TimeoutException { + return const HttpResponse( + successful: false, + body: null, + statusCode: null, + exception: ExceptionType.timeout + ); + } on HttpException { + return const HttpResponse( + successful: false, + body: null, + statusCode: null, + exception: ExceptionType.http + ); + } on HandshakeException { + return const HttpResponse( + successful: false, + body: null, + statusCode: null, + exception: ExceptionType.handshake + ); + } catch (e) { + return const HttpResponse( + successful: false, + body: null, + statusCode: null, + exception: ExceptionType.unknown + ); + } + } +} \ No newline at end of file diff --git a/lib/classes/process_modal.dart b/lib/classes/process_modal.dart index 6febebb..ac3db92 100644 --- a/lib/classes/process_modal.dart +++ b/lib/classes/process_modal.dart @@ -1,20 +1,14 @@ +import 'package:adguard_home_manager/config/globals.dart'; import 'package:flutter/material.dart'; import 'package:adguard_home_manager/widgets/process_dialog.dart'; class ProcessModal { - late BuildContext context; - - ProcessModal({ - required this.context - }); - void open(String message) async { await Future.delayed(const Duration(seconds: 0), () => { showDialog( - context: context, - builder: (c) { - context = c; + context: globalNavigatorKey.currentContext!, + builder: (ctx) { return ProcessDialog( message: message, ); @@ -26,6 +20,6 @@ class ProcessModal { } void close() { - Navigator.pop(context); + Navigator.pop(globalNavigatorKey.currentContext!); } } \ No newline at end of file diff --git a/lib/config/app_screens.dart b/lib/config/app_screens.dart index 1835dc4..2326e31 100644 --- a/lib/config/app_screens.dart +++ b/lib/config/app_screens.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:adguard_home_manager/screens/filters/filters.dart'; -import 'package:adguard_home_manager/screens/logs/logs.dart'; -import 'package:adguard_home_manager/screens/connect/connect.dart'; -import 'package:adguard_home_manager/screens/home/home.dart'; import 'package:adguard_home_manager/screens/clients/clients.dart'; +import 'package:adguard_home_manager/screens/connect/connect.dart'; +import 'package:adguard_home_manager/screens/filters/filters.dart'; +import 'package:adguard_home_manager/screens/home/home.dart'; +import 'package:adguard_home_manager/screens/logs/logs.dart'; import 'package:adguard_home_manager/screens/settings/settings.dart'; import 'package:adguard_home_manager/models/app_screen.dart'; @@ -13,12 +13,12 @@ List screensSelectServer = [ const AppScreen( name: "connect", icon: Icons.link_rounded, - body: Connect(), + child: Connect() ), const AppScreen( name: "settings", icon: Icons.settings_rounded, - body: Settings() + child: Settings() ) ]; @@ -26,26 +26,26 @@ List screensServerConnected = [ const AppScreen( name: "home", icon: Icons.home_rounded, - body: Home(), + child: Home() ), const AppScreen( name: "clients", icon: Icons.devices, - body: Clients() + child: Clients() ), const AppScreen( name: "logs", icon: Icons.list_alt_rounded, - body: Logs(), + child: Logs() ), const AppScreen( name: "filters", icon: Icons.shield_rounded, - body: Filters(), + child: Filters() ), const AppScreen( name: "settings", icon: Icons.settings_rounded, - body: Settings() + child: Settings() ) ]; \ No newline at end of file diff --git a/lib/config/globals.dart b/lib/config/globals.dart new file mode 100644 index 0000000..a955a77 --- /dev/null +++ b/lib/config/globals.dart @@ -0,0 +1,4 @@ +import 'package:flutter/material.dart'; + +final GlobalKey scaffoldMessengerKey = GlobalKey(); +final GlobalKey globalNavigatorKey = GlobalKey(); \ No newline at end of file diff --git a/lib/config/home_top_items_default_order.dart b/lib/config/home_top_items_default_order.dart new file mode 100644 index 0000000..a5a23f0 --- /dev/null +++ b/lib/config/home_top_items_default_order.dart @@ -0,0 +1,15 @@ +import 'dart:convert'; + +import 'package:adguard_home_manager/constants/enums.dart'; + +final List homeTopItemsDefaultOrder = [ + HomeTopItems.queriedDomains, + HomeTopItems.blockedDomains, + HomeTopItems.recurrentClients, + HomeTopItems.topUpstreams, + HomeTopItems.avgUpstreamResponseTime +]; + +final String homeTopItemsDefaultOrderString = jsonEncode( + List.from(homeTopItemsDefaultOrder.map((e) => e.name)) +); \ No newline at end of file diff --git a/lib/config/minimum_server_version.dart b/lib/config/minimum_server_version.dart new file mode 100644 index 0000000..f554cb9 --- /dev/null +++ b/lib/config/minimum_server_version.dart @@ -0,0 +1,4 @@ +class MinimumServerVersion { + static const String stable = "v0.107.28"; + static const String beta = "v0.108.0-b.33"; +} \ No newline at end of file diff --git a/lib/config/sizes.dart b/lib/config/sizes.dart new file mode 100644 index 0000000..0fc2640 --- /dev/null +++ b/lib/config/sizes.dart @@ -0,0 +1 @@ +const double desktopBreakpoint = 1000; \ No newline at end of file diff --git a/lib/config/theme.dart b/lib/config/theme.dart index 7391677..eba0b9c 100644 --- a/lib/config/theme.dart +++ b/lib/config/theme.dart @@ -15,7 +15,24 @@ ThemeData lightTheme(ColorScheme? dynamicColorScheme) => ThemeData( textColor: dynamicColorScheme != null ? dynamicColorScheme.onSurfaceVariant : const Color.fromRGBO(117, 117, 117, 1), iconColor: dynamicColorScheme != null ? dynamicColorScheme.onSurfaceVariant : const Color.fromRGBO(117, 117, 117, 1), ), - androidOverscrollIndicator: AndroidOverscrollIndicator.stretch, + cardTheme: CardTheme( + surfaceTintColor: dynamicColorScheme?.surfaceTint + ), + popupMenuTheme: PopupMenuThemeData( + surfaceTintColor: dynamicColorScheme?.surfaceTint + ), + navigationBarTheme: NavigationBarThemeData( + surfaceTintColor: dynamicColorScheme?.surfaceTint + ), + dialogTheme: DialogTheme( + surfaceTintColor: dynamicColorScheme?.surfaceTint + ), + // DISABLE PREDICTIVE BACK GESTURE + // pageTransitionsTheme: const PageTransitionsTheme( + // builders: { + // TargetPlatform.android: PredictiveBackPageTransitionsBuilder() + // } + // ) ); ThemeData darkTheme(ColorScheme? dynamicColorScheme) => ThemeData( @@ -34,7 +51,24 @@ ThemeData darkTheme(ColorScheme? dynamicColorScheme) => ThemeData( textColor: dynamicColorScheme != null ? dynamicColorScheme.onSurfaceVariant : const Color.fromRGBO(187, 187, 187, 1), iconColor: dynamicColorScheme != null ? dynamicColorScheme.onSurfaceVariant : const Color.fromRGBO(187, 187, 187, 1), ), - androidOverscrollIndicator: AndroidOverscrollIndicator.stretch, + cardTheme: CardTheme( + surfaceTintColor: dynamicColorScheme?.surfaceTint + ), + popupMenuTheme: PopupMenuThemeData( + surfaceTintColor: dynamicColorScheme?.surfaceTint + ), + navigationBarTheme: NavigationBarThemeData( + surfaceTintColor: dynamicColorScheme?.surfaceTint + ), + dialogTheme: DialogTheme( + surfaceTintColor: dynamicColorScheme?.surfaceTint + ), + // DISABLE PREDICTIVE BACK GESTURE + // pageTransitionsTheme: const PageTransitionsTheme( + // builders: { + // TargetPlatform.android: PredictiveBackPageTransitionsBuilder() + // } + // ) ); ThemeData lightThemeOldVersions(MaterialColor primaryColor) => ThemeData( @@ -53,7 +87,12 @@ ThemeData lightThemeOldVersions(MaterialColor primaryColor) => ThemeData( iconColor: Color.fromRGBO(117, 117, 117, 1), ), brightness: Brightness.light, - androidOverscrollIndicator: AndroidOverscrollIndicator.stretch + // DISABLE PREDICTIVE BACK GESTURE + // pageTransitionsTheme: const PageTransitionsTheme( + // builders: { + // TargetPlatform.android: PredictiveBackPageTransitionsBuilder() + // } + // ) ); ThemeData darkThemeOldVersions(MaterialColor primaryColor) => ThemeData( @@ -75,5 +114,10 @@ ThemeData darkThemeOldVersions(MaterialColor primaryColor) => ThemeData( iconColor: Color.fromRGBO(187, 187, 187, 1), ), brightness: Brightness.dark, - androidOverscrollIndicator: AndroidOverscrollIndicator.stretch + // DISABLE PREDICTIVE BACK GESTURE + // pageTransitionsTheme: const PageTransitionsTheme( + // builders: { + // TargetPlatform.android: PredictiveBackPageTransitionsBuilder() + // } + // ) ); \ No newline at end of file diff --git a/lib/constants/enums.dart b/lib/constants/enums.dart new file mode 100644 index 0000000..569db37 --- /dev/null +++ b/lib/constants/enums.dart @@ -0,0 +1,3 @@ +enum LoadStatus { loading, loaded, error } +enum HomeTopItems { queriedDomains, blockedDomains, recurrentClients, topUpstreams, avgUpstreamResponseTime } +enum CustomRulesSorting { topBottom, bottomTop } \ No newline at end of file diff --git a/lib/constants/regexps.dart b/lib/constants/regexps.dart new file mode 100644 index 0000000..e882913 --- /dev/null +++ b/lib/constants/regexps.dart @@ -0,0 +1,12 @@ +class Regexps { + static final wildcardDomain = RegExp(r'^(\*\.)?(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,10}$'); + static final domain = RegExp(r'^((?:(?:[a-zA-Z]{1})|(?:[a-zA-Z]{1}[a-zA-Z]{1})|(?:[a-zA-Z]{1}[0-9]{1})|(?:[0-9]{1}[a-zA-Z]{1})|(?:[a-zA-Z0-9][a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]))\.)+([a-zA-Z]{2,10}|[a-zA-Z0-9-]{2,30}\.[a-zA-Z]{2,10})$'); + static final ipv4Address = RegExp(r'^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$'); + static final ipv6Address = RegExp(r'(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))'); + static final subroute = RegExp(r'^\/\b([A-Za-z0-9_\-~/]*)[^\/|\.|\:]$'); + static final macAddress = RegExp(r'^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$'); + static final url = RegExp(r'^(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})$'); + static final certificate = RegExp(r'(-{3,}(\bBEGIN CERTIFICATE\b))|(-{3,}-{3,}(\END CERTIFICATE\b)-{3,})', multiLine: true); + static final privateKey = RegExp(r'(-{3,}(\bBEGIN\b).*(PRIVATE KEY\b))|(-{3,}-{3,}(\bEND\b).*(PRIVATE KEY\b)-{3,})', multiLine: true); + static final path = RegExp(r'^(\/{0,1}(?!\/))[A-Za-z0-9\/\-_]+(\.([a-zA-Z]+))?$'); +} \ No newline at end of file diff --git a/lib/constants/routes_names.dart b/lib/constants/routes_names.dart new file mode 100644 index 0000000..fa0b07c --- /dev/null +++ b/lib/constants/routes_names.dart @@ -0,0 +1,31 @@ +class RoutesNames { + static const String connect = "/connect"; + + static const String home = "/home"; + static const String queriedDomains = "/home/queried-domains"; + static const String blockedDomains = "/home/blocked-domains"; + static const String recurrentClients = "/home/recurrent-clients"; + + static const String clients = "/clients"; + static const String clientsList = "/clients/list"; + static const String clientPlaceholder = "/clients/list/placeholder"; + static const String client = "/clients/list:id"; + + static const String logs = "/logs"; + + static const String filters = "/filters"; + + static const String settings = "/settings"; + static const String safeSearch = "/settings/safe-search"; + static const String accessSettings = "/settings/access-settigs"; + static const String dhcpSettings = "/settings/dhcp-settings"; + static const String dnsSettings = "/settings/dns-settings"; + static const String encryptionSettings = "/settings/encryption-settings"; + static const String dnsRewrites = "/settings/dns-rewrites"; + static const String serverUpdates = "/settings/server-updates"; + static const String serverInfo = "/settings/server-info"; + static const String customization = "/settings/customization"; + static const String servers = "/settings/servers"; + static const String generalSettings = "/settings/general-settings"; + static const String advancedSettings = "/settings/advanced-settings"; +} \ No newline at end of file diff --git a/lib/constants/urls.dart b/lib/constants/urls.dart index 5b60855..0a8ece3 100644 --- a/lib/constants/urls.dart +++ b/lib/constants/urls.dart @@ -2,5 +2,11 @@ class Urls { static const String playStore = "https://play.google.com/store/apps/details?id=com.jgeek00.adguard_home_manager"; static const String gitHub = "https://github.com/JGeek00/adguard-home-manager"; static const String customRuleDocs = "https://kb.adguard.com/en/general/how-to-create-your-own-ad-filters"; - static const String checkLatestReleaseUrl = "https://api.github.com/repos/JGeek00/adguard-home-manager/releases/latest"; + static const String getReleasesGitHub = "https://api.github.com/repos/JGeek00/adguard-home-manager/releases"; + static const String getLatestReleaseGitHub = "https://api.github.com/repos/JGeek00/adguard-home-manager/releases/latest"; + static const String adGuardHomeReleasesTags = "https://api.github.com/repos/AdGuardTeam/AdGuardHome/releases/tags"; + static const String googleSearchUrl = "https://www.google.com/search"; + static const String connectionInstructions = "https://github.com/JGeek00/adguard-home-manager/wiki/Create-a-connection"; + static const String appDetailsWebpage = "https://apps.jgeek00.com/jlfed8mcgyz6laf"; + static const String jgeek00AppsWebpage = "https://apps.jgeek00.com"; } \ No newline at end of file diff --git a/lib/functions/app_update_download_link.dart b/lib/functions/app_update_download_link.dart new file mode 100644 index 0000000..48c798a --- /dev/null +++ b/lib/functions/app_update_download_link.dart @@ -0,0 +1,25 @@ +import 'dart:io'; + +import 'package:adguard_home_manager/models/github_release.dart'; + +String? getAppUpdateDownloadLink(GitHubRelease gitHubRelease) { + try { + if (Platform.isAndroid) { + return gitHubRelease.assets.firstWhere((item) => item.browserDownloadUrl.contains('apk')).browserDownloadUrl; + } + else if (Platform.isMacOS) { + return gitHubRelease.assets.firstWhere((item) => item.browserDownloadUrl.contains('macOS')).browserDownloadUrl; + } + else if (Platform.isWindows) { + return gitHubRelease.assets.firstWhere((item) => item.browserDownloadUrl.contains('exe')).browserDownloadUrl; + } + else if (Platform.isLinux) { + return gitHubRelease.assets.firstWhere((item) => item.browserDownloadUrl.contains('deb')).browserDownloadUrl; + } + else { + return null; + } + } catch (e) { + return null; + } +} \ No newline at end of file diff --git a/lib/functions/encode_base64.dart b/lib/functions/base64.dart similarity index 80% rename from lib/functions/encode_base64.dart rename to lib/functions/base64.dart index 3ed3d59..cb34296 100644 --- a/lib/functions/encode_base64.dart +++ b/lib/functions/base64.dart @@ -9,4 +9,8 @@ String encodeBase64UserPass(String user, String pass) { String encodeBase64(String value) { Codec stringToBase64 = utf8.fuse(base64); return stringToBase64.encode(value); +} + +String decodeBase64(String value) { + return utf8.decode(base64.decode(value)); } \ No newline at end of file diff --git a/lib/functions/block_unblock_domain.dart b/lib/functions/block_unblock_domain.dart deleted file mode 100644 index cc2c8ed..0000000 --- a/lib/functions/block_unblock_domain.dart +++ /dev/null @@ -1,61 +0,0 @@ -// ignore_for_file: use_build_context_synchronously - -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -import 'package:adguard_home_manager/classes/process_modal.dart'; -import 'package:adguard_home_manager/models/filtering_status.dart'; -import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; - -Future> blockUnblock(BuildContext context, String domain, String newStatus) async { - final serversProvider = Provider.of(context, listen: false); - final appConfigProvider = Provider.of(context, listen: false); - - final ProcessModal processModal = ProcessModal(context: context); - processModal.open(AppLocalizations.of(context)!.savingUserFilters); - - final rules = await getFilteringRules(server: serversProvider.selectedServer!); - - if (rules['result'] == 'success') { - FilteringStatus oldStatus = serversProvider.serverStatus.data!.filteringStatus; - List newRules = rules['data'].userRules.where((d) => !d.contains(domain)).toList(); - if (newStatus == 'block') { - newRules.add("||$domain^"); - } - else if (newStatus == 'unblock') { - newRules.add("@@||$domain^"); - } - FilteringStatus newObj = serversProvider.serverStatus.data!.filteringStatus; - newObj.userRules = newRules; - serversProvider.setFilteringStatus(newObj); - - final result = await postFilteringRules(server: serversProvider.selectedServer!, data: {'rules': newRules}); - - processModal.close(); - - if (result['result'] == 'success') { - return { - 'success': true, - 'message': AppLocalizations.of(context)!.userFilteringRulesUpdated - }; - } - else { - appConfigProvider.addLog(result['log']); - serversProvider.setFilteringStatus(oldStatus); - return { - 'success': false, - 'message': AppLocalizations.of(context)!.userFilteringRulesNotUpdated - }; - } - } - else { - appConfigProvider.addLog(rules['log']); - return { - 'success': false, - 'message': AppLocalizations.of(context)!.userFilteringRulesNotUpdated - }; - } -} \ No newline at end of file diff --git a/lib/functions/clear_dns_cache.dart b/lib/functions/clear_dns_cache.dart index 44c81d9..4a6fe53 100644 --- a/lib/functions/clear_dns_cache.dart +++ b/lib/functions/clear_dns_cache.dart @@ -4,25 +4,20 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:adguard_home_manager/services/api_client.dart'; import 'package:adguard_home_manager/classes/process_modal.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; import 'package:adguard_home_manager/models/server.dart'; -import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; -Future clearDnsCache(BuildContext context, Server server) async { - final ProcessModal processModal = ProcessModal(context: context); +Future clearDnsCache(BuildContext context, Server server) async { + final serversProvider = Provider.of(context, listen: false); + + final ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.clearingDnsCache); - final result = await resetDnsCache(server: server); + final result = await serversProvider.apiClient2!.resetDnsCache(); processModal.close(); - if (result['result'] == 'success') { - return true; - } - else { - final appConfigProvider = Provider.of(context, listen: false); - appConfigProvider.addLog(result['log']); - return false; - } + return result; } \ No newline at end of file diff --git a/lib/functions/compare_versions.dart b/lib/functions/compare_versions.dart new file mode 100644 index 0000000..b2f37e1 --- /dev/null +++ b/lib/functions/compare_versions.dart @@ -0,0 +1,188 @@ +import 'package:sentry_flutter/sentry_flutter.dart'; + +import 'package:adguard_home_manager/models/github_release.dart'; + +bool compareVersions({ + required String currentVersion, + required String newVersion +}) { + if (currentVersion == "") return false; + try { + if (currentVersion.contains('a')) { // alpha + return true; + } + else if (currentVersion.contains('b')) { // beta + final current = currentVersion.replaceAll('v', ''); + final newV = currentVersion.replaceAll('v', ''); + + final currentSplit = current.split('-')[0].split('.').map((e) => int.parse(e)).toList(); + final newSplit = newV.split('-')[0].split('.').map((e) => int.parse(e)).toList(); + + final currentBeta = int.parse(current.split('-')[1].replaceAll('b.', '')); + final newBeta = int.parse(newV.split('-')[1].replaceAll('b.', '')); + + if (newSplit[0] > currentSplit[0]) { + return true; + } + else if (newSplit[1] > currentSplit[1]) { + return true; + } + else if (newSplit[2] > currentSplit[2]) { + return true; + } + else if (newBeta > currentBeta) { + return true; + } + else { + return false; + } + } + else { // stable + final current = currentVersion.replaceAll('v', ''); + final newV = newVersion.replaceAll('v', ''); + + final currentSplit = current.split('.').map((e) => int.parse(e)).toList(); + final newSplit = newV.split('.').map((e) => int.parse(e)).toList(); + + if (newSplit[0] > currentSplit[0]) { + return true; + } + else if (newSplit[1] > currentSplit[1]) { + return true; + } + else if (newSplit[2] > currentSplit[2]) { + return true; + } + else { + return false; + } + } + } catch (e) { + Sentry.captureException(e); + Sentry.captureMessage("compareVersions error", params: [ + { + "fn": "compareVersions", + "currentVersion": currentVersion, + "newVersion": newVersion, + }.toString() + ]); + return false; + } +} + +bool serverVersionIsAhead({ + required String currentVersion, + required String referenceVersion, + String? referenceVersionBeta +}) { + if (currentVersion == "") return false; + try { + final current = currentVersion.replaceAll('v', ''); + final reference = referenceVersion.replaceAll('v', ''); + final referenceBeta = referenceVersionBeta?.replaceAll('v', ''); + + if (currentVersion.contains('a')) { // alpha + return true; + } + else if (current.contains('b')) { // beta + if (referenceBeta != null) { + final currentSplit = current.split('-')[0].split('.').map((e) => int.parse(e)).toList(); + final newSplit = referenceBeta.split('-')[0].split('.').map((e) => int.parse(e)).toList(); + + final currentBeta = int.parse(current.split('-')[1].replaceAll('b.', '')); + final newBeta = int.parse(referenceBeta.split('-')[1].replaceAll('b.', '')); + + if (newSplit[0] == currentSplit[0] && newSplit[1] == currentSplit[1] && newSplit[2] == currentSplit[2] && newBeta == currentBeta) { + return true; + } + else if (newSplit[0] < currentSplit[0]) { + return true; + } + else if (newSplit[1] < currentSplit[1]) { + return true; + } + else if (newSplit[2] < currentSplit[2]) { + return true; + } + else if (newBeta < currentBeta) { + return true; + } + else { + return false; + } + } + else { + return false; + } + } + else { // stable + final currentSplit = current.split('.').map((e) => int.parse(e)).toList(); + final newSplit = reference.split('.').map((e) => int.parse(e)).toList(); + + if (newSplit[0] == currentSplit[0] && newSplit[1] == currentSplit[1] && newSplit[2] == currentSplit[2]) { + return true; + } + else if (newSplit[0] < currentSplit[0]) { + return true; + } + else if (newSplit[1] < currentSplit[1]) { + return true; + } + else if (newSplit[2] < currentSplit[2]) { + return true; + } + else { + return false; + } + } + } catch (e) { + Sentry.captureException(e); + Sentry.captureMessage("serverVersionIsAhead error", params: [ + { + "fn": "serverVersionIsAhead", + "currentVersion": currentVersion, + "referenceVersion": referenceVersion, + "referenceVersionBeta": referenceVersionBeta ?? "" + }.toString() + ]); + return false; + } +} + +bool gitHubUpdateExists({ + required String currentBuildNumber, + required GitHubRelease gitHubRelease, + required bool isBeta +}) { + final versionNumberRegex = RegExp(r'\(\d+\)'); + final releaseNumberExtractedMatches = versionNumberRegex.allMatches(gitHubRelease.tagName); + + if (releaseNumberExtractedMatches.isNotEmpty) { + final releaseNumberExtracted = releaseNumberExtractedMatches.first.group(0); + + if (releaseNumberExtracted != null) { + final releaseNumber = releaseNumberExtracted.replaceAll(RegExp(r'\(|\)'), ''); + try { + final newReleaseParsed = int.parse(releaseNumber); + final currentReleaseParsed = int.parse(currentBuildNumber); + if (newReleaseParsed > currentReleaseParsed) { + return true; + } + else { + return false; + } + } catch (e) { + Sentry.captureMessage("Invalid release number. Current release: $currentBuildNumber. New release: $releaseNumber"); + return false; + } + } + else { + Sentry.captureMessage("Invalid release number. Tagname: ${gitHubRelease.tagName}"); + return false; + } + } + else { + Sentry.captureMessage("No matches. ${gitHubRelease.tagName}"); + return false; + } +} \ No newline at end of file diff --git a/lib/functions/conversions.dart b/lib/functions/conversions.dart index 37fcf4b..4eeceac 100644 --- a/lib/functions/conversions.dart +++ b/lib/functions/conversions.dart @@ -1,5 +1,3 @@ -import 'package:intl/intl.dart'; - bool? convertFromIntToBool(int value) { if (value == 1) { return true; diff --git a/lib/functions/copy_clipboard.dart b/lib/functions/copy_clipboard.dart index 87c8135..fbd13ec 100644 --- a/lib/functions/copy_clipboard.dart +++ b/lib/functions/copy_clipboard.dart @@ -3,18 +3,21 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:adguard_home_manager/config/globals.dart'; + void copyToClipboard({ - required BuildContext context, required String value, required String successMessage }) async { - await Clipboard.setData( - ClipboardData(text: value) - ); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(successMessage), - backgroundColor: Colors.green, - ) - ); + if (scaffoldMessengerKey.currentState != null) { + await Clipboard.setData( + ClipboardData(text: value) + ); + scaffoldMessengerKey.currentState!.showSnackBar( + SnackBar( + content: Text(successMessage), + backgroundColor: Colors.green, + ) + ); + } } \ No newline at end of file diff --git a/lib/functions/desktop_mode.dart b/lib/functions/desktop_mode.dart new file mode 100644 index 0000000..4586068 --- /dev/null +++ b/lib/functions/desktop_mode.dart @@ -0,0 +1,5 @@ +import 'package:adguard_home_manager/config/sizes.dart'; + +bool isDesktop(double width) { + return width > desktopBreakpoint; +} \ No newline at end of file diff --git a/lib/functions/format_time.dart b/lib/functions/format_time.dart index e5baff0..41f0dd4 100644 --- a/lib/functions/format_time.dart +++ b/lib/functions/format_time.dart @@ -13,4 +13,17 @@ String convertTimestampLocalTimezone(DateTime timestamp, String format) { String formatTimeOfDay(TimeOfDay timestamp, String format) { DateFormat f = DateFormat(format); return f.format(DateTime(0, 0, 0, timestamp.hour, timestamp.minute)); +} + +String formatRemainingSeconds(int seconds) { + int h, m, s; + h = seconds ~/ 3600; + m = ((seconds - h * 3600)) ~/ 60; + s = seconds - (h * 3600) - (m * 60); + + String hourLeft = h.toString().length < 2 ? "0$h" : h.toString(); + String minuteLeft = m.toString().length < 2 ? "0$m" : m.toString(); + String secondsLeft = s.toString().length < 2 ? "0$s" : s.toString(); + + return "$hourLeft:$minuteLeft:$secondsLeft"; } \ No newline at end of file diff --git a/lib/functions/get_filtered_status.dart b/lib/functions/get_filtered_status.dart index 7cddfab..6825ebf 100644 --- a/lib/functions/get_filtered_status.dart +++ b/lib/functions/get_filtered_status.dart @@ -126,4 +126,43 @@ Map getFilteredStatus(BuildContext context, AppConfigProvider a default: return {'filtered': null, 'label': 'Unknown'}; } +} + +bool isDomainBlocked(String filterKey) { + switch (filterKey) { + case 'NotFilteredNotFound': + return false; + + case 'NotFilteredWhiteList': + return false; + + case 'NotFilteredError': + return false; + + case 'FilteredBlackList': + return true; + + case 'FilteredSafeBrowsing': + return true; + + case 'FilteredParental': + return true; + + case 'FilteredInvalid': + return true; + + case 'FilteredSafeSearch': + return true; + + case 'FilteredBlockedService': + return true; + + case 'Rewrite': + case 'RewriteEtcHosts': + case 'RewriteRule': + return false; + + default: + return false; + } } \ No newline at end of file diff --git a/lib/functions/is_ip.dart b/lib/functions/is_ip.dart new file mode 100644 index 0000000..73ee1e9 --- /dev/null +++ b/lib/functions/is_ip.dart @@ -0,0 +1,10 @@ +import 'package:adguard_home_manager/constants/regexps.dart'; + +bool isIpAddress(String value) { + if (Regexps.ipv4Address.hasMatch(value) || Regexps.ipv6Address.hasMatch(value)) { + return true; + } + else { + return false; + } +} \ No newline at end of file diff --git a/lib/functions/maps_fns.dart b/lib/functions/maps_fns.dart new file mode 100644 index 0000000..974e5f0 --- /dev/null +++ b/lib/functions/maps_fns.dart @@ -0,0 +1,5 @@ +Map removePropFromMap(Map o, String prop) { + Map obj = {...o}; + obj.remove(prop); + return o; +} \ No newline at end of file diff --git a/lib/functions/open_url.dart b/lib/functions/open_url.dart new file mode 100644 index 0000000..599eea0 --- /dev/null +++ b/lib/functions/open_url.dart @@ -0,0 +1,43 @@ +import 'dart:io'; + +import 'package:flutter_custom_tabs/flutter_custom_tabs.dart' as flutter_custom_tabs; +import 'package:url_launcher/url_launcher.dart' as url_launcher; +import 'package:sentry_flutter/sentry_flutter.dart'; + +void openUrl(String url) async { + if (!(url.startsWith("http") || url.startsWith("https"))) { + try { + url_launcher.launchUrl(Uri.parse(url)); + } catch (e, stackTrace) { + Sentry.captureException(e, stackTrace: stackTrace); + } + return; + } + + if (Platform.isAndroid || Platform.isIOS) { + try { + await flutter_custom_tabs.launchUrl( + Uri.parse(url), + customTabsOptions: const flutter_custom_tabs.CustomTabsOptions( + shareState: flutter_custom_tabs.CustomTabsShareState.browserDefault, + urlBarHidingEnabled: true, + showTitle: true, + ), + safariVCOptions: const flutter_custom_tabs.SafariViewControllerOptions( + barCollapsingEnabled: true, + dismissButtonStyle: flutter_custom_tabs.SafariViewControllerDismissButtonStyle.close, + ), + ); + } catch (e, stackTrace) { + url_launcher.launchUrl(Uri.parse(url)); + Sentry.captureException(e, stackTrace: stackTrace); + } + } + else { + try { + url_launcher.launchUrl(Uri.parse(url)); + } catch (e, stackTrace) { + Sentry.captureException(e, stackTrace: stackTrace); + } + } +} \ No newline at end of file diff --git a/lib/functions/snackbar.dart b/lib/functions/snackbar.dart index ac74407..d0d7bc0 100644 --- a/lib/functions/snackbar.dart +++ b/lib/functions/snackbar.dart @@ -3,28 +3,32 @@ import 'package:flutter/material.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; +import 'package:adguard_home_manager/config/globals.dart'; -void showSnacbkar({ - required BuildContext context, +void showSnackbar({ required AppConfigProvider appConfigProvider, required String label, required Color color, - Color? labelColor + Color? labelColor, + GlobalKey? key, }) async { + final GlobalKey scaffoldKey = key ?? scaffoldMessengerKey; if (appConfigProvider.showingSnackbar == true) { - ScaffoldMessenger.of(context).clearSnackBars(); + scaffoldKey.currentState?.clearSnackBars(); await Future.delayed(const Duration(milliseconds: 500)); } appConfigProvider.setShowingSnackbar(true); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - label, - style: TextStyle( - color: labelColor ?? Colors.white - ), + + final snackBar = SnackBar( + content: Text( + label, + style: TextStyle( + color: labelColor ?? Colors.white ), - backgroundColor: color, - ) - ).closed.then((value) => appConfigProvider.setShowingSnackbar(false)); + ), + backgroundColor: color, + ); + scaffoldKey.currentState?.showSnackBar(snackBar).closed.then( + (value) => appConfigProvider.setShowingSnackbar(false) + ); } \ No newline at end of file diff --git a/lib/functions/time_server_disabled.dart b/lib/functions/time_server_disabled.dart new file mode 100644 index 0000000..07b6f2b --- /dev/null +++ b/lib/functions/time_server_disabled.dart @@ -0,0 +1,11 @@ +DateTime generateTimeDeadline(int time) { + DateTime date = DateTime.now(); + date = date.add(Duration(milliseconds: time)); + return date; +} + +String generateRemainingTimeString(Duration difference) { + final int seconds = difference.inSeconds+1; + final DateTime time = DateTime(0, 0, 0, 0, 0, seconds > 0 ? seconds : 0); + return "${time.hour > 0 ? "${time.hour}:" : ''}${time.minute.toString().padLeft(2, '0')}:${time.second.toString().padLeft(2, '0')}"; +} \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b4463a7..f923e8b 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -4,6 +4,7 @@ "connect": "Connect", "servers": "Servers", "createConnection": "Create connection", + "editConnection": "Edit connection", "name": "Name", "ipDomain": "IP address or domain", "path": "Path", @@ -23,7 +24,7 @@ "invalidUsernamePassword": "Invalid username or password", "tooManyAttempts": "Too many attempts. Try again later.", "cantReachServer": "Can't reach server. Check connection data.", - "sslError": "SSL error", + "sslError": "Handshake exception. Cannot establish a secure connection with the server. This can be a SSL error. Go to Settings > Advanced settings and enable Override SSL validation.", "unknownError": "Unknown error", "connectionNotCreated": "Connection couldn't be created", "connecting": "Connecting...", @@ -45,10 +46,10 @@ "save": "Save", "serverStatus": "Server status", "connectionNotUpdated": "Connection not updated", - "ruleFilteringWidget": "Rule\nfiltering", - "safeBrowsingWidget": "Safe\nbrowsing", - "parentalFilteringWidget": "Parental\nfiltering", - "safeSearchWidget": "Safe\nsearch", + "ruleFilteringWidget": "Rule filtering", + "safeBrowsingWidget": "Safe browsing", + "parentalFilteringWidget": "Parental filtering", + "safeSearchWidget": "Safe search", "ruleFiltering": "Rule filtering", "safeBrowsing": "Safe browsing", "parentalFiltering": "Parental filtering", @@ -56,10 +57,10 @@ "serverStatusNotRefreshed": "Server status could not be refreshed", "loadingStatus": "Loading status...", "errorLoadServerStatus": "Server status could not be loaded", - "topQueriedDomains": "Top queried domains", + "topQueriedDomains": "Queried domains", "viewMore": "View more", - "topClients": "Top clients", - "topBlockedDomains": "Top blocked domains", + "topClients": "Clients", + "topBlockedDomains": "Blocked domains", "appSettings": "App settings", "theme": "Theme", "light": "Light", @@ -170,7 +171,7 @@ "dnsQueries": "DNS queries", "average": "Average", "blockedFilters": "Blocked by filters", - "malwarePhisingBlocked": "Blocked malware/phising", + "malwarePhishingBlocked": "Blocked malware/phishing", "blockedAdultWebsites": "Blocked adult websites", "generalSettings": "General settings", "generalSettingsDescription": "Various different settings", @@ -280,7 +281,7 @@ "accessSettingsDescription": "Configure access rules for the server", "loadingClients": "Loading clients...", "clientsNotLoaded": "Clients couldn't be loaded.", - "noAllowedClients": "No hay clientes permitidos", + "noAllowedClients": "No allowed clients", "allowedClientsDescription": "If this list has entries, AdGuard Home will accept requests only from these clients.", "blockedClientsDescription": "If this list has entries, AdGuard Home will drop requests from these clients. This field is ignored if there are entries in Allowed clients.", "disallowedDomainsDescription": "AdGuard Home drops DNS queries matching these domains, and these queries don't even appear in the query log.", @@ -316,6 +317,7 @@ "deletingRule": "Deleting rule...", "enablingList": "Enabling list...", "disablingList": "Disabling list...", + "savingList": "Saving list...", "disableFiltering": "Disable filtering", "enablingFiltering": "Enabling filtering...", "disablingFiltering": "Disabling filtering...", @@ -355,7 +357,7 @@ "gateway": "Gateway", "gatewayNotValid": "Gateway not valid", "leaseTime": "Lease time", - "seconds": "seconds", + "seconds": "{time} seconds", "leaseTimeNotValid": "Lease time not valid", "restoreConfiguration": "Reset configuration", "restoreConfigurationMessage": "Are you sure you want to continue? This will reset all the configuration. This action cannot be undone.", @@ -558,8 +560,8 @@ "validPrivateKey": "Valid private key", "expirationDate": "Expiration date", "keysNotMatch": "Invalid certificate or key: tls: private key does not match public key", - "nameTimeLogs": "Name and time on logs", - "nameTimeLogsDescription": "Show client name and processing time on logs list", + "timeLogs": "Time on logs", + "timeLogsDescription": "Show processing time on logs list", "hostNames": "Host names", "keyType": "Key type", "updateAvailable": "Update available", @@ -584,5 +586,225 @@ "clientsSelected": "clients selected", "invalidDomain": "Invalid domain", "loadingBlockedServicesList": "Loading blocked services list...", - "blockedServicesListNotLoaded": "The blocked services list could not be loaded" + "blockedServicesListNotLoaded": "The blocked services list could not be loaded", + "error": "Error", + "updates": "Updates", + "updatesDescription": "Update the AdGuard Home server", + "updateNow": "Update now", + "currentVersion": "Current version", + "requestStartUpdateFailed": "Request to start update failed", + "requestStartUpdateSuccessful": "Request to start update successfull", + "serverUpdated": "Server is updated", + "unknownStatus": "Unknown status", + "checkingUpdates": "Checking updates...", + "checkUpdates": "Check updates", + "requestingUpdate": "Requesting update...", + "autoupdateUnavailable": "Autoupdate unavailable", + "autoupdateUnavailableDescription": "The autoupdate service is not available for this server. It could be because the server is running on a Docker container. You have to update your server manually.", + "minute": "{time} minute", + "minutes": "{time} minutes", + "hour": "{time} hour", + "hours": "{time} hours", + "remainingTime": "Remaining time", + "safeSearchSettings": "Safe search settings", + "loadingSafeSearchSettings": "Loading safe search settings...", + "safeSearchSettingsNotLoaded": "Error when loading safe search settings.", + "loadingLogsSettings": "Loading logs settings...", + "selectOptionLeftColumn": "Select an option of the left column", + "selectClientLeftColumn": "Select a client of the left column", + "disableList": "Disable list", + "enableList": "Enable list", + "screens": "Screens", + "copiedClipboard": "Copied to clipboard", + "seeDetails": "See details", + "listNotAvailable": "List not available", + "copyListUrl": "Copy list URL", + "listUrlCopied": "List URL copied to the clipboard", + "unsupportedVersion": "Unsupported version", + "unsupprtedVersionMessage": "The support for your server version {version} is not guaranteed. This application may have some issues working with that server version.\n\nAdGuard Home Manager is designed to work with the stable releases of the AdGuard Home server. It may work with alpha and beta releases, but the compatibility is not guaranteed and the app may have some issues working with that versions.", + "iUnderstand": "I understand", + "appUpdates": "Application updates", + "usingLatestVersion": "You are using the latest version", + "ipLogs": "IP on logs", + "ipLogsDescription": "Show always IP address on logs instead of client name", + "application": "Application", + "combinedChart": "Combined chart", + "combinedChartDescription": "Combine all charts into one", + "statistics": "Statistics", + "errorLoadFilters": "Error when loading filters.", + "clientRemovedSuccessfully": "Client removed successfully.", + "editRewriteRule": "Edit rewrite rule", + "dnsRewriteRuleUpdated": "DNS rewrite rule updated successfully", + "dnsRewriteRuleNotUpdated": "DNS rewrite rule could not be updated", + "updatingRule": "Updating rule...", + "serverUpdateNeeded": "Server update needed", + "updateYourServer": "Update your AdGuard Home server to {version} or greater to use this feature.", + "january": "January", + "february": "February", + "march": "March", + "april": "April", + "may": "May", + "june": "June", + "july": "July", + "august": "August", + "september": "September", + "october": "October", + "november": "November", + "december": "December", + "malwarePhishing": "Malware/phishing", + "queries": "Queries", + "adultSites": "Adult sites", + "quickFilters": "Quick filters", + "searchDomainInternet": "Search domain on the Internet", + "hideServerAddress": "Hide server address", + "hideServerAddressDescription": "Hides the server address on the home screen", + "topItemsOrder": "Top items order", + "topItemsOrderDescription": "Order the home screen top items lists", + "topItemsReorderInfo": "Hold and swipe an item to reorder it.", + "discardChanges": "Discard changes", + "discardChangesDescription": "Are you sure you want to discard the changes?", + "others": "Others", + "showChart": "Show chart", + "hideChart": "Hide chart", + "showTopItemsChart": "Show top items chart", + "showTopItemsChartDescription": "Shows by default the ring chart on the top items sections. Only affects to the mobile view.", + "openMenu": "Open menu", + "closeMenu": "Close menu", + "openListUrl": "Open list URL", + "selectionMode": "Selection mode", + "enableDisableSelected": "Enable or disable selected items", + "deleteSelected": "Delete selected items", + "deleteSelectedLists": "Delete selected lists", + "allSelectedListsDeletedSuccessfully": "All selected lists have been deleted successfully.", + "deletionResult": "Deletion result", + "deletingLists": "Deleting lists...", + "failedElements": "Failed elements", + "processingLists": "Processing lists...", + "enableDisableResult": "Enable or disable result", + "selectedListsEnabledDisabledSuccessfully": "All selected lists have been enabled or disabled successfully", + "sslWarning": "If you are using an HTTPS connection with a self signed certificate, make sure to enable \"Don't check SSL certificate\" at Settings > Advanced settings.", + "unsupportedServerVersion": "Unsupported server version", + "unsupportedServerVersionMessage": "Your AdGuard Home server version is too old and is not supported by AdGuard Home Manager. You will need to upgrade your AdGuard Home server to a newer version to use this application.", + "yourVersion": "Your version: {version}", + "minimumRequiredVersion": "Minimum required version: {version}", + "topUpstreams": "Top upstreams", + "averageUpstreamResponseTime": "Average upstream response time", + "dhcpNotAvailable": "The DHCP server is not available.", + "osServerInstalledIncompatible": "The OS where the server is installed is not compatible with this feature.", + "resetSettings": "Reset settings", + "resetEncryptionSettingsDescription": "Are you sure you want to reset to default values the encryption settings?", + "resettingConfig": "Resetting configuration...", + "configurationResetSuccessfully": "Configuration resetted successfully", + "configurationResetError": "The configuration couldn't be resetted", + "testUpstreamDnsServers": "Test upstream DNS servers", + "errorTestUpstreamDns": "Error when testing upstream DNS servers.", + "useCustomIpEdns": "Use custom IP for EDNS", + "useCustomIpEdnsDescription": "Allow to use custom IP for EDNS", + "sortingOptions": "Sorting options", + "fromHighestToLowest": "From highest to lowest", + "fromLowestToHighest": "From lowest to highest", + "queryLogsAndStatistics": "Query logs and statistics", + "ignoreClientQueryLog": "Ignore this client in query log", + "ignoreClientStatistics": "Ignore this client in statistics", + "savingChanges": "Saving changes...", + "fallbackDnsServers": "Fallback DNS servers", + "fallbackDnsServersDescription": "Configure fallback DNS servers", + "fallbackDnsServersInfo": "List of fallback DNS servers used when upstream DNS servers are not responding. The syntax is the same as in the main upstreams field above.", + "noFallbackDnsAdded": "No fallback DNS servers added.", + "blockedResponseTtl": "Blocked response TTL", + "blockedResponseTtlDescription": "Specifies for how many seconds the clients should cache a filtered response", + "invalidValue": "Invalid value", + "noDataChart": "There's no data to display this chart.", + "noData": "No data", + "unblockClient": "Unblock client", + "blockingClient": "Blocking client...", + "unblockingClient": "Unblocking client...", + "upstreamDnsCacheConfiguration": "DNS upstream cache configuration", + "enableDnsCachingClient": "Enable DNS caching for this client", + "dnsCacheSize": "DNS cache size", + "nameInvalid": "Name is required", + "oneIdentifierRequired": "At least one identifier is required", + "dnsCacheNumber": "DNS cache size must be a number", + "errors": "Errors", + "redirectHttpsWarning": "If you have enabled \"Redirect to HTTPS automatically\" on your AdGuard Home server, you must select an HTTPS connection and use the HTTPS port of your server.", + "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.", + "statisticsConfigUpdated": "Statistics settings updated successfully", + "statisticsConfigNotUpdated": "Statistics settings couldn't be updated", + "customTimeInHours": "Custom time (in hours)", + "invalidTime": "Invalid time", + "removeDomain": "Remove domain", + "addDomain": "Add domain", + "notLess1Hour": "Time cannot be less than 1 hour", + "rateLimit": "Rate limit", + "subnetPrefixLengthIpv4": "Subnet prefix length for IPv4", + "subnetPrefixLengthIpv6": "Subnet prefix length for IPv6", + "rateLimitAllowlist": "Rate limit allowlist", + "rateLimitAllowlistDescription": "IP addresses excluded from rate limiting", + "dnsOptions": "DNS options", + "editor": "Editor", + "editCustomRules": "Edit custom rules", + "savingCustomRules": "Saving custom rules...", + "customRulesUpdatedSuccessfully": "Custom rules updated successfully", + "customRulesNotUpdated": "Custom rules could not be updated", + "reorder": "Reorder", + "showHide": "Show/hide", + "noElementsReorderMessage": "Enable some elements on the show/hide tab to reorder them here.", + "enablePlainDns": "Enable plain DNS", + "enablePlainDnsDescription": "Plain DNS is enabled by default. You can disable it to force all devices to use encrypted DNS. To do this, you must enable at least one encrypted DNS protocol.", + "date": "Date", + "loadingChangelog": "Loading changelog...", + "invalidIpOrUrl": "Invalid IP address or URL", + "addPersistentClient": "Add as a persistent client", + "blockThisClientOnly": "Block for this client only", + "unblockThisClientOnly": "Unblock for this client only", + "domainBlockedThisClient": "{domain} blocked for this client", + "domainUnblockedThisClient": "{domain} unblocked for this client", + "disallowThisClient": "Disallow this client", + "allowThisClient": "Allow this client", + "clientAllowedSuccessfully": "Client allowed successfully", + "clientDisallowedSuccessfully": "Client disallowed successfully", + "changesNotSaved": "Changes could not be saved", + "allowingClient": "Allowing client...", + "disallowingClient": "Disallowing client...", + "clientIpCopied": "Client IP copied to the clipboard", + "clientNameCopied": "Client name copied to the clipboard", + "dnsServerAddressCopied": "DNS server address copied to the clipboard", + "select": "Select", + "liveLogs": "Live logs", + "hereWillAppearRealtimeLogs": "Here there will appear the logs on realtime.", + "applicationDetails": "Application details", + "applicationDetailsDescription": "App repository, stores where it's available, and more", + "myOtherApps": "My other apps", + "myOtherAppsDescription": "Check my other apps, make a donation, contact support, and more", + "topToBottom": "From top to bottom", + "bottomToTop": "From bottom to top", + "upstreamTimeout": "Upstream timeout", + "upstreamTimeoutHelper": "Specifies the number of seconds to wait for a response from the upstream server", + "fieldCannotBeEmpty": "This field cannot be empty" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 95b7323..c93c501 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -4,6 +4,7 @@ "connect": "Conectar", "servers": "Servidores", "createConnection": "Crear conexión", + "editConnection": "Editar conexión", "name": "Nombre", "ipDomain": "Dirección IP o dominio", "path": "Ruta", @@ -23,7 +24,7 @@ "invalidUsernamePassword": "Usuario o contraseña no válidos.", "tooManyAttempts": "Demasiados intentos. Prueba de nuevo más tarde.", "cantReachServer": "No se puede alcanzar el servidor. Comprueba los datos de conexión.", - "sslError": "Error de SSL", + "sslError": "Handshake exception. No se ha podido establecer una conexión segura con el servidor. Es posible que sea un error de SSL. Ve a Ajustes > Ajustes avanzados y activa No comprobar SSL.", "unknownError": "Error desconocido", "connectionNotCreated": "No se pudo crear la conexión", "connecting": "Conectando...", @@ -45,10 +46,10 @@ "save": "Guardar", "connectionNotUpdated": "Conexión no actualizada", "serverStatus": "Estado del servidor", - "ruleFilteringWidget": "Bloqueo por\nfiltros", - "safeBrowsingWidget": "Navegación\nsegura", - "parentalFilteringWidget": "Control\nparental", - "safeSearchWidget": "Búsqueda\nsegura", + "ruleFilteringWidget": "Bloqueo por filtros", + "safeBrowsingWidget": "Navegación segura", + "parentalFilteringWidget": "Control parental", + "safeSearchWidget": "Búsqueda segura", "ruleFiltering": "Bloqueo por filtros", "safeBrowsing": "Navegación segura", "parentalFiltering": "Control parental", @@ -56,10 +57,10 @@ "serverStatusNotRefreshed": "No se ha podido actualizar el estado del servidor", "loadingStatus": "Cargando estado...", "errorLoadServerStatus": "Error al cargar el estado", - "topQueriedDomains": "Top dominios solicitados", + "topQueriedDomains": "Dominios solicitados", "viewMore": "Ver más", - "topClients": "Top clientes recurrentes", - "topBlockedDomains": "Top dominios bloqueados", + "topClients": "Clientes recurrentes", + "topBlockedDomains": "Dominios bloqueados", "appSettings": "Ajustes de la app", "theme": "Tema", "light": "Claro", @@ -170,7 +171,7 @@ "dnsQueries": "Consultas DNS", "average": "Promedio", "blockedFilters": "Bloqueado por filtros", - "malwarePhisingBlocked": "Malware/phising bloqueado", + "malwarePhishingBlocked": "Malware/phising bloqueado", "blockedAdultWebsites": "Sitios para adultos bloqueados", "generalSettings": "Ajustes generales", "generalSettingsDescription": "Varios ajustes generales", @@ -316,6 +317,7 @@ "deletingRule": "Eliminando regla...", "enablingList": "Habilitando lista...", "disablingList": "Deshabilitando lista...", + "savingList": "Guardando lista...", "disableFiltering": "Deshabilitar filtrado", "enablingFiltering": "Habilitando filtrado...", "disablingFiltering": "Deshabilitando filtrado...", @@ -355,7 +357,7 @@ "gateway": "Puerta de enlace", "gatewayNotValid": "Puerta de enlace no válida", "leaseTime": "Tiempo de asignación", - "seconds": "segundos", + "seconds": "{time} segundos", "leaseTimeNotValid": "Tiempo de asignación no válido", "restoreConfiguration": "Restaurar configuración", "restoreConfigurationMessage": "¿Estás seguro de que deseas continuar? Se reseteará toda la configuración. Esta acción no se puede deshacer.", @@ -401,7 +403,7 @@ "dnsRewriteRuleDeleted": "Reescritura DNS eliminada correctamente", "dnsRewriteRuleNotDeleted": "La reescritura DNS no pudo ser eliminada", "addDnsRewrite": "Añadir reescritura DNS", - "addingRewrite": "Añadiend reescritura...", + "addingRewrite": "Añadiendo reescritura...", "dnsRewriteRuleAdded": "Regla de reescritura DNS añadida correctamente", "dnsRewriteRuleNotAdded": "La regla de reescritura DNS no ha podido ser añadida", "logsSettings": "Ajustes de registros", @@ -558,8 +560,8 @@ "validPrivateKey": "Clave privada válida", "expirationDate": "Fecha de expiración", "keysNotMatch": "Certificado o clave inválido: tls: la clave privada no corresponde con la clave pública", - "nameTimeLogs": "Nombre y tiempo en logs", - "nameTimeLogsDescription": "Mostrar el nombre del cliente y el tiempo de procesamiento en la lista de logs", + "timeLogs": "Tiempo en logs", + "timeLogsDescription": "Mostrar el tiempo de procesamiento en la lista de logs", "hostNames": "Nombres de host", "keyType": "Tipo de clave", "updateAvailable": "Actualización disponible", @@ -584,5 +586,225 @@ "clientsSelected": "clientes seleccionados", "invalidDomain": "Dominio no válido", "loadingBlockedServicesList": "Cargando lista de servicios bloqueados...", - "blockedServicesListNotLoaded": "No se ha podido cargar la lista de servicios bloqueados" + "blockedServicesListNotLoaded": "No se ha podido cargar la lista de servicios bloqueados", + "error": "Error", + "updates": "Actualizaciones", + "updatesDescription": "Actualiza el servidor AdGuard Home", + "updateNow": "Actualizar ahora", + "currentVersion": "Versión actual", + "requestStartUpdateFailed": "Petición para iniciar la actualización fallida", + "requestStartUpdateSuccessful": "Petición para iniciar la actualización satisfactoria", + "serverUpdated": "Servidor actualizado", + "unknownStatus": "Estado desconocido", + "checkingUpdates": "Comprobando actualizaciones...", + "checkUpdates": "Comprobar actualizaciones", + "requestingUpdate": "Solicitando actualización...", + "autoupdateUnavailable": "Autoactualización no disponible", + "autoupdateUnavailableDescription": "El servicio de actualización automática del servidor no está disponible. Puede ser porque el servidor se esté ejecutando en un contenedor Docker. Tienes que actualizar tu servidor manualmente.", + "minute": "{time} minuto", + "minutes": "{time} minutos", + "hour": "{time} hora", + "hours": "{time} horas", + "remainingTime": "Tiempo restante", + "safeSearchSettings": "Configuración de búsqueda segura", + "loadingSafeSearchSettings": "Cargando configuración de búsqueda segura...", + "safeSearchSettingsNotLoaded": "Error al cargar la configuración de búsqueda segura.", + "loadingLogsSettings": "Cargando configuración de registros...", + "selectOptionLeftColumn": "Selecciona una opción de la columna de la izquierda", + "selectClientLeftColumn": "Selecciona un cliente de la columna de la izquierda", + "disableList": "Deshabilitar lista", + "enableList": "Habilitar lista", + "screens": "Pantallas", + "copiedClipboard": "Copiado al portapapeles", + "seeDetails": "Ver los detalles", + "listNotAvailable": "Lista no disponible", + "copyListUrl": "Copiar URL de lista", + "listUrlCopied": "URL de la lista copiada al portapapeles", + "unsupportedVersion": "Versión no soportada", + "unsupprtedVersionMessage": "El soporte para la versión del servidor {version} no está garantizada. Esta aplicación puede tener problemas al trabajar con esa versión del servidor.\n\nAdGuard Home Manager está diseñado para trabajar con las versiones estables del servidor AdGuard Home. Puede funcionar con versiones alpha y beta, pero la compatibilidad no está asegurada y la aplicación puede tener problemas para trabajar con esas versiones.", + "iUnderstand": "Lo entiendo", + "appUpdates": "Actualizaciones de la app", + "usingLatestVersion": "Estás usando la última versión", + "ipLogs": "IP en registros", + "ipLogsDescription": "Mostrar siempre dirección IP en vez del nombre del cliente", + "application": "Aplicación", + "combinedChart": "Gráfico combinado", + "combinedChartDescription": "Combina todos los gráficos en uno solo", + "statistics": "Estadísticas", + "errorLoadFilters": "Error al cargar los filtros.", + "clientRemovedSuccessfully": "Cliente eliminado correctamente.", + "editRewriteRule": "Editar reescritura DNS", + "dnsRewriteRuleUpdated": "Regla de reescritura DNS actualizada correctamente", + "dnsRewriteRuleNotUpdated": "La regla de reescritura DNS no ha podido ser actualizada", + "updatingRule": "Actualizando regla...", + "serverUpdateNeeded": "Actualización del servidor necesaria", + "updateYourServer": "Actualiza tu servidor AdGuard Home a {version} para utilizar esta funcionalidad.", + "january": "Enero", + "february": "Febrero", + "march": "Marzo", + "april": "Abril", + "may": "Mayo", + "june": "Junio", + "july": "Julio", + "august": "Agosto", + "september": "Septiembre", + "october": "Octubre", + "november": "Noviembre", + "december": "Diciembre", + "malwarePhishing": "Malware/phising", + "queries": "Peticiones", + "adultSites": "Sitios de adultos", + "quickFilters": "Filtros rápidos", + "searchDomainInternet": "Buscar dominio en internet", + "hideServerAddress": "Ocultar dirección del servidor", + "hideServerAddressDescription": "Oculta la dirección del servidor en la pantalla de inicio", + "topItemsOrder": "Orden de los top", + "topItemsOrderDescription": "Ordena las listas de top de elementos en la pantalla de inicio", + "topItemsReorderInfo": "Mantén presionado y desliza un elemento para reordenarlo.", + "discardChanges": "Descartar cambios", + "discardChangesDescription": "¿Estás seguro de que deseas descartar los cambios realizados?", + "others": "Otros", + "showChart": "Mostrar gráfico", + "hideChart": "Ocultar gráfico", + "showTopItemsChart": "Mostrar gráfico en top de items", + "showTopItemsChartDescription": "Muestra por defecto el gráfico de anillo en las secciones de top de items. Sólo afecta a la vista móvil.", + "openMenu": "Abrir menú", + "closeMenu": "Cerrar menú", + "openListUrl": "Abrir URL de lista", + "selectionMode": "Modo de selección", + "enableDisableSelected": "Activar o desactivar elementos seleccionados", + "deleteSelected": "Eliminar elementos seleccionados", + "deleteSelectedLists": "Eliminar listas seleccionadas", + "allSelectedListsDeletedSuccessfully": "Todas las listas seleccionadas han sido eliminadas correctamente.", + "deletionResult": "Resultado de eliminación", + "deletingLists": "Eliminando listas...", + "failedElements": "Elementos fallidos", + "processingLists": "Procesando listas...", + "enableDisableResult": "Resultado de activar o desactivar", + "selectedListsEnabledDisabledSuccessfully": "Todas las listas seleccionadas se han activado o desactivado correctamente.", + "sslWarning": "Si estás usando una conexión HTTPS con un certificado autofirmado, asegúrate de activar \"No comprobar el certificado SSL\" en Ajustes > Ajustes avanzados.", + "unsupportedServerVersion": "Versión del servidor no soportada", + "unsupportedServerVersionMessage": "La versión de tu servidor AdGuard Home es demasiado antigua y no está soportada por AdGuard Home Manager. Necesitarás actualizar tu servidor AdGuard Home a una versión más actual para utilizar esta aplicación.", + "yourVersion": "Tu versión: {version}", + "minimumRequiredVersion": "Versión mínima requerida: {version}", + "topUpstreams": "DNS de subida más frecuentes", + "averageUpstreamResponseTime": "Tiempo promedio de respuesta upstream", + "dhcpNotAvailable": "El servidor DHCP no está disponible.", + "osServerInstalledIncompatible": "El SO donde el servidor está instalado no es compatible con esta característica.", + "resetSettings": "Resetear configuración", + "resetEncryptionSettingsDescription": "Estás seguro que deseas restaurar a valores por defecto la configuración de encriptación?", + "resettingConfig": "Reseteando configuración...", + "configurationResetSuccessfully": "Configuración reseteada correctamente", + "configurationResetError": "La configuración no ha podido ser reseteada", + "testUpstreamDnsServers": "Probar servidores DNS de subida", + "errorTestUpstreamDns": "Error al probar los servidores DNS de subida.", + "useCustomIpEdns": "Usar IP personalizada para EDNS", + "useCustomIpEdnsDescription": "Permitir usar IP personalizada para EDNS", + "sortingOptions": "Opciones de ordenación", + "fromHighestToLowest": "De mayor a menor", + "fromLowestToHighest": "De menor a mayor", + "queryLogsAndStatistics": "Registro de consultas y estadísticas", + "ignoreClientQueryLog": "Ignorar este cliente en el registro de consultas", + "ignoreClientStatistics": "Ignorar este cliente en las estadísticas", + "savingChanges": "Guardando cambios...", + "fallbackDnsServers": "Servidores DNS alternativos", + "fallbackDnsServersDescription": "Configura los servidores DNS alternativos", + "fallbackDnsServersInfo": "Lista de servidores DNS alternativos utilizados cuando los servidores DNS de subida no responden. La sintaxis es la misma que en el campo de los principales DNS de subida anterior.", + "noFallbackDnsAdded": "No hay servidores DNS alternativos añadidos.", + "blockedResponseTtl": "Respuesta TTL bloqueada", + "blockedResponseTtlDescription": "Especifica durante cuántos segundos los clientes deben almacenar en cache una respuesta filtrada", + "invalidValue": "Valor no válido", + "noDataChart": "No hay datos para mostrar este gráfico.", + "noData": "No hay datos", + "unblockClient": "Desbloquear cliente", + "blockingClient": "Bloqueando cliente...", + "unblockingClient": "Desbloqueando cliente...", + "upstreamDnsCacheConfiguration": "Configuración de la caché DNS upstream", + "enableDnsCachingClient": "Habilitar caché de DNS para este cliente", + "dnsCacheSize": "Tamaño de caché de DNS", + "nameInvalid": "Se requiere un nombre", + "oneIdentifierRequired": "Se require al menos un identificador", + "dnsCacheNumber": "El tamaño de caché de DNS debe ser un número", + "errors": "Errores", + "redirectHttpsWarning": "Si tienes activado \"Redireccionar a HTTPS automáticamente\" en tu servidor AdGuard Home, debes seleccionar una conexión HTTPS y utilizar el puerto de HTTPS de tu servidor.", + "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.", + "statisticsConfigNotUpdated": "La configuración de estadísticas no pudo ser actualizada.", + "statisticsConfigUpdated": "Configuración de estadísticas actualizada correctamente.", + "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", + "rateLimit": "Limitación de velocidad", + "subnetPrefixLengthIpv4": "Longitud del prefijo de subred para IPv4", + "subnetPrefixLengthIpv6": "Longitud del prefijo de subred para IPv6", + "rateLimitAllowlist": "Lista de permitidos de limitación de velocidad", + "rateLimitAllowlistDescription": "Direcciones IP excluidas de la limitación de velocidad", + "dnsOptions": "Opciones de DNS", + "editor": "Editor", + "editCustomRules": "Editar reglas personalizadas", + "savingCustomRules": "Guardando reglas personalizadas...", + "customRulesUpdatedSuccessfully": "Reglas personalizadas actualizadas correctamente", + "customRulesNotUpdated": "Las reglas personalizadas no pudieron ser actualizadas", + "reorder": "Reordenar", + "showHide": "Mostrar/ocultar", + "noElementsReorderMessage": "Activa algunos elementos en la pestaña de mostrar/ocultar para reordenarlos aquí.", + "enablePlainDns": "Activar DNS simple (sin cifrado)", + "enablePlainDnsDescription": "El DNS simple (sin cifrado) está activado de forma predeterminada. Puedes desactivarlo para obligar a todos los dispositivos a utilizar DNS cifrado. Para ello, debes habilitar al menos un protocolo DNS cifrado.", + "date": "Fecha", + "loadingChangelog": "Cargando registro de cambios...", + "invalidIpOrUrl": "Dirección IP o URL no válida", + "addPersistentClient": "Añadir como cliente persistente", + "blockThisClientOnly": "Bloquear sólo para este cliente", + "unblockThisClientOnly": "Desbloquear sólo para este cliente", + "domainBlockedThisClient": "{domain} bloqueado para este cliente", + "domainUnblockedThisClient": "{domain} desbloqueado para este cliente", + "disallowThisClient": "No permitir este cliente", + "allowThisClient": "Permitir este cliente", + "clientAllowedSuccessfully": "Cliente permitido correctamente", + "clientDisallowedSuccessfully": "Cliente no permitido correctamente", + "changesNotSaved": "Los cambios no han podido ser guardados", + "allowingClient": "Permitiendo cliente...", + "disallowingClient": "No permitiendo cliente...", + "clientIpCopied": "Dirección IP del cliente copiada al portapapeles", + "clientNameCopied": "Nombre del cliente copiado al portapapeles", + "dnsServerAddressCopied": "Dirección del servidor DNS copiada al portapapeles", + "select": "Seleccionar", + "liveLogs": "Registros en directo", + "hereWillAppearRealtimeLogs": "Aquí aparecerán los registros en tiempo real.", + "applicationDetails": "Detalles de la aplicación", + "applicationDetailsDescription": "Repositorio de la app, tiendas donde está disponible, y más", + "myOtherApps": "Mis otras apps", + "myOtherAppsDescription": "Comprueba mis otras apps, hacer una donación, contactar al soporte, y más", + "topToBottom": "Desde arriba hacia abajo", + "bottomToTop": "Desde abajo hacia arriba", + "upstreamTimeout": "Tiempo de espera del upstream", + "upstreamTimeoutHelper": "Especifica el número de segundos que se debe esperar para recibir una respuesta del servidor upstream", + "fieldCannotBeEmpty": "El campo no puede estar vacío" } \ No newline at end of file diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb new file mode 100644 index 0000000..077fcbb --- /dev/null +++ b/lib/l10n/app_pl.arb @@ -0,0 +1,657 @@ +{ + "home": "Dom", + "settings": "Ustawienia", + "connect": "Połączyć", + "servers": "Serwery", + "createConnection": "Utwórz połączenie", + "name": "Nazwa", + "ipDomain": "Adres IP lub domena", + "path": "Ścieżka", + "port": "Port", + "username": "Nazwa użytkownika", + "password": "Hasło", + "defaultServer": "Serwer domyślny", + "general": "Ogólne", + "connection": "Połączenie", + "authentication": "Uwierzytelnianie", + "other": "Inne", + "invalidPort": "Nieprawidłowy port", + "invalidPath": "Nieprawidłowa ścieżka", + "invalidIpDomain": "Nieprawidłowy adres IP lub domena", + "ipDomainNotEmpty": "Adres IP lub domena nie mogą być puste", + "nameNotEmpty": "Nazwa nie może być pusta", + "invalidUsernamePassword": "Nieprawidłowa nazwa użytkownika lub hasło", + "tooManyAttempts": "Zbyt wiele prób, spróbuj ponownie później", + "cantReachServer": "Nie można połączyć się z serwerem. Sprawdź dane połączenia.", + "sslError": "Błąd SSL. Przejdź do pozycji Ustawienia > Ustawienia zaawansowane i włącz opcję Zastąp sprawdzanie poprawności SSL.", + "unknownError": "Nieznany błąd", + "connectionNotCreated": "Nie można utworzyć połączenia", + "connecting": "Łączenie...", + "connected": "Połączony", + "selectedDisconnected": "Zaznaczone, ale rozłączone", + "connectionDefaultSuccessfully": "Połączenie ustawione jako domyślne pomyślnie.", + "connectionDefaultFailed": "Nie można ustawić połączenia jako domyślne.", + "noSavedConnections": "Brak zapisanych połączeń", + "cannotConnect": "Nie można połączyć się z serwerem", + "connectionRemoved": "Połączenie zostało pomyślnie usunięte.", + "connectionCannotBeRemoved": "Nie można usunąć połączenia.", + "remove": "Usuń", + "removeWarning": "Czy na pewno chcesz usunąć połączenie z tym serwerem AdGuard Home?", + "cancel": "Anuluj", + "defaultConnection": "Połączenie domyślne", + "setDefault": "Ustaw jako domyślne", + "edit": "Edytuj", + "delete": "Usuń", + "save": "Zapisz", + "serverStatus": "Stan serwera", + "connectionNotUpdated": "Połączenie nie zostało zaktualizowane", + "ruleFilteringWidget": "Widget Reguł filtrowania", + "safeBrowsingWidget": "Widget Bezpieczne przeglądanie", + "parentalFilteringWidget": "Widget Filtrowania rodzicielskiego", + "safeSearchWidget": "Widget Bezpiecznego wyszukiwania", + "ruleFiltering": "Reguły filtrowania", + "safeBrowsing": "Bezpieczne przeglądanie", + "parentalFiltering": "Filtrowanie rodzicielskie", + "safeSearch": "Bezpieczne wyszukiwanie", + "serverStatusNotRefreshed": "Nie można odświeżyć stanu serwera", + "loadingStatus": "Stan ładowania...", + "errorLoadServerStatus": "Nie można załadować stanu serwera", + "topQueriedDomains": "Najczęściej wyszukiwane domeny", + "viewMore": "Zobacz więcej", + "topClients": "Najlepsi klienci", + "topBlockedDomains": "Najczęściej blokowane domeny", + "appSettings": "Ustawienia aplikacji", + "theme": "Motyw", + "light": "Jasny", + "dark": "Ciemny", + "systemDefined": "Zdefiniowany przez system", + "close": "Zamknij", + "connectedTo": "Połączony z:", + "selectedServer": "Wybrany serwer:", + "noServerSelected": "Nie wybrano serwera", + "manageServer": "Zarządzanie serwerem", + "allProtections": "Wszystkie zabezpieczenia", + "userNotEmpty": "Nazwa użytkownika nie może być pusta", + "passwordNotEmpty": "Hasło nie może być puste", + "examplePath": "Przykład: /adguard", + "helperPath": "Jeśli używasz zwrotnego serwera proxy", + "aboutApp": "O aplikacji", + "appVersion": "Wersja aplikacji", + "createdBy": "Stworzone przez", + "clients": "Klienci", + "allowed": "Dozwolone", + "blocked": "Zablokowane", + "noClientsList": "Na tej liście nie ma klientów", + "activeClients": "Aktywny", + "removeClient": "Usuń klienta", + "removeClientMessage": "Czy na pewno chcesz usunąć tego klienta z listy?", + "confirm": "Potwierdź", + "removingClient": "Usuwanie klienta...", + "clientNotRemoved": "Nie można usunąć klienta z listy", + "addClient": "Dodaj klienta", + "list": "Lista", + "ipAddress": "Adres IP", + "ipNotValid": "Adres IP jest nieprawidłowy", + "clientAddedSuccessfully": "Klient został pomyślnie dodany do listy.", + "addingClient": "Dodawanie klienta...", + "clientNotAdded": "Nie można dodać klienta do listy.", + "clientAnotherList": "Ten klient jest już na innej liście.", + "noSavedLogs": "Brak zapisanych danych w dzienniku logów", + "logs": "Logi", + "copyLogsClipboard": "Kopiowanie dzienników logów do schowka", + "logsCopiedClipboard": "Dzienniki logów skopiowane do schowka", + "advancedSettings": "Ustawienia zaawansowane", + "dontCheckCertificate": "Nie sprawdzaj certyfikatu SSL", + "dontCheckCertificateDescription": "Zastępuje sprawdzanie poprawności certyfikatu SSL serwera", + "advancedSetupDescription": "Opcje zaawansowane", + "settingsUpdatedSuccessfully": "Ustawienia zostały pomyślnie zaktualizowane.", + "cannotUpdateSettings": "Nie można zaktualizować ustawień.", + "restartAppTakeEffect": "Uruchom ponownie aplikację", + "loadingLogs": "Ładowanie logów dzienników...", + "logsNotLoaded": "Nie można załadować listy dzienników", + "processed": "Przetworzone\nBrak listy", + "processedRow": "Przetworzone (brak listy)", + "blockedBlacklist": "Zablokowane\nCzarna lista", + "blockedBlacklistRow": "Zablokowane (czarna lista)", + "blockedSafeBrowsing": "Zablokowane\nBezpieczne przeglądanie", + "blockedSafeBrowsingRow": "Zablokowane (bezpieczne przeglądanie)", + "blockedParental": "Zablokowane\nFiltrowanie rodzicielskie", + "blockedParentalRow": "Zablokowane (filtrowanie rodzicielskie)", + "blockedInvalid": "Zablokowany\nNieprawidłowy", + "blockedInvalidRow": "Zablokowane (nieprawidłowe)", + "blockedSafeSearch": "Zablokowane\nBezpieczne wyszukiwanie", + "blockedSafeSearchRow": "Zablokowane (bezpieczne wyszukiwanie)", + "blockedService": "Zablokowana\nZablokowana usługa", + "blockedServiceRow": "Zablokowana (zablokowana usługa)", + "processedWhitelist": "Przetworzone\nBiała lista", + "processedWhitelistRow": "Przetworzone (biała lista)", + "processedError": "Przetworzone\nBłąd", + "processedErrorRow": "Przetworzone (błąd)", + "rewrite": "Nadpisać", + "status": "Status", + "result": "Wynik", + "time": "Czas", + "blocklist": "Lista zablokowanych", + "request": "Żądanie", + "domain": "Domena", + "type": "Type", + "clas": "Klasa", + "response": "Odpowiedź", + "dnsServer": "DNS server", + "elapsedTime": "Czas, który upłynął", + "responseCode": "Kod odpowiedzi", + "client": "Klient", + "deviceIp": "Adres IP urządzenia", + "deviceName": "Nazwa urządzenia", + "logDetails": "Szczegóły dziennika logów", + "blockingRule": "Reguła blokowania", + "blockDomain": "Blokowanie domeny", + "couldntGetFilteringStatus": "Nie można uzyskać stanu filtrowanias", + "unblockDomain": "Odblokuj domenę", + "userFilteringRulesNotUpdated": "Nie można zaktualizować reguł filtrowania użytkowników.", + "userFilteringRulesUpdated": "Reguły filtrowania użytkowników zostały pomyślnie zaktualizowane", + "savingUserFilters": "Zapisywanie filtrów użytkownika...", + "filters": "Filtry", + "logsOlderThan": "Dzienniki logów starsze niż", + "responseStatus": "Stan odpowiedzi", + "selectTime": "Wybierz czas", + "notSelected": "Nie wybrano", + "resetFilters": "Resetowanie filtrów", + "noLogsDisplay": "Brak dzienników logów do wyświetlenia", + "noLogsThatOld": "Możliwe, że dla wybranego czasu nie zapisano żadnych dzienników logów. Spróbuj wybrać nowszą godzinę.", + "apply": "Zastosować", + "selectAll": "Zaznacz wszystko", + "unselectAll": "Odznacz wszystko", + "all": "Wszystko", + "filtered": "Przefiltrowano", + "checkAppLogs": "Sprawdź dzienniki logów aplikacji", + "refresh": "Odśwież", + "search": "Szukaj", + "dnsQueries": "Zapytania DNS", + "average": "Średnia", + "blockedFilters": "Zablokowane przez filtry", + "malwarePhishingBlocked": "Zablokowane złośliwe oprogramowanie/phishing", + "blockedAdultWebsites": "Zablokowane witryny dla dorosłych", + "generalSettings": "Ustawienia główne", + "generalSettingsDescription": "Różne ustawienia", + "hideZeroValues": "Ukryj wartości zerowe", + "hideZeroValuesDescription": "Na ekranie głównym ukryj bloki o zerowej wartości", + "webAdminPanel": "Panel administracyjny WWW", + "visitGooglePlay": "Odwiedź stronę Google Play", + "gitHub": "Kod aplikacji dostępny na GitHub", + "blockClient": "Zablokuj klienta", + "selectTags": "Wybierz tagi", + "noTagsSelected": "Brak zaznaczonych tagów", + "tags": "Tagi", + "identifiers": "Identyfikatory", + "identifier": "Identyfikator", + "identifierHelper": "Adres IP , CIDR, Adres MAC, or ClientID", + "noIdentifiers": "Nie dodano identyfikatorów", + "useGlobalSettings": "Użyj ustawień globalnych", + "enableFiltering": "Włącz filtrowanie", + "enableSafeBrowsing": "Włącz bezpieczne przeglądanie", + "enableParentalControl": "Włącz kontrolę rodzicielską", + "enableSafeSearch": "Włącz bezpieczne wyszukiwanie", + "blockedServices": "Zablokowane usługi", + "selectBlockedServices": "Wybierz usługi do zablokowaniak", + "noBlockedServicesSelected": "Brak zablokowanych usług", + "services": "Usługi", + "servicesBlocked": "Usługi zablokowane", + "tagsSelected": "wybrane tagi", + "upstreamServers": "Serwery nadrzędne", + "serverAddress": " Adres servera", + "noUpstreamServers": "Brak serwerów nadrzędnych.", + "willBeUsedGeneralServers": "Wykorzystane zostaną ogólne serwery nadrzędne.", + "added": "Dodane", + "clientUpdatedSuccessfully": "Klient został pomyślnie zaktualizowany", + "clientNotUpdated": "Nie można zaktualizować klienta", + "clientDeletedSuccessfully": "Klient został pomyślnie usunięty", + "clientNotDeleted": "Nie można usunąć klienta", + "options": "Opcje", + "loadingFilters": "Ładowanie filtrów...", + "filtersNotLoaded": "Nie można załadować filtrów.", + "whitelists": "Biała lista", + "blacklists": "Czarna lista", + "rules": "Zasady", + "customRules": "Reguły niestandardowe", + "enabledRules": "Włączone reguły", + "enabled": "Włączone", + "disabled": "Wyłączone", + "rule": "Reguła", + "addCustomRule": "Dodaj niestandardową regułę", + "removeCustomRule": "Usuń regułę niestandardową", + "removeCustomRuleMessage": "Czy na pewno chcesz usunąć tę regułę niestandardową?", + "updatingRules": "Aktualizowanie reguł niestandardowych...", + "ruleRemovedSuccessfully": "Reguła została pomyślnie usunięta", + "ruleNotRemoved": "Nie można usunąć reguły", + "ruleAddedSuccessfully": "Reguła dodana pomyślnie", + "ruleNotAdded": "Nie można dodać reguły", + "noCustomFilters": "Brak filtrów niestandardowych", + "noBlockedClients": "Brak zablokowanych klientów", + "noBlackLists": "Brak czarnych list", + "noWhiteLists": "Brak białych list", + "addWhitelist": "Dodaj białą listę", + "addBlacklist": "Dodaj czarną listę", + "urlNotValid": "Adres URL jest nieprawidłowy", + "urlAbsolutePath": "Adres URL lub ścieżka bezwzględna", + "addingList": "Dodawanie listy...", + "listAdded": "Lista została dodana pomyślnie. Dodane elementy:", + "listAlreadyAdded": "Lista została już dodana", + "listUrlInvalid": "Adres URL listy jest nieprawidłowy", + "listNotAdded": "Nie można dodać listy", + "listDetails": "Szczegóły listy", + "listType": "Typ listy", + "whitelist": "Biała lista", + "blacklist": "Czarna list", + "latestUpdate": "Najnowsza aktualizacja", + "disable": "Wyłączyć", + "enable": "Włączać", + "currentStatus": "Aktualny stan", + "listDataUpdated": "Dane listy zostały pomyślnie zaktualizowane", + "listDataNotUpdated": "Nie można zaktualizować danych listy", + "updatingListData": "Aktualizowanie danych listy...", + "editWhitelist": "Edytuj białą listę", + "editBlacklist": "Edytuj czarną listę", + "deletingList": "Usuwanie listy...", + "listDeleted": "Lista usunięta pomyślnie", + "listNotDeleted": "Nie można usunąć listy", + "deleteList": "Usuń listę", + "deleteListMessage": "Czy na pewno chcesz usunąć tę listę? Tej akcji nie można cofnąć.", + "serverSettings": "Ustawienia serwera", + "serverInformation": "Informacje o serwerze", + "serverInformationDescription": "Informacje i status serwera", + "loadingServerInfo": "Ładowanie informacji o serwerze...", + "serverInfoNotLoaded": "Nie można załadować informacji o serwerze.", + "dnsAddresses": "Adresy DNS", + "seeDnsAddresses": "Zobacz adresy DNS", + "dnsPort": "Port DNS", + "httpPort": "Port HTTP", + "protectionEnabled": "Ochrona włączona", + "dhcpAvailable": "Dostępne DHCP", + "serverRunning": "Serwer działa", + "serverVersion": "Wersia Serwera", + "serverLanguage": "Język serwera", + "yes": "Tak", + "no": "Nie", + "allowedClients": "Dozwoleni klienci", + "disallowedClients": "Niedozwoloni klienci", + "disallowedDomains": "Niedozwolone domeny", + "accessSettings": "Ustawienia dostępu", + "accessSettingsDescription": "Konfigurowanie reguł dostępu dla serwera", + "loadingClients": "Ładowanie klientów...", + "clientsNotLoaded": "Nie można załadować klientów.", + "noAllowedClients": "Brak dozwolonych klientów.", + "allowedClientsDescription": "Jeśli ta lista zawiera wpisy, AdGuard Home będzie akceptować żądania tylko od tych klientów.", + "blockedClientsDescription": "Jeśli ta lista zawiera wpisy, AdGuard Home odrzuci żądania od tych klientów. To pole jest ignorowane, jeśli w obszarze Dozwolone klienty znajdują się wpisy", + "disallowedDomainsDescription": "AdGuard Home usuwa zapytania DNS pasujące do tych domen, a zapytania te nawet nie pojawiają się w dzienniku zapytań", + "addClientFieldDescription": "CIDRs, adres IP or ClientID", + "clientIdentifier": "Identyfikator klienta", + "allowClient": "Zezwól klientowi", + "disallowClient": "Nie zezwalaj klientowi", + "noDisallowedDomains": "Brak niedozwolonych domen", + "domainNotAdded": "Nie można dodać domeny", + "statusSelected": "Wybrany status", + "updateLists": "Aktualizuj listy", + "checkHostFiltered": "Sprawdź hosta", + "updatingLists": "Aktualizowanie list...", + "listsUpdated": "Zaktualizowano listy", + "listsNotUpdated": "Nie można zaktualizować list", + "listsNotLoaded": "Nie można załadować list", + "domainNotValid": "Nieprawidłowa domena", + "check": "Sprawdź", + "checkingHost": "Sprawdzanie hosta...", + "errorCheckingHost": "Nie można sprawdzić hosta", + "block": "Zablokować", + "unblock": "Odblokować", + "custom": "Niestandardowy", + "addImportant": "Dodawaj $important", + "howCreateRules": "Jak tworzyć reguły niestandardowe", + "examples": "Przykłady", + "example1": "Zablokuj dostęp do example.org i wszystkich jej subdomen.", + "example2": "Odblokowuje dostęp do example.org i wszystkich jego subdomen.", + "example3": "Dodaje komentarz.", + "example4": "Blokowanie dostępu do domen zgodnych z określonym wyrażeniem regularnym.", + "moreInformation": "Więcej informacji", + "addingRule": "Dodawanie reguły...", + "deletingRule": "Usuwanie reguły...", + "enablingList": "Włączanie listy...", + "disablingList": "Wyłączanie listy...", + "disableFiltering": "Wyłącz filtrowanie", + "enablingFiltering": "Włączanie filtrowania...", + "disablingFiltering": "Wyłączanie filtrowania...", + "filteringStatusUpdated": "Stan filtrowania został pomyślnie zaktualizowany", + "filteringStatusNotUpdated": "Nie można zaktualizować stanu filtrowania", + "updateFrequency": "Częstotliwość aktualizacji", + "never": "Nigdy", + "hour1": "Co 1 godzinę", + "hours12": "Co 12 godzin", + "hours24": "Co 24 godziny", + "days3": "Co 3 dni", + "days7": "Co 7 dni", + "changingUpdateFrequency": "Zmieniam...", + "updateFrequencyChanged": "Częstotliwość aktualizacji została pomyślnie zmieniona", + "updateFrequencyNotChanged": "Nie można zmienić częstotliwości aktualizacji", + "updating": "Aktualizowanie wartości...", + "blockedServicesUpdated": "Zablokowane usługi zostały pomyślnie zaktualizowane", + "blockedServicesNotUpdated": "Nie można zaktualizować zablokowanych usług", + "insertDomain": "Wstaw domenę, aby sprawdzić jej status.", + "dhcpSettings": "Ustawienia DHCP", + "dhcpSettingsDescription": "Konfigurowanie serwera DHCP", + "dhcpSettingsNotLoaded": "Nie można załadować ustawień DHCP", + "loadingDhcp": "Ładowanie ustawień DHCP...", + "enableDhcpServer": "Włącz serwer DHCP", + "selectInterface": "Wybierz interfejs", + "hardwareAddress": "Adres sprzętowy", + "gatewayIp": "Adres IP bramy", + "ipv4addresses": "Adres IPv4", + "ipv6addresses": "Adres IPv6", + "neededSelectInterface": "Aby skonfigurować serwer DHCP, należy wybrać interfejs.", + "ipv4settings": "Ustawienia IPv4", + "startOfRange": "Początek zakresu", + "endOfRange": "Koniec zakresu", + "ipv6settings": "Ustawienia IPv6", + "subnetMask": "Maska podsieci", + "subnetMaskNotValid": "Maska podsieci jest nieprawidłowa", + "gateway": "Brama", + "gatewayNotValid": "Brama jest nieprawidłowa", + "leaseTime": "Czas dzierżawy", + "seconds": "{time} sekund", + "leaseTimeNotValid": "Czas dzierżawy jest nieaktualny", + "restoreConfiguration": "Zresetuj konfigurację", + "restoreConfigurationMessage": "Czy na pewno chcesz kontynuować? Spowoduje to zresetowanie całej konfiguracji. Tej czynności nie można cofnąć.", + "changeInterface": "Zmień interfejs", + "savingSettings": "Zapisywanie ustawień...", + "settingsSaved": "Ustawienia zapisane pomyślnie", + "settingsNotSaved": "Nie można zapisać ustawień", + "restoringConfig": "Przywracanie konfiguracji...", + "configRestored": "Konfiguracja została zresetowana pomyślnie", + "configNotRestored": "Nie można zresetować konfiguracji", + "dhcpStatic": "Dzierżawy statyczne DHCP", + "noDhcpStaticLeases": "Nie znaleziono dzierżaw statycznych DHCP", + "deleting": "Usuwanie...", + "staticLeaseDeleted": "Dzierżawa statyczna DHCP została pomyślnie usunięta", + "staticLeaseNotDeleted": "Nie można usunąć dzierżawy statycznej DHCP", + "deleteStaticLease": "Usuwanie dzierżawy statycznej...", + "deleteStaticLeaseDescription": "Dzierżawa statyczna DHCP zostanie usunięta. Tej akcji nie można cofnąć.", + "addStaticLease": "Dodawanie dzierżawy statycznej", + "macAddress": "Adres MAC", + "macAddressNotValid": "Nieprawidłowy adres MAC", + "hostName": "Nazwa hosta", + "hostNameError": "Nazwa hosta nie może być pusta", + "creating": "Tworzenie...", + "staticLeaseCreated": "Dzierżawa statyczna DHCP utworzona pomyślnie", + "staticLeaseNotCreated": "Nie można utworzyć dzierżawy statycznej DHCP", + "staticLeaseExists": "Dzierżawa statyczna DHCP już istnieje", + "serverNotConfigured": "Serwer nie jest skonfigurowany", + "restoreLeases": "Resetowanie dzierżaw", + "restoreLeasesMessage": "Czy na pewno chcesz kontynuować? Spowoduje to zresetowanie wszystkich istniejących dzierżaw. Tej czynności nie można cofnąć.", + "restoringLeases": "Resetowanie dzierżaw...", + "leasesRestored": "Dzierżawy zostały pomyślnie zresetowane", + "leasesNotRestored": "Nie można zresetować dzierżaw", + "dhcpLeases": "Dzierżawy DHCP", + "noLeases": "Brak dostępnych dzierżaw DHCP", + "dnsRewrites": "Przepisywanie DNS", + "dnsRewritesDescription": "Konfigurowanie niestandardowych reguł DNS", + "loadingRewriteRules": "Wczytywanie reguł przepisywania...", + "rewriteRulesNotLoaded": "Nie można załadować reguł przepisywania DNS.", + "noRewriteRules": "Brak reguł przepisywania DNS.", + "answer": "Odpowiedź", + "deleteDnsRewrite": "Usuń przepisywanie DNS", + "deleteDnsRewriteMessage": "Czy na pewno chcesz usunąć to przepisanie DNS? Tej akcji nie można cofnąć.", + "dnsRewriteRuleDeleted": "Reguła przepisywania DNS została pomyślnie usunięta", + "dnsRewriteRuleNotDeleted": "Nie można usunąć reguły przepisywania DNS", + "addDnsRewrite": "Dodaj przepisywanie DNS", + "addingRewrite": "Dodawanie przepisywania...", + "dnsRewriteRuleAdded": "Pomyślnie dodano regułę przepisywania DNS", + "dnsRewriteRuleNotAdded": "Nie można dodać reguły przepisywania DNS", + "logsSettings": "Ustawienia dzienników logów", + "enableLog": "Włącz dziennik logów", + "clearLogs": "Czyszczenie dzienników logów", + "anonymizeClientIp": "Anonimizuj adres IP klienta", + "hours6": "co 6 godzin", + "days30": "co 30 dni", + "days90": "co 90 dni", + "retentionTime": "Czas retencji", + "selectOneItem": "Wybierz jeden element", + "logSettingsNotLoaded": "Nie można załadować ustawień dziennika logów.", + "updatingSettings": "Aktualizowanie ustawień...", + "logsConfigUpdated": "Ustawienia dzienników logów zostały pomyślnie zaktualizowane", + "logsConfigNotUpdated": "Nie można zaktualizować ustawień dzienników logów", + "deletingLogs": "Czyszczenie dzienników logów...", + "logsCleared": "Dzienniki logów wyczyszczone pomyślnie", + "logsNotCleared": "Nie można wyczyścić dzienników logów", + "runningHomeAssistant": "Działa na Home Assistant", + "serverError": "Błąd serwera", + "noItems": "Brak elementów do pokazania w tym miejscu", + "dnsSettings": "Ustawienia DNS", + "dnsSettingsDescription": "Konfigurowanie połączenia z serwerami DNS", + "upstreamDns": "Nadrzędne serwery DNS", + "bootstrapDns": "Serwery DNS Bootstrap", + "noUpstreamDns": "Nie dodano nadrzędnych serwerów DNS.", + "dnsMode": "Tryb DNS", + "noDnsMode": "Nie wybrano trybu DNS", + "loadBalancing": "Równoważenie obciążenia", + "parallelRequests": "Równoległe żądania", + "fastestIpAddress": "Najszybszy adres IP", + "loadBalancingDescription": "Wysyłaj zapytania do jednego serwera nadrzędnego naraz. AdGuard Home wykorzystuje ważony algorytm losowy, aby wybrać serwer, dzięki czemu najszybszy serwer jest używany częściej.", + "parallelRequestsDescription": "Użyj zapytań równoległych, aby przyspieszyć rozwiązywanie problemów, wysyłając jednocześnie zapytania do wszystkich serwerów nadrzędnych.", + "fastestIpAddressDescription": "Odpytuj wszystkie serwery DNS i zwracaj najszybszy adres IP spośród wszystkich odpowiedzi. Spowalnia to zapytania DNS, ponieważ AdGuard Home musi czekać na odpowiedzi ze wszystkich serwerów DNS, ale poprawia ogólną łączność.", + "noBootstrapDns": "Nie dodano żadnych serwerów Bootstrap DNS.", + "bootstrapDnsServersInfo": "Serwery DNS Bootstrap służą do rozpoznawania adresów IP programów rozpoznawania nazw DoH/DoT określonych jako nadrzędne.", + "privateReverseDnsServers": "Prywatne zwrotne serwery DNS", + "privateReverseDnsServersDescription": "Serwery DNS, których AdGuard Home używa do lokalnych zapytań PST. Serwery te są używane na przykład do rozwiązywania żądań PST dotyczących adresów w prywatnych zakresach adresów IP \"192.168.12.34\", przy użyciu odwrotnego DNS. Jeśli nie jest ustawiony, AdGuard Home używa adresów domyślnych programów rozpoznawania nazw DNS systemu operacyjnego, z wyjątkiem adresów samego AdGuard Home.", + "reverseDnsDefault": "Domyślnie AdGuard Home używa następujących programów do rozpoznawania odwrotnych nazw DNS", + "addItem": "Dodaj element", + "noServerAddressesAdded": "Nie dodano adresów serwerów.", + "usePrivateReverseDnsResolvers": "Użyj prywatnych programów do rozpoznawania nazw zwrotnych DNS", + "usePrivateReverseDnsResolversDescription": "Wykonaj odwrotne wyszukiwanie DNS dla adresów obsługiwanych lokalnie przy użyciu tych serwerów nadrzędnych. Jeśli jest wyłączona, AdGuard Home odpowiada NXDOMAIN na wszystkie takie żądania PST, z wyjątkiem klientów znanych z DHCP, /etc/hosts itd.", + "enableReverseResolving": "Włącz odwrotne rozpoznawanie adresów IP klientów", + "enableReverseResolvingDescription": "Odwrotne rozpoznawanie adresów IP klientów w ich nazwach hostów przez wysyłanie zapytań PTR do odpowiednich programów rozpoznawania nazw (prywatne serwery DNS dla klientów lokalnych, serwery nadrzędne dla klientów z publicznymi adresami IP).", + "dnsServerSettings": "Ustawienia serwera DNS AdGuard Home", + "limitRequestsSecond": "Limit szybkości na sekundę", + "valueNotNumber": "Wartość nie jest liczbą", + "enableEdns": "Włączanie podsieci klienta EDNS", + "enableEdnsDescription": "Dodaj opcję podsieci klienta EDNS (ECS) do żądań nadrzędnych i rejestruj wartości wysyłane przez klientów w dzienniku zapytań.", + "enableDnssec": "Włącz usługę DNSSEC", + "enableDnssecDescription": "Ustaw flagę DNSSEC w wychodzących zapytaniach DNS i sprawdź wynik (wymagany jest moduł rozpoznawania nazw obsługujący DNSSEC).", + "disableResolvingIpv6": "Wyłączanie rozpoznawania adresów IPv6", + "disableResolvingIpv6Description": "Usuń wszystkie zapytania DNS dotyczące adresów IPv6 (typ AAAA).", + "blockingMode": "Tryb blokowania", + "defaultMode": "Domyślny", + "defaultDescription": "Odpowiedz zerowym adresem IP (0.0.0.0 dla A; :: dla AAAA), gdy zostanie zablokowany przez regułę w stylu Adblock; odpowiedz adresem IP określonym w regule, jeśli zostanie zablokowany przez regułę w stylu /etc/hosts", + "refusedDescription": "Odpowiedz kodem REFUSED", + "nxdomainDescription": "Odpowiadanie za pomocą kodu NXDOMAIN", + "nullIp": "Null IP", + "nullIpDescription": "Odpowiedz, podając zerowy adres IP (0.0.0.0 dla A; :: dla AAAA)", + "customIp": "Niestandardowy adres IP", + "customIpDescription": "Odpowiadanie przy użyciu ręcznie ustawionego adresu IP", + "dnsCacheConfig": "Konfiguracja pamięci podręcznej DNS", + "cacheSize": "Rozmiar pamięci podręcznej", + "inBytes": "W bajtach", + "overrideMinimumTtl": "Zastąp minimalny czas TTL", + "overrideMinimumTtlDescription": "Wydłużanie wartości krótkiego czasu wygaśnięcia (sekund) odbieranych z serwera nadrzędnego podczas buforowania odpowiedzi DNS.", + "overrideMaximumTtl": "Zastąp maksymalne ustawienie TTL", + "overrideMaximumTtlDescription": "Ustaw maksymalną wartość czasu wygaśnięcia (sekundy) dla wpisów w pamięci podręcznej DNS.", + "optimisticCaching": "Optymistyczne buforowanie", + "optimisticCachingDescription": "Spraw, aby AdGuard Home odpowiadał z pamięci podręcznej, nawet gdy wpisy wygasły, a także spróbuj je odświeżyć.", + "loadingDnsConfig": "Ładowanie konfiguracji DNS...", + "dnsConfigNotLoaded": "Nie można załadować konfiguracji DNS.", + "blockingIpv4": "Blokowanie protokołu IPv4", + "blockingIpv4Description": "Adres IP do zwrócenia w przypadku zablokowanego żądania A", + "blockingIpv6": "Blokowanie protokołu IPv6", + "blockingIpv6Description": "Adres IP, który ma zostać zwrócony w przypadku zablokowanego żądania AAAA", + "invalidIp": "Nieprawidłowy adres IP", + "dnsConfigSaved": "Konfiguracja serwera DNS zapisana pomyślnie", + "dnsConfigNotSaved": "Nie można zapisać konfiguracji serwera DNS", + "savingConfig": "Zapisywanie konfiguracji...", + "someValueNotValid": "Niektóre wartości są nieprawidłowe", + "upstreamDnsDescription": "Konfigurowanie serwerów nadrzędnych i trybu DNS", + "bootstrapDnsDescription": "Konfigurowanie serwerów DNS ładowania początkowego", + "privateReverseDnsDescription": "Konfigurowanie niestandardowych programów rozpoznawania nazw DNS i włączanie prywatnego rozpoznawania zwrotnej domeny DNS", + "dnsServerSettingsDescription": "Konfigurowanie limitu szybkości, trybu blokowania i innych funkcji", + "dnsCacheConfigDescription": "Konfigurowanie sposobu zarządzania pamięcią podręczną DNS przez serwer", + "comment": "Komentarz", + "address": "Adres", + "commentsDescription": "Komentarze są zawsze poprzedzone znakiem #. Nie musisz go dodawać, zostanie dodany automatycznie.", + "encryptionSettings": "Ustawienia szyfrowania", + "encryptionSettingsDescription": "Obsługa szyfrowania (HTTPS/QUIC/TLS)", + "loadingEncryptionSettings": "Ładowanie ustawień szyfrowania...", + "encryptionSettingsNotLoaded": "Nie można załadować ustawień szyfrowania.", + "enableEncryption": "Włącz szyfrowanie", + "enableEncryptionTypes": "HTTPS, DNS przez HTTPS i DNS przez TLS", + "enableEncryptionDescription": "Jeśli szyfrowanie jest włączone, interfejs administratora AdGuard Home będzie działał przez HTTPS, a serwer DNS będzie nasłuchiwał żądań przez DNS przez HTTPS i DNS przez TLS.", + "serverConfiguration": "Konfiguracja serwera", + "domainName": "Nazwa domeny", + "domainNameDescription": "Jeśli jest ustawiony, AdGuard Home wykrywa identyfikatory klientów, odpowiada na zapytania DDR i wykonuje dodatkowe walidacje połączeń. Jeśli te funkcje nie są ustawione, są wyłączone. Musi być zgodna z jedną z nazw DNS w certyfikacie.", + "redirectHttps": "Automatyczne przekierowywanie do protokołu HTTPS", + "httpsPort": "Port HTTPS", + "tlsPort": "DNS przez port TLS", + "dnsOverQuicPort": "DNS przez port QUIC", + "certificates": "Certifikaty", + "certificatesDescription": "Aby korzystać z szyfrowania, musisz podać prawidłowy łańcuch certyfikatów SSL dla swojej domeny. Możesz uzyskać bezpłatny certyfikat na letsencrypt.org lub kupić go w jednym z zaufanych urzędów certyfikacji.", + "certificateFilePath": "Ustaw ścieżkę pliku certyfikatów", + "pasteCertificateContent": "Wklejanie zawartości certyfikatów", + "certificatePath": "Ścieżka certyfikatu", + "certificateContent": "Zawartość certyfikatu", + "privateKey": "Klucz prywatny", + "privateKeyFile": "Ustaw plik klucza prywatnego", + "pastePrivateKey": "Wklejanie zawartości klucza prywatnego", + "usePreviousKey": "Użyj wcześniej zapisanego klucza", + "privateKeyPath": "Ścieżka klucza prywatnego", + "invalidCertificate": "Nieprawidłowy certyfikat", + "invalidPrivateKey": "Nieprawidłowy klucz prywatny", + "validatingData": "Sprawdzanie poprawności danych", + "dataValid": "Dane prawidłowe", + "dataNotValid": "Dane nieprawidłowe", + "encryptionConfigSaved": "Konfiguracja szyfrowania zapisana pomyślnie", + "encryptionConfigNotSaved": "Nie można zapisać konfiguracji szyfrowania", + "configError": "Błąd konfiguracji", + "enterOnlyCertificate": "Wprowadź tylko certyfikat. Nie wpisuj linii ---BEGIN--- i ---END---.", + "enterOnlyPrivateKey": "Wprowadź tylko klucz. Nie wpisuj linii ---BEGIN--- i ---END---.", + "noItemsSearch": "Brak elementów dla tego wyszukiwania.", + "clearSearch": "Wyczyść wyszukiwanie", + "exitSearch": "Zakończ wyszukiwanie", + "searchClients": "Wyszukaj klientów", + "noClientsSearch": "Brak klientów dla tego wyszukiwania.", + "customization": "Personalizacja", + "customizationDescription": "Dostosuj tę aplikację", + "color": "Kolor", + "useDynamicTheme": "Użyj motywu dynamicznego", + "red": "Czerwony", + "green": "Zielony", + "blue": "Niebieski", + "yellow": "Żółty", + "orange": "Pomarańczowy", + "brown": "Brązowy", + "cyan": "Błękitny", + "purple": "Fioletowy", + "pink": "Różowy", + "deepOrange": "Głęboki pomarańczowy", + "indigo": "Indygo", + "useThemeColorStatus": "Użyj koloru motywu dla statusu", + "useThemeColorStatusDescription": "Zastępuje zielone i czerwone kolory statusu kolorami motywu i szarością", + "invalidCertificateChain": "Nieprawidłowy łańcuch certyfikatów", + "validCertificateChain": "Prawidłowy łańcuch certyfikatów", + "subject": "Temat", + "issuer": "Wydawca", + "expires": "Wygasa", + "validPrivateKey": "Prawidłowy klucz prywatny", + "expirationDate": "Data ważności", + "keysNotMatch": "Nieprawidłowy certyfikat lub klucz: TLS: klucz prywatny nie jest zgodny z kluczem publicznym", + "timeLogs": "Czas w dziennikach logów", + "timeLogsDescription": "Pokaż czas przetwarzania na liście dzienników logów", + "hostNames": "Nazwy hostów", + "keyType": "Typ klucza", + "updateAvailable": "Dostępna aktualizacja", + "installedVersion": "Zainstalowana wersja", + "newVersion": "Nowa wersja", + "source": "Źródło", + "downloadUpdate": "Pobierz aktualizację", + "download": "Pobierz", + "doNotRememberAgainUpdate": "Nie pamiętam ponownie dla tej wersji", + "downloadingUpdate": "Pobieranie aktualizacji", + "completed": "ukończone", + "permissionNotGranted": "Pozwolenie nie zostało udzielone", + "inputSearchTerm": "Wprowadź wyszukiwany termin.", + "answers": "Odpowiedzi", + "copyClipboard": "Kopiuj do schowka", + "domainCopiedClipboard": "Domena skopiowana do schowka", + "clearDnsCache": "Wyczyść pamięć podręczną DNS", + "clearDnsCacheMessage": "Czy na pewno chcesz wyczyścić pamięć podręczną DNS?", + "dnsCacheCleared": "Pamięć podręczna DNS została pomyślnie wyczyszczona", + "clearingDnsCache": "Czyszczenie pamięci podręcznej...", + "dnsCacheNotCleared": "Nie można wyczyścić pamięci podręcznej DNS", + "clientsSelected": "Wybrani klienci", + "invalidDomain": "Nieprawidłowa domena", + "loadingBlockedServicesList": "Ładowanie listy zablokowanych usług...", + "blockedServicesListNotLoaded": "Nie można załadować listy zablokowanych usług", + "error": "Błąd", + "updates": "Aktualizacje", + "updatesDescription": "Zaktualizuj serwer AdGuard Home", + "updateNow": "Aktualizuj teraz", + "currentVersion": "Aktualna wersja", + "requestStartUpdateFailed": "Żądanie rozpoczęcia aktualizacji nie powiodło się", + "requestStartUpdateSuccessful": "Żądanie rozpoczęcia aktualizacji powiodło się", + "serverUpdated": "Serwer jest aktualizowany", + "unknownStatus": "Status nieznany", + "checkingUpdates": "Sprawdzanie aktualizacji...", + "checkUpdates": "Sprawdź aktualizacje", + "requestingUpdate": "Żądanie aktualizacji...", + "autoupdateUnavailable": "Automatyczna aktualizacja jest niedostępna", + "autoupdateUnavailableDescription": "Usługa automatycznej aktualizacji nie jest dostępna dla tego serwera. Może to być spowodowane tym, że serwer działa w kontenerze Docker. Musisz zaktualizować serwer ręcznie.", + "minute": "{time} minut", + "minutes": "{time} minut", + "hour": "{time} godzina", + "hours": "{time} godzin", + "remainingTime": "Pozostały czas", + "safeSearchSettings": "Ustawienia bezpiecznego wyszukiwania", + "loadingSafeSearchSettings": "Ładowanie ustawień bezpiecznego wyszukiwania...", + "safeSearchSettingsNotLoaded": "Błąd podczas ładowania ustawień bezpiecznego wyszukiwania.", + "loadingLogsSettings": "Ładowanie ustawień dzienników logów...", + "selectOptionLeftColumn": "Wybierz opcję lewej kolumny", + "selectClientLeftColumn": "Wybierz klienta z lewej kolumny", + "disableList": "Wyłącz listę", + "enableList": "Włącz listę", + "screens": "Ekrany", + "copiedClipboard": "Skopiowane do schowka", + "seeDetails": "Zobacz szczegóły", + "listNotAvailable": "Lista niedostępna", + "copyListUrl": "Skopiuj adres URL listy", + "listUrlCopied": "Adres URL listy skopiowany do schowka", + "unsupportedVersion": "Nieobsługiwana wersja", + "unsupprtedVersionMessage": "Nie gwarantuje się wsparcia dla wersji serwera {version}. Ta aplikacja może mieć pewne problemy z działaniem na tej wersji serwera.\n\nAdGuard Home Manager został zaprojektowany do współpracy ze stabilnymi wersjami serwera AdGuard Home. Może działać z wersjami alfa i beta, ale kompatybilność nie jest gwarantowana, a aplikacja może mieć pewne problemy z działaniem w tych wersjach.", + "iUnderstand": "Rozumiem", + "appUpdates": "Aktualizacje aplikacji", + "usingLatestVersion": "Używasz najnowszej wersji", + "ipLogs": "IP w dziennikach logów", + "ipLogsDescription": "Pokaż zawsze adres IP w dziennikach logów zamiast nazwy klienta", + "application": "Aplikacja", + "combinedChart": "Wykres łączony", + "combinedChartDescription": "Połącz wszystkie wykresy w jeden", + "statistics": "Statystyka", + "errorLoadFilters": "Błąd podczas ładowania filtrów.", + "clientRemovedSuccessfully": "Klient został pomyślnie usunięty.", + "editRewriteRule": "Edytowanie reguły przepisywania", + "dnsRewriteRuleUpdated": "Reguła przepisywania DNS została pomyślnie zaktualizowana", + "dnsRewriteRuleNotUpdated": "Nie można zaktualizować reguły przepisywania DNS", + "updatingRule": "Aktualizuję regułę...", + "serverUpdateNeeded": "Wymagana aktualizacja serwera", + "updateYourServer": "Aby móc korzystać z tej funkcji, zaktualizuj serwer AdGuard Home do wersji {version} lub nowszej.", + "january": "Styczeń", + "february": "Luty", + "march": "Mrzec", + "april": "Kwiecień", + "may": "Maj", + "june": "Czerwiec", + "july": "Lipiec", + "august": "Sierpień", + "september": "Wrzesień", + "october": "Październik", + "november": "Listopad", + "december": "Grudzień", + "malwarePhishing": "Złośliwe oprogramowanie / wyłudzanie informacji", + "queries": "Zapytania", + "adultSites": "Strony dla dorosłych", + "quickFilters": "Szybkie filtry", + "searchDomainInternet": "Wyszukaj domenę w Internecie" +} diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb new file mode 100644 index 0000000..c0a6063 --- /dev/null +++ b/lib/l10n/app_ru.arb @@ -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-порт." +} diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb new file mode 100644 index 0000000..d0f26c5 --- /dev/null +++ b/lib/l10n/app_tr.arb @@ -0,0 +1,810 @@ +{ + "home": "Anasayfa", + "settings": "Ayarlar", + "connect": "Bağlan", + "servers": "Sunucular", + "createConnection": "Bağlantı oluştur", + "editConnection": "Bağlantıyı düzenle", + "name": "Ad", + "ipDomain": "IP adresi veya alan adı", + "path": "Dosya Yolu", + "port": "Bağlantı noktası", + "username": "Kullanıcı adı", + "password": "Şifre", + "defaultServer": "Varsayılan sunucu olarak ayarla", + "general": "Genel", + "connection": "Bağlantı", + "authentication": "Kimlik doğrulama", + "other": "Diğer", + "invalidPort": "Geçersiz bağlantı noktası", + "invalidPath": "Geçersiz dosya yolu", + "invalidIpDomain": "Geçersiz IP veya alan adı", + "ipDomainNotEmpty": "IP veya alan adı boş olamaz", + "nameNotEmpty": "Ad boş bırakılamaz", + "invalidUsernamePassword": "Geçersiz kullanıcı adı veya şifre", + "tooManyAttempts": "Çok fazla deneme yapıldı. Daha sonra tekrar deneyin.", + "cantReachServer": "Sunucuya ulaşılamıyor. Bağlantınızı kontrol edin.", + "sslError": "SSL hatası. Ayarlar > Gelişmiş ayarlar bölümüne gidin ve SSL doğrulamasını geçersiz kıl seçeneğini etkinleştirin.", + "unknownError": "Bilinmeyen hata", + "connectionNotCreated": "Bağlantı kurulamadı", + "connecting": "Bağlanılıyor...", + "connected": "Bağlantı kuruldu", + "selectedDisconnected": "Seçildi ancak bağlantı kesildi", + "connectionDefaultSuccessfully": "Bağlantı başarıyla varsayılan olarak ayarlandı.", + "connectionDefaultFailed": "Bağlantı varsayılan olarak ayarlanamadı.", + "noSavedConnections": "Kaydedilmiş bağlantı yok", + "cannotConnect": "Sunucuya bağlanılamıyor", + "connectionRemoved": "Bağlantı başarıyla kaldırıldı", + "connectionCannotBeRemoved": "Bağlantı kaldırılamaz.", + "remove": "Kaldır", + "removeWarning": "Bu AdGuard Home sunucusuyla olan bağlantıyı kaldırmak istediğinizden emin misiniz?", + "cancel": "İptal", + "defaultConnection": "Varsayılan bağlantı", + "setDefault": "Varsayılan sunucu yap", + "edit": "Düzenle", + "delete": "Sil", + "save": "Kaydet", + "serverStatus": "Sunucu durumu", + "connectionNotUpdated": "Bağlantı Güncellenmedi", + "ruleFilteringWidget": "Kural filtreleme", + "safeBrowsingWidget": "Güvenli gezinme", + "parentalFilteringWidget": "Ebeveyn filtreleme", + "safeSearchWidget": "Güvenli arama", + "ruleFiltering": "Kural filtreleme", + "safeBrowsing": "Güvenli gezinme", + "parentalFiltering": "Ebeveyn filtreleme", + "safeSearch": "Güvenli arama", + "serverStatusNotRefreshed": "Sunucu durumu yenilenemedi", + "loadingStatus": "Durum yükleniyor...", + "errorLoadServerStatus": "Sunucu durumu yüklenemedi", + "topQueriedDomains": "En çok sorgulananlar", + "viewMore": "Daha fazla göster", + "topClients": "Öne çıkan istemciler", + "topBlockedDomains": "En çok engellenenler", + "appSettings": "Uygulama ayarları", + "theme": "Tema", + "light": "Açık", + "dark": "Koyu", + "systemDefined": "Otomatik (Cihazınızın renk düzenine göre)", + "close": "Kapat", + "connectedTo": "Bağlandı:", + "selectedServer": "Seçili sunucu:", + "noServerSelected": "Seçili sunucu yok", + "manageServer": "Sunucuyu yönet", + "allProtections": "Tüm korumalar", + "userNotEmpty": "Kullanıcı adı boş bırakılamaz", + "passwordNotEmpty": "Şifre boş bırakılamaz", + "examplePath": "Örnek: /adguard", + "helperPath": "Eğer ters proxy kullanıyorsanız", + "aboutApp": "Uygulama hakkında", + "appVersion": "Uygulama sürümü", + "createdBy": "Geliştirici", + "clients": "İstemciler", + "allowed": "İzin verilen", + "blocked": "Engellenen", + "noClientsList": "Bu listede hiç istemci yok", + "activeClients": "Etkin", + "removeClient": "İstemciyi kaldır", + "removeClientMessage": "Bu istemciyi listeden çıkarmak istediğinize emin misiniz?", + "confirm": "Onayla", + "removingClient": "İstemci kaldırılıyor...", + "clientNotRemoved": "İstemci listeden çıkarılamadı", + "addClient": "İstemci ekle", + "list": "Liste", + "ipAddress": "IP adresi", + "ipNotValid": "IP adresi geçersiz", + "clientAddedSuccessfully": "İstemci listeye başarıyla eklendi", + "addingClient": "İstemci ekleniyor...", + "clientNotAdded": "İstemci listeye eklenemedi", + "clientAnotherList": "Bu istemci henüz başka bir listede", + "noSavedLogs": "Kayıtlı günlük yok", + "logs": "Günlükler", + "copyLogsClipboard": "Günlükleri panoya kopyala", + "logsCopiedClipboard": "Günlükler panoya kopyalandı", + "advancedSettings": "Gelişmiş ayarlar", + "dontCheckCertificate": "SSL sertifikasını kontrol etme", + "dontCheckCertificateDescription": "Sunucunun SSL sertifikası doğrulamasını geçersiz kılar.", + "advancedSetupDescription": "Gelişmiş seçenekleri yönet", + "settingsUpdatedSuccessfully": "Ayarlar başarıyla güncellendi.", + "cannotUpdateSettings": "Ayarlar güncellenemiyor.", + "restartAppTakeEffect": "Uygulamayı yeniden başlat", + "loadingLogs": "Günlükler yükleniyor...", + "logsNotLoaded": "Günlüklerin listesi yüklenemedi", + "processed": "İşlendi\nListe yok", + "processedRow": "İşlendi (Liste yok)", + "blockedBlacklist": "Engellendi\nKara Liste", + "blockedBlacklistRow": "Engellendi (Kara liste)", + "blockedSafeBrowsing": "Engellendi\nGüvenli gezinme", + "blockedSafeBrowsingRow": "Engellendi (Güvenli gezinme)", + "blockedParental": "Engellendi\nEbeveyn filtreleme", + "blockedParentalRow": "Engellendi (Ebeveyn filtreleme)", + "blockedInvalid": "Engellendi\nGeçersiz", + "blockedInvalidRow": "Engellendi (Geçersiz)", + "blockedSafeSearch": "Engellendi\nGüvenli arama", + "blockedSafeSearchRow": "Engellendi (Güvenli arama)", + "blockedService": "Engellendi\nBelirlenen hizmet", + "blockedServiceRow": "Engellendi (Belirlenen hizmet)", + "processedWhitelist": "İşlendi\nBeyaz liste", + "processedWhitelistRow": "İşlendi (Beyaz liste)", + "processedError": "İşlendi\nHata", + "processedErrorRow": "İşlendi (Hata)", + "rewrite": "Yeniden Yaz", + "status": "Durum", + "result": "Sonuç", + "time": "Saat", + "blocklist": "Engelleme Listesi", + "request": "İstek", + "domain": "Alan adı", + "type": "Tür", + "clas": "Sınıf", + "response": "Yanıt", + "dnsServer": "DNS sunucusu", + "elapsedTime": "İşlem süresi", + "responseCode": "Yanıt kodu", + "client": "İstemci", + "deviceIp": "IP adresi", + "deviceName": "İstemci adı", + "logDetails": "Günlük detayları", + "blockingRule": "Engelleme kuralı", + "blockDomain": "Alan adını engelle", + "couldntGetFilteringStatus": "Filtreleme durumu alınamıyor", + "unblockDomain": "Alan adı engelini kaldır", + "userFilteringRulesNotUpdated": "Kullanıcı filtreleme kuralları güncellenemedi", + "userFilteringRulesUpdated": "Kullanıcı filtreleme kuralları başarıyla güncellendi", + "savingUserFilters": "Kullanıcı filtreleri kaydediliyor...", + "filters": "Filtreler", + "logsOlderThan": "Daha eski günlükler", + "responseStatus": "Yanıt durumu", + "selectTime": "Zaman seç", + "notSelected": "Seçili değil", + "resetFilters": "Filtreleri sıfırla", + "noLogsDisplay": "Gösterilecek günlük yok", + "noLogsThatOld": "Seçilen zaman için kaydedilmiş herhangi bir günlük bulunmuyor olabilir. Daha yakın bir zaman seçmeyi deneyin.", + "apply": "Uygula", + "selectAll": "Hepsini seç", + "unselectAll": "Seçimleri kaldır", + "all": "Hepsi", + "filtered": "Filtrelenmiş", + "checkAppLogs": "Uygulama günlüklerini kontrol edin", + "refresh": "Yenile", + "search": "Ara", + "dnsQueries": "DNS sorguları", + "average": "Ortalama", + "blockedFilters": "Engellenen alan adları", + "malwarePhishingBlocked": "Engellenen zararlı içerikler", + "blockedAdultWebsites": "Engellenen yetişkin içerikler", + "generalSettings": "Genel ayarlar", + "generalSettingsDescription": "Çeşitli farklı ayarları yönet", + "hideZeroValues": "Sıfır değerlerini gizle", + "hideZeroValuesDescription": "Ana ekranda, değeri sıfır olan blokları gizler.", + "webAdminPanel": "Web yönetim paneli", + "visitGooglePlay": "Google Play sayfasını ziyaret et", + "gitHub": "Kaynak kodlarına GitHub'dan ulaşabilirsiniz", + "blockClient": "İstemciyi engelle", + "selectTags": "Etiketleri seç", + "noTagsSelected": "Seçili etiket yok", + "tags": "Etiketler", + "identifiers": "Tanımlayıcılar", + "identifier": "Tanımlayıcı", + "identifierHelper": "IP adresi, CIDR, MAC adresi veya ClientID", + "noIdentifiers": "Tanımlayıcı eklenmedi", + "useGlobalSettings": "Küresel ayarları kullan", + "enableFiltering": "Filtrelemeyi etkinleştir", + "enableSafeBrowsing": "Güvenli gezinmeyi etkinleştir", + "enableParentalControl": "Ebeveyn kontrolünü etkinleştir", + "enableSafeSearch": "Güvenli aramayı etkinleştir", + "blockedServices": "Engellenen hizmetler", + "selectBlockedServices": "Engellenen hizmetleri seç", + "noBlockedServicesSelected": "Engellenen hizmetler seçilmedi", + "services": "Hizmetler", + "servicesBlocked": "Hizmetler engellendi", + "tagsSelected": "Etiket seçildi", + "upstreamServers": "Üst sunucular", + "serverAddress": "Sunucu adresi", + "noUpstreamServers": "Üst sunucu yok.", + "willBeUsedGeneralServers": "Genel üst sunucular kullanılacak.", + "added": "Eklenenler", + "clientUpdatedSuccessfully": "İstemci ayarları başarıyla güncellendi", + "clientNotUpdated": "İstemci güncellenemedi", + "clientDeletedSuccessfully": "İstemci başarıyla kaldırıldı", + "clientNotDeleted": "İstemci silinemedi", + "options": "Seçenekler", + "loadingFilters": "Filtreler yükleniyor...", + "filtersNotLoaded": "Filtreler yüklenemedi.", + "whitelists": "Beyaz listeler", + "blacklists": "Kara listeler", + "rules": "Kurallar", + "customRules": "Özel kurallar", + "enabledRules": "Etkin kural", + "enabled": "Etkin", + "disabled": "Devre dışı", + "rule": "Kural", + "addCustomRule": "Özel kural ekle", + "removeCustomRule": "Özel kuralı kaldır", + "removeCustomRuleMessage": "Bu özel kuralı kaldırmak istediğinizden emin misiniz?", + "updatingRules": "Özel kurallar güncelleniyor...", + "ruleRemovedSuccessfully": "Kural başarıyla kaldırıldı", + "ruleNotRemoved": "Kural kaldırılamadı", + "ruleAddedSuccessfully": "Kural başarıyla eklendi", + "ruleNotAdded": "Kural eklenemedi", + "noCustomFilters": "Özel filtreler yok", + "noBlockedClients": "Engellenmiş istemci yok", + "noBlackLists": "Kara listeler yok", + "noWhiteLists": "Beyaz listeler yok", + "addWhitelist": "Beyaz liste ekle", + "addBlacklist": "Kara liste ekle", + "urlNotValid": "Bağlantı adresi geçerli değil", + "urlAbsolutePath": "Bağlantı adresi veya dosya yolu", + "addingList": "Liste ekleniyor...", + "listAdded": "Liste başarıyla eklendi. Eklenen öğeler:", + "listAlreadyAdded": "Liste zaten eklenmiş", + "listUrlInvalid": "Liste bağlantı adresi geçersiz", + "listNotAdded": "Liste eklenemedi", + "listDetails": "Liste detayları", + "listType": "Liste türü", + "whitelist": "Beyaz liste", + "blacklist": "Kara liste", + "latestUpdate": "Son güncelleme", + "disable": "Devre dışı bırak", + "enable": "Etkinleştir", + "currentStatus": "Mevcut durum", + "listDataUpdated": "Liste verileri başarıyla güncellendi", + "listDataNotUpdated": "Liste verileri güncellenemedi", + "updatingListData": "Liste verileri güncelleniyor...", + "editWhitelist": "Beyaz listeyi düzenle", + "editBlacklist": "Kara listeyi düzenle", + "deletingList": "Liste siliniyor...", + "listDeleted": "Liste başarıyla silindi", + "listNotDeleted": "Liste silinemedi", + "deleteList": "Listeyi sil", + "deleteListMessage": "Bu listeyi silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.", + "serverSettings": "Sunucu ayarları", + "serverInformation": "Sunucu bilgisi", + "serverInformationDescription": "Sunucu bilgisi ve durumunu öğren", + "loadingServerInfo": "Sunucu bilgisi yükleniyor...", + "serverInfoNotLoaded": "Sunucu bilgisi yüklenemedi.", + "dnsAddresses": "DNS adresleri", + "seeDnsAddresses": "DNS adreslerine göz at", + "dnsPort": "DNS bağlantı noktası", + "httpPort": "HTTP bağlantı noktası", + "protectionEnabled": "Koruma durumu", + "dhcpAvailable": "DHCP durumu", + "serverRunning": "Sunucu durumu", + "serverVersion": "Sunucu sürümü", + "serverLanguage": "Sunucu dili", + "yes": "Etkin", + "no": "Mevcut değil", + "allowedClients": "İzin verilen istemciler", + "disallowedClients": "İzin verilmeyen istemciler", + "disallowedDomains": "İzin verilmeyen alan adları", + "accessSettings": "Erişim ayarları", + "accessSettingsDescription": "Sunucu için erişim kurallarını yapılandır", + "loadingClients": "İstemciler yükleniyor...", + "clientsNotLoaded": "İstemciler yüklenemedi.", + "noAllowedClients": "İzin verilmiş istemci yok", + "allowedClientsDescription": "Eğer bu liste girdiler içeriyorsa, AdGuard Home yalnızca bu istemcilerden gelen talepleri kabul edecektir.", + "blockedClientsDescription": "Bu liste girdileri içeriyorsa, AdGuard Home bu istemcilerden gelen talepleri reddedecektir. Bu alan adı, İzin Verilen İstemciler'de girdi varsa görmezden gelinir.", + "disallowedDomainsDescription": "AdGuard Home, bu alan adlarına uyan DNS sorgularını reddeder ve bu sorgular sorgu günlüğünde bile görünmez.", + "addClientFieldDescription": "CIDR'ler, IP adresi veya ClientID", + "clientIdentifier": "İstemci tanımlayıcısı", + "allowClient": "İstemciye izin ver", + "disallowClient": "İstemciyi engelle", + "noDisallowedDomains": "İzin verilmeyen alan adı yok", + "domainNotAdded": "Alan adı eklenemedi", + "statusSelected": "Durum seçildi.", + "updateLists": "Listeleri güncelle", + "checkHostFiltered": "Filtrelemeyi kontrol et", + "updatingLists": "Listeler güncelleniyor...", + "listsUpdated": "Listeler güncellendi", + "listsNotUpdated": "Listeler güncellenemedi", + "listsNotLoaded": "Listeler yüklenemedi", + "domainNotValid": "Alan adı geçersiz", + "check": "Kontrol et", + "checkingHost": "Kontrol ediliyor", + "errorCheckingHost": "Kontrol etme başarısız", + "block": "Engelle", + "unblock": "Engeli kaldır", + "custom": "Özel", + "addImportant": "Başına $important ekle", + "howCreateRules": "Özel kurallar nasıl oluşturulur?", + "examples": "Örnekler", + "example1": "example.org ve tüm alt alan adlarına erişimi engeller.", + "example2": "example.org ve tüm alt alan adlarına erişimi engellemeyi kaldırır.", + "example3": "Yorum ekler.", + "example4": "Belirtilen düzenli ifadeye uyan alan adlarına erişimi engeller.", + "moreInformation": "Daha fazla bilgi", + "addingRule": "Kural ekleniyor...", + "deletingRule": "Kural siliniyor...", + "enablingList": "Liste etkinleştiriliyor...", + "disablingList": "Liste devre dışı bırakılıyor...", + "savingList": "Liste kaydediliyor...", + "disableFiltering": "Filtrelemeyi devre dışı bırak", + "enablingFiltering": "Filtreleme etkinleştiriliyor...", + "disablingFiltering": "Filtreleme devre dışı bırakılıyor...", + "filteringStatusUpdated": "Filtreleme durumu başarıyla güncellendi", + "filteringStatusNotUpdated": "Filtreleme durumu güncellenemedi", + "updateFrequency": "Güncelleme sıklığı", + "never": "Asla", + "hour1": "1 saat", + "hours12": "12 saat", + "hours24": "24 saat", + "days3": "3 gün", + "days7": "7 gün", + "changingUpdateFrequency": "Değiştiriliyor...", + "updateFrequencyChanged": "Güncelleme sıklığı başarıyla değiştirildi", + "updateFrequencyNotChanged": "Güncelleme sıklığı değiştirilemedi", + "updating": "Değerler güncelleniyor...", + "blockedServicesUpdated": "Engellenen hizmetler başarıyla güncellendi", + "blockedServicesNotUpdated": "Engellenen hizmetler güncellenemedi", + "insertDomain": "Filtreleme durumunu kontrol etmek için bir alan adı ekleyin.", + "dhcpSettings": "DHCP ayarları", + "dhcpSettingsDescription": "DHCP sunucusunu yapılandır", + "dhcpSettingsNotLoaded": "DHCP ayarları yüklenemedi", + "loadingDhcp": "DHCP ayarları yükleniyor...", + "enableDhcpServer": "DHCP sunucusunu etkinleştir", + "selectInterface": "Arayüz seç", + "hardwareAddress": "Donanım adresi", + "gatewayIp": "Ağ Geçidi IP'si", + "ipv4addresses": "IPv4 adresleri", + "ipv6addresses": "IPv6 adresleri", + "neededSelectInterface": "DHCP sunucusunu yapılandırmak için bir arayüz seçmeniz gerekir.", + "ipv4settings": "IPv4 ayarları", + "startOfRange": "Menzilin başlangıcı", + "endOfRange": "Menzilin sonu", + "ipv6settings": "IPv6 ayarları", + "subnetMask": "Alt ağ maskesi", + "subnetMaskNotValid": "Alt ağ maskesi geçerli değil", + "gateway": "Ağ Geçidi", + "gatewayNotValid": "Ağ geçidi geçerli değil", + "leaseTime": "Kira süresi", + "seconds": "{time} saniye", + "leaseTimeNotValid": "Kira süresi geçerli değil", + "restoreConfiguration": "Yapılandırmayı sıfırla", + "restoreConfigurationMessage": "Devam etmek istediğinizden emin misiniz? Bu, tüm yapılandırmayı sıfırlayacak. Bu işlem geri alınamaz.", + "changeInterface": "Arayüzü değiştir", + "savingSettings": "Ayarlar kaydediliyor...", + "settingsSaved": "Ayarlar başarıyla kaydedildi", + "settingsNotSaved": "Ayarlar kaydedilemedi", + "restoringConfig": "Yapılandırma geri yükleniyor...", + "configRestored": "Yapılandırma başarıyla sıfırlandı", + "configNotRestored": "Yapılandırma sıfırlanamadı", + "dhcpStatic": "DHCP statik kiraları", + "noDhcpStaticLeases": "DHCP statik kirası bulunamadı", + "deleting": "Siliniyor...", + "staticLeaseDeleted": "DHCP statik kirası başarıyla silindi", + "staticLeaseNotDeleted": "DHCP statik kirası silinemedi", + "deleteStaticLease": "Statik kirasını sil", + "deleteStaticLeaseDescription": "DHCP statik kirası silinecek. Bu işlem geri alınamaz.", + "addStaticLease": "Statik kira ekleyin", + "macAddress": "MAC adresi", + "macAddressNotValid": "MAC adresi geçersiz", + "hostName": "Ana bilgisayar adı", + "hostNameError": "Ana bilgisayar adı boş olamaz", + "creating": "Oluşturuluyor...", + "staticLeaseCreated": "DHCP statik kirası başarıyla oluşturuldu", + "staticLeaseNotCreated": "DHCP statik kirası oluşturulamadı", + "staticLeaseExists": "DHCP statik kirası zaten mevcut", + "serverNotConfigured": "Sunucu yapılandırılmamış", + "restoreLeases": "Kiraları sıfırla", + "restoreLeasesMessage": "Devam etmek istediğinizden emin misiniz? Bu, mevcut tüm kiraları sıfırlayacaktır. Bu işlem geri alınamaz.", + "restoringLeases": "Kiralar sıfırlanıyor...", + "leasesRestored": "Kiralar başarıyla sıfırlandı", + "leasesNotRestored": "Kiralar sıfırlanamadı", + "dhcpLeases": "DHCP kiraları", + "noLeases": "Kullanılabilir DHCP kiraları yok", + "dnsRewrites": "DNS yeniden yazımları", + "dnsRewritesDescription": "Özel DNS kurallarını yapılandır", + "loadingRewriteRules": "Yeniden yazım kuralları yükleniyor...", + "rewriteRulesNotLoaded": "DNS yeniden yazım kuralları yüklenemedi.", + "noRewriteRules": "DNS yeniden yazım kuralları yok", + "answer": "Yanıt", + "deleteDnsRewrite": "DNS yeniden yazımını sil", + "deleteDnsRewriteMessage": "Bu DNS yeniden yazımını silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.", + "dnsRewriteRuleDeleted": "DNS yeniden yazım kuralı başarıyla silindi", + "dnsRewriteRuleNotDeleted": "DNS yeniden yazım kuralı silinemedi", + "addDnsRewrite": "DNS yeniden yazımı ekle", + "addingRewrite": "Yeniden yazım ekleniyor...", + "dnsRewriteRuleAdded": "DNS yeniden yazım kuralı başarıyla eklendi", + "dnsRewriteRuleNotAdded": "DNS yeniden yazım kuralı eklenemedi", + "logsSettings": "Günlük ayarları", + "enableLog": "Günlüğü etkinleştir", + "clearLogs": "Günlükleri temizle", + "anonymizeClientIp": "İstemci IP'sini gizle", + "hours6": "6 saat", + "days30": "30 gün", + "days90": "90 gün", + "retentionTime": "Saklama süresi", + "selectOneItem": "Bir öğe seçin", + "logSettingsNotLoaded": "Günlük ayarları yüklenemedi.", + "updatingSettings": "Ayarlar güncelleniyor...", + "logsConfigUpdated": "Günlük ayarları başarıyla güncellendi", + "logsConfigNotUpdated": "Günlük ayarları güncellenemedi", + "deletingLogs": "Günlükler temizleniyor...", + "logsCleared": "Günlükler başarıyla temizlendi", + "logsNotCleared": "Günlükler temizlenemedi", + "runningHomeAssistant": "Ev asistanı üzerinde çalıştır", + "serverError": "Sunucu hatası", + "noItems": "Burada gösterilecek öğe yok", + "dnsSettings": "DNS ayarları", + "dnsSettingsDescription": "DNS sunucuları ile bağlantıyı yapılandır", + "upstreamDns": "Üst DNS sunucuları", + "bootstrapDns": "Önyükleme DNS sunucuları", + "noUpstreamDns": "Üst DNS sunucuları eklenmedi.", + "dnsMode": "DNS modu", + "noDnsMode": "DNS modu seçili değil", + "loadBalancing": "Yük dengeleme", + "parallelRequests": "Paralel istekler", + "fastestIpAddress": "En hızlı IP adresi", + "loadBalancingDescription": "Her seferinde bir üst sunucuya sorgu yapar. AdGuard Home, sunucuyu seçmek için ağırlıklı rastgele algoritmasını kullanır, böylece en hızlı sunucu daha sık kullanılır.", + "parallelRequestsDescription": "Tüm üst sunucuları aynı anda sorgulayarak çözümlemeyi hızlandırmak için paralel sorgular kullanılır.", + "fastestIpAddressDescription": "Tüm DNS sunucularına sorgu yapın ve tüm yanıtlar arasında en hızlı IP adresini döndürür. Bu, AdGuard Home'un tüm DNS sunucularından yanıtları beklemesi gerektiği için DNS sorgularını yavaşlatır, ancak genel bağlantıyı iyileştirir.", + "noBootstrapDns": "Önyükleme DNS sunucuları eklenmedi.", + "bootstrapDnsServersInfo": "Önyükleme ​​DNS sunucuları, üst kaynaklarda belirttiğiniz DoH/DoT çözümleyicilerinin IP adreslerini çözmek için kullanılır.", + "privateReverseDnsServers": "Özel ters DNS sunucuları", + "privateReverseDnsServersDescription": "AdGuard Home'un yerel PTR sorguları için kullandığı DNS sunucuları. Bu sunucular, özel IP aralıklarındaki adresler için ters DNS kullanarak PTR isteklerini çözmek için kullanılır, örneğin '192.168.12.34' olarak ayarlanmamışsa AdGuard Home, AdGuard Home'un kendi adresleri dışında, işletim sisteminizin varsayılan DNS çözümleyicilerinin adreslerini kullanır.", + "reverseDnsDefault": "Varsayılan olarak, AdGuard Home aşağıdaki ters DNS çözümleyicilerini kullanır", + "addItem": "Öğe ekle", + "noServerAddressesAdded": "Sunucu adresleri eklenmedi.", + "usePrivateReverseDnsResolvers": "Özel ters DNS çözümleyicilerini kullan", + "usePrivateReverseDnsResolversDescription": "Bu üst sunucuları kullanarak yerel olarak sunulan adresler için ters DNS sorguları gerçekleştirin. Devre dışı bırakılırsa, AdGuard Home, DHCP, /etc/hosts vb. kaynaklardan bilinen istemciler dışında tüm PTR isteklerine NXDOMAIN yanıtı verir.", + "enableReverseResolving": "İstemcilerin IP adreslerinin ters çözümlemesini etkinleştir", + "enableReverseResolvingDescription": "Karşılık gelen çözümleyicilere (yerel istemciler için özel DNS sunucuları, genel IP adresleri olan istemciler için üst sunuculara) PTR sorguları göndererek istemcilerin IP adreslerini ana makine adlarının tersine çözer.", + "dnsServerSettings": "AdGuard Home DNS sunucusu ayarları", + "limitRequestsSecond": "Sıklık limiti", + "valueNotNumber": "Değer bir sayı değil", + "enableEdns": "EDNS istemci alt ağını etkinleştir", + "enableEdnsDescription": "Kaynak yönü isteklerine EDNS İstemci Alt Ağı Seçeneği (ECS) ekler ve istemciler tarafından gönderilen değerleri sorgu günlüğüne kaydeder.", + "enableDnssec": "DNSSEC'i etkinleştir", + "enableDnssecDescription": "Giden DNS sorguları için DNSSEC özelliğini etkinleştirir ve sonucu kontrol eder. (DNSSEC etkinleştirilmiş bir çözümleyici gerekli)", + "disableResolvingIpv6": "IPv6 adreslerinin çözümlenmesini devre dışı bırak", + "disableResolvingIpv6Description": "IPv6 adresleri için tüm DNS sorgularını bırakır (AAAA yazar) ve HTTPS yanıtlarından IPv6 ipuçlarını kaldırır.", + "blockingMode": "Engelleme modu", + "defaultMode": "Varsayılan", + "defaultDescription": "Reklam engelleme tarzı bir kural tarafından engellendiğinde sıfır IP adresi ile yanıt verir. (A için 0.0.0.0; :: AAAA için) /etc/hosts tarzı bir kural tarafından engellendiğinde kuralda belirtilen IP adresi ile yanıt verir.", + "refusedDescription": "REFUSED kodu ile yanıt verir.", + "nxdomainDescription": "NXDOMAIN kodu ile yanıt verir.", + "nullIp": "Boş IP", + "nullIpDescription": "Sıfır IP adresi ile yanıt verir. (A için 0.0.0.0; :: AAAA için)", + "customIp": "Özel IP", + "customIpDescription": "Manuel olarak ayarlanmış bir IP adresi ile yanıt verir.", + "dnsCacheConfig": "DNS önbellek yapılandırması", + "cacheSize": "Önbellek boyutu", + "inBytes": "Alınacak önbelleğin boyutunu ayarla (Bayt olarak)", + "overrideMinimumTtl": "Minimum kullanım süresini geçersiz kıl", + "overrideMinimumTtlDescription": "DNS yanıtlarını önbelleğe alırken üst sunucudan alınan minimum kullanım süresi değerini (TTL) saniye olarak ayarlayın.", + "overrideMaximumTtl": "Maksimum kullanım süresini geçersiz kıl", + "overrideMaximumTtlDescription": "DNS yanıtlarını önbelleğe alırken üst sunucudan alınan maksimum kullanım süresi değerini (TTL) saniye olarak ayarlayın.", + "optimisticCaching": "İyimser önbelleğe alma", + "optimisticCachingDescription": "Girişlerin süresi dolmuş olsa bile Adguard Home'un önbellekten yanıt vermesini sağlar ve aynı zamanda bunları yenilemeye çalışır.", + "loadingDnsConfig": "DNS yapılandırması yükleniyor...", + "dnsConfigNotLoaded": "DNS yapılandırması yüklenemedi.", + "blockingIpv4": "IPv4 engelleniyor", + "blockingIpv4Description": "Engellenen bir A isteği için döndürülecek IP adresi", + "blockingIpv6": "IPv6 engelleniyor", + "blockingIpv6Description": "Engellenen bir AAAA isteği için döndürülecek IP adresi", + "invalidIp": "Geçersiz IP adresi", + "dnsConfigSaved": "DNS sunucusu yapılandırması başarıyla kaydedildi", + "dnsConfigNotSaved": "DNS sunucusu yapılandırması kaydedilemedi", + "savingConfig": "Yapılandırma kaydediliyor...", + "someValueNotValid": "Bazı değerler geçerli değil", + "upstreamDnsDescription": "Üst sunucuları ve DNS modunu yapılandır", + "bootstrapDnsDescription": "Önyükleme DNS sunucularını yapılandır", + "privateReverseDnsDescription": "Özel DNS çözümleyicileri yapılandır ve özel ters DNS çözümlemeyi etkinleştir", + "dnsServerSettingsDescription": "Hız limiti, engelleme modu ve daha fazlasını yapılandır", + "dnsCacheConfigDescription": "Sunucunun DNS önbelleğini nasıl yöneteceğini yapılandır", + "comment": "Yorum", + "address": "Adres", + "commentsDescription": "Yorumlar her zaman # işareti ile başlar. Eklemenize gerek yok, otomatik olarak eklenecektir.", + "encryptionSettings": "Şifreleme ayarları", + "encryptionSettingsDescription": "Şifreleme (HTTPS/QUIC/TLS) desteği", + "loadingEncryptionSettings": "Şifreleme ayarları yükleniyor...", + "encryptionSettingsNotLoaded": "Şifreleme ayarları yüklenemedi.", + "enableEncryption": "Şifrelemeyi etkinleştir", + "enableEncryptionTypes": "HTTPS, DNS-over-HTTPS ve DNS-over-TLS", + "enableEncryptionDescription": "Eğer şifreleme etkinleştirilmişse, AdGuard Home yönetici arayüzü HTTPS üzerinden çalışacaktır ve DNS sunucusu DNS üzerinden HTTPS ve TLS ile gelen isteklere yanıt verecektir.", + "serverConfiguration": "Sunucu yapılandırması", + "domainName": "Alan adı", + "domainNameDescription": "Eğer ayarlanırsa, AdGuard Home istemci kimliklerini tespit eder, DDR sorgularına yanıt verir ve ek bağlantı doğrulamalarını gerçekleştirir. Ayarlanmazsa, bu özellikler devre dışı bırakılır. Sertifikadaki DNS adlarından biriyle eşleşmelidir.", + "redirectHttps": "Otomatik olarak HTTPS'e yönlendir", + "httpsPort": "HTTPS bağlantı noktası", + "tlsPort": "DNS-over-TLS bağlantı noktası", + "dnsOverQuicPort": "DNS-over-QUIC bağlantı noktası", + "certificates": "Sertifikalar", + "certificatesDescription": "Şifreleme kullanmak için, alan adınız için geçerli bir SSL sertifikası zinciri sağlamanız gereklidir. letsencrypt.org'dan ücretsiz bir sertifika alabilir veya güvenilir sertifika yetkililerinden satın alabilirsiniz.", + "certificateFilePath": "Sertifika dosyası belirle", + "pasteCertificateContent": "Sertifika içeriğini yapıştır", + "certificatePath": "Sertifikanın dosya yolu", + "certificateContent": "Sertifika içeriği", + "privateKey": "Özel anahtarlar", + "privateKeyFile": "Özel anahtar dosyası belirle", + "pastePrivateKey": "Özel anahtar içeriğini yapıştır", + "usePreviousKey": "Önceden kaydedilmiş olan anahtarı kullan", + "privateKeyPath": "Özel anahtarın dosya yolu", + "invalidCertificate": "Geçersiz sertifika", + "invalidPrivateKey": "Geçersiz özel anahtar", + "validatingData": "Veri doğrulama", + "dataValid": "Veri geçerli", + "dataNotValid": "Veri geçersiz", + "encryptionConfigSaved": "Şifreleme yapılandırması başarıyla kaydedildi", + "encryptionConfigNotSaved": "Şifreleme yapılandırması kaydedilemedi", + "configError": "Yapılandırma hatası", + "enterOnlyCertificate": "Yalnızca sertifikayı girin. ---BEGIN--- ve ---END--- satırlarını girmeyin.", + "enterOnlyPrivateKey": "Yalnızca anahtarı girin. ---BEGIN--- ve ---END--- satırlarını girmeyin.", + "noItemsSearch": "Bu arama için hiçbir öğe yok.", + "clearSearch": "Aramayı temizle", + "exitSearch": "Aramadan çık", + "searchClients": "İstemcileri ara", + "noClientsSearch": "Bu arama ile ilgili hiçbir istemci bulunamadı.", + "customization": "Özelleştirme", + "customizationDescription": "Bu uygulamayı özelleştir", + "color": "Renk", + "useDynamicTheme": "Dinamik renk teması kullan", + "red": "Kırmızı", + "green": "Yeşil", + "blue": "Mavi", + "yellow": "Sarı", + "orange": "Turuncu", + "brown": "Kahverengi", + "cyan": "Camgöbeği", + "purple": "Mor", + "pink": "Pembe", + "deepOrange": "Koyu turuncu", + "indigo": "Çivit mavisi", + "useThemeColorStatus": "Durum için tema rengini kullan", + "useThemeColorStatusDescription": "Yeşil ve kırmızı durum renklerini tema rengi ve gri ile değiştirir.", + "invalidCertificateChain": "Geçersiz sertifika zinciri", + "validCertificateChain": "Geçerli sertifika zinciri", + "subject": "Konu", + "issuer": "Veren", + "expires": "Süresi dolacak", + "validPrivateKey": "Geçerli özel anahtar", + "expirationDate": "Son kullanma tarihi", + "keysNotMatch": "Geçersiz bir sertifika veya anahtar: tls: özel anahtar genel anahtarla eşleşmiyor.", + "timeLogs": "Günlüklerde işlem süresini göster", + "timeLogsDescription": "Günlükler listesinde zaman yerine işlem süresini gösterir.", + "hostNames": "Ana bilgisayar adları", + "keyType": "Anahtar türü", + "updateAvailable": "Güncelleme mevcut", + "installedVersion": "Yüklü sürüm", + "newVersion": "Yeni sürüm", + "source": "Kaynak", + "downloadUpdate": "Güncellemeyi indir", + "download": "İndir", + "doNotRememberAgainUpdate": "Bu sürüm için tekrar hatırlama.", + "downloadingUpdate": "İndiriliyor", + "completed": "Tamamlandı", + "permissionNotGranted": "İzin verilmedi", + "inputSearchTerm": "Bir arama terimi girin.", + "answers": "Yanıtlar", + "copyClipboard": "Panoya kopyala", + "domainCopiedClipboard": "Alan adı panoya kopyalandı", + "clearDnsCache": "DNS önbelleğini temizle", + "clearDnsCacheMessage": "DNS önbelleğini temizlemek istediğinizden emin misiniz?", + "dnsCacheCleared": "DNS önbelleği başarıyla temizlendi", + "clearingDnsCache": "Önbellek temizleniyor...", + "dnsCacheNotCleared": "DNS önbelleği temizlenemedi", + "clientsSelected": "Seçilmiş istemci", + "invalidDomain": "Geçersiz alan adı", + "loadingBlockedServicesList": "Engellenen hizmetler listesi yükleniyor...", + "blockedServicesListNotLoaded": "Engellenen hizmetler listesi yüklenemedi", + "error": "Hata", + "updates": "Güncellemeler", + "updatesDescription": "AdGuard Home sunucusunu güncelle", + "updateNow": "Şimdi güncelle", + "currentVersion": "Yüklü sürüm", + "requestStartUpdateFailed": "Güncellemeyi başlatma isteği başarısız oldu", + "requestStartUpdateSuccessful": "Güncellemeyi başlatma isteği başarılı", + "serverUpdated": "Sunucu güncellendi", + "unknownStatus": "Bilinmeyen durum", + "checkingUpdates": "Güncellemeler kontrol ediliyor...", + "checkUpdates": "Güncellemeleri kontrol et", + "requestingUpdate": "Güncelleme talep ediliyor...", + "autoupdateUnavailable": "Otomatik güncelleme kullanılamıyor", + "autoupdateUnavailableDescription": "Otomatik güncelleme servisi bu sunucu için kullanılamıyor. Bunun nedeni sunucunun bir Docker konteynerinde çalışıyor olması olabilir. Sunucunuzu manuel olarak güncellemeniz gerekecektir.", + "minute": "{time} dakika", + "minutes": "{time} dakika", + "hour": "{time} saat", + "hours": "{time} saat", + "remainingTime": "Kalan süre", + "safeSearchSettings": "Güvenli arama ayarları", + "loadingSafeSearchSettings": "Güvenli arama ayarları yükleniyor...", + "safeSearchSettingsNotLoaded": "Güvenli arama ayarları yüklenirken hata oluştu.", + "loadingLogsSettings": "Günlük ayarları yükleniyor...", + "selectOptionLeftColumn": "Sol sütundan bir seçenek seçin", + "selectClientLeftColumn": "Sol sütundan bir istemci seçin", + "disableList": "Listeyi devre dışı bırak", + "enableList": "Listeyi etkinleştir", + "screens": "Ekranlar", + "copiedClipboard": "Panoya kopyalandı", + "seeDetails": "Detayları gör", + "listNotAvailable": "Liste mevcut değil", + "copyListUrl": "Bağlantıyı kopyala", + "listUrlCopied": "Liste bağlantısı panoya kopyalandı", + "unsupportedVersion": "Desteklenmeyen sürüm", + "unsupprtedVersionMessage": "Sunucu sürümünüz {version} için destek garantisi verilmiyor. Bu uygulamanın bu sunucu sürümüyle çalışmasında bazı sorunlar olabilir. AdGuard Home Yöneticisi, AdGuard Home sunucunun kararlı sürümleriyle çalışacak şekilde tasarlanmıştır. Alfa ve beta sürümleriyle çalışabilir, ancak uyumluluk garanti edilmez ve uygulama bu sürümlerle çalışırken bazı sorunlar yaşayabilir.", + "iUnderstand": "Anladım", + "appUpdates": "Uygulama güncellemeleri", + "usingLatestVersion": "En son sürümü kullanıyorsunuz", + "ipLogs": "Günlüklerde IP adresini göster", + "ipLogsDescription": "Günlükler listesinde istemci adı yerine IP adresini gösterir.", + "application": "Uygulama", + "combinedChart": "Birleştirilmiş grafik", + "combinedChartDescription": "Tüm grafikleri bir araya getirir.", + "statistics": "İstatistikler", + "errorLoadFilters": "Filtreler yüklenirken hata oluştu.", + "clientRemovedSuccessfully": "İstemci başarıyla kaldırıldı.", + "editRewriteRule": "Yeniden yazım kuralını düzenle", + "dnsRewriteRuleUpdated": "DNS yeniden yazım kuralı başarıyla güncellendi", + "dnsRewriteRuleNotUpdated": "DNS yeniden yazım kuralı güncellenemedi", + "updatingRule": "Kural güncelleniyor...", + "serverUpdateNeeded": "Sunucu güncellemesi gerekli", + "updateYourServer": "Bu özelliği kullanmak için AdGuard Home sunucunuzu {version} veya üzeri bir sürüme güncelleyin.", + "january": "Ocak", + "february": "Şubat", + "march": "Mart", + "april": "Nisan", + "may": "Mayıs", + "june": "Haziran", + "july": "Temmuz", + "august": "Ağustos", + "september": "Eylül", + "october": "Ekim", + "november": "Kasım", + "december": "Aralık", + "malwarePhishing": "Zararlı yazılım/oltalama", + "queries": "Sorgular", + "adultSites": "Yetişkin içerikler", + "quickFilters": "Hızlı filtreler", + "searchDomainInternet": "Alan adını arat", + "hideServerAddress": "Sunucu adresini gizle", + "hideServerAddressDescription": "Ana ekranda sunucu adresini gizler.", + "topItemsOrder": "Öne çıkan öğeler sıralaması", + "topItemsOrderDescription": "Ana ekrandaki öne çıkan öğe listelerini sıralayın.", + "topItemsReorderInfo": "Yeniden sıralamak için bir öğeyi basılı tutun ve kaydırın.", + "discardChanges": "Değişiklikleri iptal et", + "discardChangesDescription": "Değişiklikleri iptal etmek istediğinizden emin misiniz?", + "others": "Diğerleri", + "showChart": "Göster", + "hideChart": "Gizle", + "showTopItemsChart": "Öne çıkan öğeler grafiği", + "showTopItemsChartDescription": "Varsayılan olarak öne çıkan öğeler bölümünde halka grafiğini gösterir. Sadece mobil görünümü etkiler.", + "openMenu": "Menüyü genişlet", + "closeMenu": "Menüyü daralt", + "openListUrl": "Liste bağlantısını aç", + "selectionMode": "Seçim modu", + "enableDisableSelected": "Seçili öğeleri etkinleştir veya devre dışı bırak", + "deleteSelected": "Seçili öğeleri sil", + "deleteSelectedLists": "Seçili listeleri sil", + "allSelectedListsDeletedSuccessfully": "Seçilen tüm listeler başarıyla silindi.", + "deletionResult": "Silinme sonucu", + "deletingLists": "Listeler siliniyor...", + "failedElements": "Başarısız öğeler", + "processingLists": "Listeler işleniyor...", + "enableDisableResult": "Sonucu etkinleştir veya devre dışı bırak", + "selectedListsEnabledDisabledSuccessfully": "Seçilen tüm listeler başarıyla etkinleştirildi veya devre dışı bırakıldı", + "sslWarning": "Kendinden imzalı bir sertifika ile HTTPS bağlantısı kullanıyorsanız, Ayarlar > Gelişmiş ayarlar bölümünde \"SSL sertifikasını asla kontrol etme\" seçeneğini etkinleştirdiğinizden emin olun.", + "unsupportedServerVersion": "Desteklenmeyen sunucu sürümü", + "unsupportedServerVersionMessage": "AdGuard Home sunucu sürümünüz çok eski ve AdGuard Home Manager tarafından desteklenmiyor. Bu uygulamayı kullanmak için AdGuard Home sunucunuzu daha yeni bir sürüme yükseltmeniz gerekecektir.", + "yourVersion": "Yüklü sürüm: {version}", + "minimumRequiredVersion": "Gerekli minimum sürüm: {version}", + "topUpstreams": "Öne çıkan DNS sunucuları", + "averageUpstreamResponseTime": "DNS sunucuları işlem süresi" , + "dhcpNotAvailable": "DHCP sunucusu kullanılamıyor.", + "osServerInstalledIncompatible": "AdGuard Home, işletim sisteminizde DHCP sunucusu çalıştıramıyor.", + "resetSettings": "Ayarları sıfırla", + "resetEncryptionSettingsDescription": "Şifreleme ayarlarını sıfırlamak istediğinizden emin misiniz?", + "resettingConfig": "Yapılandırma sıfırlanıyor...", + "configurationResetSuccessfully": "Yapılandırma başarıyla sıfırlandı", + "configurationResetError": "Yapılandırma sıfırlanamadı", + "testUpstreamDnsServers": "DNS sunucusunu test et", + "errorTestUpstreamDns": "DNS sunucularını test ederken hata oluştu.", + "useCustomIpEdns": "EDNS için özel IP kullan", + "useCustomIpEdnsDescription": "EDNS için özel IP kullanımına izin ver", + "sortingOptions": "Sıralama seçenekleri", + "fromHighestToLowest": "Yüksekten düşüğe", + "fromLowestToHighest": "Düşükten yükseğe", + "queryLogsAndStatistics": "Sorgu günlüğü ve istatistikler", + "ignoreClientQueryLog": "Sorgu günlüğünde bu istemciyi yoksay", + "ignoreClientStatistics": "İstatistiklerde bu istemciyi yoksay", + "savingChanges": "Değişiklikler kaydediliyor...", + "fallbackDnsServers": "Yedek DNS sunucuları", + "fallbackDnsServersDescription": "Yedek DNS sunucularını yapılandır", + "fallbackDnsServersInfo": "Üst DNS sunucuları yanıt vermediğinde kullanılan yedek DNS sunucularının listesi. Sözdizimi, yukarıdaki ana üst kaynak alanıyla aynıdır.", + "noFallbackDnsAdded": "Yedek DNS sunucusu eklenmedi.", + "blockedResponseTtl": "Engellenen yanıtın kullanım süresi", + "blockedResponseTtlDescription": "İstemcilerin filtrelenmiş bir yanıtı kaç saniye süreyle önbelleğe alması gerektiğini belirtir", + "invalidValue": "Geçersiz değer", + "noDataChart": "Bu grafiği görüntüleyecek veri yok.", + "noData": "Veri yok", + "unblockClient": "İstemci engelini kaldır", + "blockingClient": "İstemci engelleniyor...", + "unblockingClient": "İstemci engeli kaldırılıyor...", + "upstreamDnsCacheConfiguration": "DNS önbellek yapılandırması", + "enableDnsCachingClient": "Bu istemci için DNS önbelleğe almayı etkinleştir", + "dnsCacheSize": "DNS önbellek boyutu (Bayt cinsinden)", + "nameInvalid": "Ad gereklidir", + "oneIdentifierRequired": "En az bir tanımlayıcı gereklidir", + "dnsCacheNumber": "DNS önbellek boyutu bir rakam içermelidir", + "errors": "Hatalar", + "redirectHttpsWarning": "AdGuard Home sunucunuzda \"Otomatik olarak HTTPS'e yönlendir\" seçeneğini etkinleştirdiyseniz, bir HTTPS bağlantısı seçmeli ve sunucunuzun HTTPS bağlantı noktasını kullanmalısınız.", + "logsSettingsDescription": "Sorgu günlüklerini yapılandır", + "ignoredDomains": "Yok sayılan alan adları", + "noIgnoredDomainsAdded": "Yok sayılacak alan adı eklenmedi", + "pauseServiceBlocking": "Hizmet engellemeyi duraklat", + "newSchedule": "Yeni program", + "editSchedule": "Programı düzenle", + "timezone": "Zaman dilimi", + "monday": "Pazartesi", + "tuesday": "Salı", + "wednesday": "Çarşamba", + "thursday": "Perşembe", + "friday": "Cuma", + "saturday": "Cumartesi", + "sunday": "Pazar", + "from": "Başlangıç", + "to": "Bitiş", + "selectStartTime": "Başlangıç zamanını seç", + "selectEndTime": "Bitiş zamanını seç", + "startTimeBeforeEndTime": "Başlangıç zamanı bitiş zamanından önce olmalıdır.", + "noBlockingScheduleThisDevice": "Bu cihaz için herhangi bir engelleme programı bulunmamaktadır.", + "selectTimezone": "Bir zaman dilimi seç", + "selectClientsFiltersInfo": "Görüntülemek istediğiniz istemcileri seçin. Hiçbir istemci seçilmemişse, hepsi görüntülenecektir.", + "noDataThisSection": "Bu bölüm için veri yok.", + "statisticsSettings": "İstatistik ayarları", + "statisticsSettingsDescription": "İstatistikler için veri toplamayı yapılandır", + "loadingStatisticsSettings": "İstatistik ayarları yükleniyor...", + "statisticsSettingsLoadError": "İstatistik ayarları yüklenirken bir hata oluştu.", + "statisticsConfigUpdated": "İstatistik ayarları başarıyla güncellendi", + "statisticsConfigNotUpdated": "İstatistik ayarları güncellenemedi", + "customTimeInHours": "Özel zaman (Saat olarak)", + "invalidTime": "Geçersiz zaman", + "removeDomain": "Alan adını kaldır", + "addDomain": "Alan adı ekle", + "notLess1Hour": "Zaman 1 saatten az olamaz", + "rateLimit": "Hız sınırı", + "subnetPrefixLengthIpv4": "IPv4 için alt ağ önek uzunluğu", + "subnetPrefixLengthIpv6": "IPv6 için alt ağ önek uzunluğu", + "rateLimitAllowlist": "Hız sınırlama izin listesi", + "rateLimitAllowlistDescription": "Hız sınırlamasından hariç tutulan IP adresleri", + "dnsOptions": "DNS ayarları", + "editor": "Editör", + "editCustomRules": "Özel kuralları düzenle", + "savingCustomRules": "Özel kurallar kaydediliyor...", + "customRulesUpdatedSuccessfully": "Özel kurallar başarıyla güncellendi", + "customRulesNotUpdated": "Özel kurallar güncellenemedi", + "reorder": "Sırala", + "showHide": "Göster/gizle", + "noElementsReorderMessage": "Burada yeniden sıralamak için göster/gizle sekmesindeki bazı öğeleri etkinleştirin.", + "enablePlainDns": "Düz DNS'i etkinleştir", + "enablePlainDnsDescription": "Düz DNS varsayılan olarak etkindir. Tüm aygıtları şifrelenmiş DNS kullanmaya zorlamak için bunu devre dışı bırakabilirsiniz. Bunu yapmak için en az bir şifrelenmiş DNS protokolünü etkinleştirmeniz gerekir.", + "date": "Tarih", + "loadingChangelog": "Değişiklikler yükleniyor...", + "invalidIpOrUrl": "Geçersiz IP adresi veya URL", + "addPersistentClient": "Kalıcı istemci olarak ekle", + "blockThisClientOnly": "Yalnızca bu istemci için engelle", + "unblockThisClientOnly": "Yalnızca bu istemci için engeli kaldır", + "domainBlockedThisClient": "Bu istemci için {domain} engellendi", + "domainUnblockedThisClient": "Bu istemci için {domain} engeli kaldırıldı", + "disallowThisClient": "Bu istemciye izin verme", + "allowThisClient": "Bu istemciye izin ver", + "clientAllowedSuccessfully": "İstemciye başarıyla izin verildi", + "clientDisallowedSuccessfully": "İstemci başarıyla reddedildi", + "changesNotSaved": "Değişiklikler kaydedilemedi", + "allowingClient": "İstemciye izin veriliyor...", + "disallowingClient": "İstemci reddediliyor...", + "clientIpCopied": "İstemci IP'si panoya kopyalandı", + "clientNameCopied": "İstemci adı panoya kopyalandı", + "dnsServerAddressCopied": "DNS sunucu adresi panoya kopyalandı", + "select": "Seç", + "liveLogs": "Canlı günlükler", + "hereWillAppearRealtimeLogs": "Burada gerçek zamanlı günlükler görünecek.", + "applicationDetails": "Uygulama detayları", + "applicationDetailsDescription": "Uygulama deposu, mevcut olduğu mağazalar ve daha fazlası", + "myOtherApps": "Diğer uygulamalarım", + "myOtherAppsDescription": "Diğer uygulamalarımı kontrol et, bağış yap, destekle iletişime geç ve daha fazlası", + "topToBottom": "Yukarıdan aşağıya", + "bottomToTop": "Aşağıdan yukarıya", + "upstreamTimeout": "Üst sunucu zaman aşımı", + "upstreamTimeoutHelper": "Üst DNS sunucusundan yanıt bekleme süresini saniye cinsinden belirtir", + "fieldCannotBeEmpty": "Bu alan boş olamaz" +} diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb new file mode 100644 index 0000000..a789942 --- /dev/null +++ b/lib/l10n/app_zh.arb @@ -0,0 +1,793 @@ +{ + "home": "主页", + "settings": "设置", + "connect": "连接", + "servers": "服务器", + "createConnection": "创建连接", + "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": "安全浏览", + "parentalFiltering": "家长过滤", + "safeSearch": "安全搜索", + "serverStatusNotRefreshed": "无法更新服务器状态", + "loadingStatus": "加载服务器状态...", + "errorLoadServerStatus": "无法加载服务器状态", + "topQueriedDomains": "请求域名排行", + "viewMore": "查看更多", + "topClients": "客户端排行", + "topBlockedDomains": "被拦截域名排行", + "appSettings": "App 设置", + "theme": "主题", + "light": "明亮", + "dark": "暗夜", + "systemDefined": "跟随系统", + "close": "关闭", + "connectedTo": "连接到 :", + "selectedServer": "选择的服务器 :", + "noServerSelected": "未选择服务器", + "manageServer": "管理服务器", + "allProtections": "所有保护", + "userNotEmpty": "用户名不能为空", + "passwordNotEmpty": "密码不能为空", + "examplePath": "例如: /adguard", + "helperPath": "如果使用反向代理,可填写路径", + "aboutApp": "关于本应用", + "appVersion": "App 版本", + "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": "被过滤器拦截", + "malwarePhishingBlocked": "被拦截的恶意/钓鱼网站", + "blockedAdultWebsites": "被拦截的成人网站", + "generalSettings": "常规设置", + "generalSettingsDescription": "各种不同的设置", + "hideZeroValues": "隐藏零值", + "hideZeroValuesDescription": "在主屏幕上隐藏零值块", + "webAdminPanel": "Web 管理面板", + "visitGooglePlay": "访问 Google Play 页面", + "gitHub": "开源代码可在 GitHub 上获得", + "blockClient": "拦截客户端", + "selectTags": "选择标签", + "noTagsSelected": "未选择标签", + "tags": "标签", + "identifiers": "标识符", + "identifier": "标识符", + "identifierHelper": "IP 地址、CIDR、MAC 地址或客户端 ID", + "noIdentifiers": "未添加标识符", + "useGlobalSettings": "使用全局设置", + "enableFiltering": "启用过滤", + "enableSafeBrowsing": "启用安全浏览", + "enableParentalControl": "启用家长控制", + "enableSafeSearch": "启用安全搜索", + "blockedServices": "被拦截的服务", + "selectBlockedServices": "选择要拦截的服务", + "noBlockedServicesSelected": "未选择被拦截的服务", + "services": "服务", + "servicesBlocked": "被拦截的服务", + "tagsSelected": "已选择标签", + "upstreamServers": "上游服务器", + "serverAddress": "服务器地址", + "noUpstreamServers": "无上游服务器", + "willBeUsedGeneralServers": "将使用常规上游服务器", + "added": "已添加", + "clientUpdatedSuccessfully": "客户端更新成功", + "clientNotUpdated": "无法更新客户端", + "clientDeletedSuccessfully": "客户端删除成功", + "clientNotDeleted": "无法删除客户端", + "options": "选项", + "loadingFilters": "正在加载过滤器...", + "filtersNotLoaded": "无法加载过滤器", + "whitelists": "白名单", + "blacklists": "黑名单", + "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 查询,这些查询甚至不会出现在查询日志中", + "addClientFieldDescription": "CIDR、IP 地址或客户端 ID", + "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": "禁用过滤", + "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": "选择接口", + "hardwareAddress": "硬件地址", + "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": "重置租用", + "restoreLeasesMessage": "您确定要继续吗?这将重置所有现有租用 此操作无法撤消", + "restoringLeases": "正在重置租用...", + "leasesRestored": "租用重置成功", + "leasesNotRestored": "无法重置租用", + "dhcpLeases": "DHCP 租用", + "noLeases": "无可用的 DHCP 租用", + "dnsRewrites": "DNS 重写", + "dnsRewritesDescription": "配置自定义 DNS 规则", + "loadingRewriteRules": "正在加载重写规则...", + "rewriteRulesNotLoaded": "无法加载 DNS 重写规则", + "noRewriteRules": "无 DNS 重写规则", + "answer": "响应", + "deleteDnsRewrite": "删除 DNS 重写", + "deleteDnsRewriteMessage": "您确定要删除此 DNS 重写吗?此操作无法撤消", + "dnsRewriteRuleDeleted": "DNS 重写规则删除成功", + "dnsRewriteRuleNotDeleted": "无法删除 DNS 重写规则", + "addDnsRewrite": "添加 DNS 重写", + "addingRewrite": "正在添加重写...", + "dnsRewriteRuleAdded": "DNS 重写规则添加成功", + "dnsRewriteRuleNotAdded": "无法添加 DNS 重写规则", + "logsSettings": "日志设置", + "enableLog": "启用日志", + "clearLogs": "清除日志", + "anonymizeClientIp": "匿名化客户端 IP", + "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": "上游 DNS 服务器", + "bootstrapDns": "引导 DNS 服务器", + "noUpstreamDns": "未添加上游 DNS 服务器 ", + "dnsMode": "DNS 模式", + "noDnsMode": "未选择 DNS 模式", + "loadBalancing": "负载均衡", + "parallelRequests": "并行请求", + "fastestIpAddress": "最快的 IP 地址", + "loadBalancingDescription": "每次查询一个上游服务器 AdGuard Home 使用其加权随机算法选择服务器,以便更频繁地使用最快的服务器", + "parallelRequestsDescription": "使用并行查询同时加速解析,同时查询所有上游服务器", + "fastestIpAddressDescription": "查询所有 DNS 服务器并返回所有响应中最快的 IP 地址 这会减慢 DNS 查询,因为 AdGuard Home 必须等待所有 DNS 服务器的响应,但可以改善整体连接性", + "noBootstrapDns": "未添加引导 DNS 服务器 ", + "bootstrapDnsServersInfo": "引导 DNS 服务器用于解析您指定的上游 DoH/DoT 解析器的 IP 地址 ", + "privateReverseDnsServers": "私有反向 DNS 服务器", + "privateReverseDnsServersDescription": "AdGuard Home 用于本地 PTR 查询的 DNS 服务器 这些服务器用于解析私有 IP 范围内的地址的 PTR 请求,例如 \"192.168.12.34\" 如果未设置,AdGuard Home 将使用操作系统的默认 DNS 解析器地址,但不包括 AdGuard Home 本身的地址 ", + "reverseDnsDefault": "默认情况下,AdGuard Home 使用以下默认反向 DNS 解析器", + "addItem": "添加项目", + "noServerAddressesAdded": "未添加服务器地址", + "usePrivateReverseDnsResolvers": "使用私有反向 DNS 解析器", + "usePrivateReverseDnsResolversDescription": "使用这些上游服务器执行本地提供的地址的反向 DNS 查询 如果禁用,AdGuard Home 会对所有此类 PTR 请求(除了来自 DHCP、/etc/hosts 等已知客户端)响应 NXDOMAIN", + "enableReverseResolving": "启用客户端 IP 地址的反向解析", + "enableReverseResolvingDescription": "通过向相应的解析器发送 PTR 查询,将客户端 IP 地址进行反向解析为主机名(对于本地客户端使用私有 DNS 服务器,对于具有公共 IP 地址的客户端使用上游服务器)", + "dnsServerSettings": "AdGuard Home DNS 服务器设置", + "limitRequestsSecond": "每秒速率限制", + "valueNotNumber": "值不是数字", + "enableEdns": "启用 EDNS 客户子网", + "enableEdnsDescription": "在上游请求中添加 EDNS 客户子网选项(ECS),并在查询日志中记录客户端发送的值", + "enableDnssec": "启用 DNSSEC", + "enableDnssecDescription": "在传出的 DNS 查询中设置 DNSSEC 标志并检查结果(需要启用 DNSSEC 的解析器)", + "disableResolvingIpv6": "禁用 IPv6 地址解析", + "disableResolvingIpv6Description": "丢弃所有用于 IPv6 地址(AAAA 记录)的 DNS 查询", + "blockingMode": "拦截模式", + "defaultMode": "默认", + "defaultDescription": "当按照 Adblock 样式规则被拦截时,返回零 IP 地址(0.0.0.0 对应 A;:: 对应 AAAA);当按照 /etc/hosts 样式规则被拦截时,返回规则中指定的 IP 地址", + "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": "在缓存 DNS 响应时,从上游服务器接收到的最短的存活时间值(秒)", + "overrideMaximumTtl": "覆盖最大 TTL", + "overrideMaximumTtlDescription": "为 DNS 缓存条目设置最大存活时间值(秒)", + "optimisticCaching": "乐观缓存", + "optimisticCachingDescription": "即使条目已过期,AdGuard Home 也将从缓存中响应,并尝试刷新它们", + "loadingDnsConfig": "正在加载 DNS 配置...", + "dnsConfigNotLoaded": "无法加载 DNS 配置", + "blockingIpv4": "拦截 IPv4", + "blockingIpv4Description": "拦截包含 A 记录的请求时返回的 IP 地址", + "blockingIpv6": "拦截 IPv6", + "blockingIpv6Description": "拦截包含 AAAA 记录的请求时返回的 IP 地址", + "invalidIp": "无效的 IP 地址", + "dnsConfigSaved": "DNS 服务器配置保存成功", + "dnsConfigNotSaved": "DNS 服务器配置无法保存", + "savingConfig": "正在保存配置...", + "someValueNotValid": "某些值无效", + "upstreamDnsDescription": "配置上游服务器和 DNS 模式", + "bootstrapDnsDescription": "配置引导 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": "如果启用加密,AdGuard Home 管理界面将通过 HTTPS 运行,并且 DNS 服务器将监听 DNS-over-HTTPS 和 DNS-over-TLS 请求", + "serverConfiguration": "服务器配置", + "domainName": "域名", + "domainNameDescription": "如果设置,AdGuard Home 将检测 ClientID、响应 DDR 查询并执行其他连接验证 如果未设置,这些功能将被禁用 必须与证书中的 DNS 名称之一匹配", + "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 ", + "updateNow": "立即更新", + "currentVersion": "当前版本", + "requestStartUpdateFailed": "请求启动更新失败", + "requestStartUpdateSuccessful": "请求启动更新成功", + "serverUpdated": "AdGuard Home 已是最新版", + "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": "您的服务器版本 {version} 不在支持范围,配合使用可能会存在问题\n\nAdGuard Home Manager 只适配了 AdGuard Home 服务器的稳定版本 alpha 和 beta 版本也许能用,但不保证兼容性,同时使用时可能会存在问题", + "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": "1月", + "february": "2月", + "march": "3月", + "april": "4月", + "may": "5月", + "june": "6月", + "july": "7月", + "august": "8月", + "september": "9月", + "october": "10月", + "november": "11月", + "december": "12月", + "malwarePhishing": "恶意/钓鱼网站", + "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": "主要上游", + "averageUpstreamResponseTime": "平均上游响应时间", + "dhcpNotAvailable": "DHCP 服务器不可用 ", + "osServerInstalledIncompatible": "服务器安装的操作系统与此功能不兼容 ", + "resetSettings": "重置设置", + "resetEncryptionSettingsDescription": "您确定要将加密设置重置为默认值吗?", + "resettingConfig": "正在重置配置...", + "configurationResetSuccessfully": "配置已成功重置", + "configurationResetError": "配置无法重置", + "testUpstreamDnsServers": "测试上游 DNS 服务器", + "errorTestUpstreamDns": "测试上游 DNS 服务器时出错 ", + "useCustomIpEdns": "使用 EDNS 的自定义 IP", + "useCustomIpEdnsDescription": "允许使用 EDNS 的自定义 IP", + "sortingOptions": "排序选项", + "fromHighestToLowest": "从高到低", + "fromLowestToHighest": "从低到高", + "queryLogsAndStatistics": "查询日志和统计", + "ignoreClientQueryLog": "在查询日志中忽略此客户端", + "ignoreClientStatistics": "在统计中忽略此客户端", + "savingChanges": "正在保存更改...", + "fallbackDnsServers": "备用 DNS 服务器", + "fallbackDnsServersDescription": "配置备用 DNS 服务器", + "fallbackDnsServersInfo": "当上游 DNS 服务器无响应时使用的备用 DNS 服务器列表 语法与上面的主上游字段相同 ", + "noFallbackDnsAdded": "未添加备用 DNS 服务器 ", + "blockedResponseTtl": "被阻止的响应 TTL", + "blockedResponseTtlDescription": "指定客户端应缓存过滤响应的秒数", + "invalidValue": "无效值", + "noDataChart": "没有数据显示此图表 ", + "noData": "无数据", + "unblockClient": "解除客户端封锁", + "blockingClient": "正在封锁客户端...", + "unblockingClient": "正在解除客户端封锁...", + "upstreamDnsCacheConfiguration": "DNS 上游缓存配置", + "enableDnsCachingClient": "为此客户端启用 DNS 缓存", + "dnsCacheSize": "DNS 缓存大小", + "nameInvalid": "名称是必需的", + "oneIdentifierRequired": "至少需要一个标识符", + "dnsCacheNumber": "DNS 缓存大小必须是一个数字", + "errors": "错误", + "redirectHttpsWarning": "如果您在 AdGuard Home 服务器上启用了 '自动重定向到 HTTPS',则必须选择 HTTPS 连接并使用服务器的 HTTPS 端口 ", + "logsSettingsDescription": "配置查询日志", + "ignoredDomains": "忽略的域名", + "noIgnoredDomainsAdded": "未添加忽略的域名", + "pauseServiceBlocking": "暂停服务阻止", + "newSchedule": "新计划", + "editSchedule": "编辑计划", + "timezone": "时区", + "monday": "星期一", + "tuesday": "星期二", + "wednesday": "星期三", + "thursday": "星期四", + "friday": "星期五", + "saturday": "星期六", + "sunday": "星期日", + "from": "从", + "to": "到", + "selectStartTime": "选择开始时间", + "selectEndTime": "选择结束时间", + "startTimeBeforeEndTime": "开始时间必须在结束时间之前 ", + "noBlockingScheduleThisDevice": "此设备没有阻止计划 ", + "selectTimezone": "选择时区", + "selectClientsFiltersInfo": "选择您想要显示的客户端 如果没有选择任何客户端,将显示所有客户端 ", + "noDataThisSection": "本节没有数据 ", + "statisticsSettings": "统计设置", + "statisticsSettingsDescription": "配置统计数据收集", + "loadingStatisticsSettings": "正在加载统计设置...", + "statisticsSettingsLoadError": "加载统计设置时发生错误 ", + "statisticsConfigUpdated": "统计设置成功更新", + "statisticsConfigNotUpdated": "统计设置无法更新", + "customTimeInHours": "自定义时间(以小时为单位)", + "invalidTime": "无效时间", + "removeDomain": "移除域名", + "addDomain": "添加域名", + "notLess1Hour": "时间不能少于 1 小时", + "rateLimit": "速率限制", + "subnetPrefixLengthIpv4": "IPv4 的子网前缀长度", + "subnetPrefixLengthIpv6": "IPv6 的子网前缀长度", + "rateLimitAllowlist": "速率限制白名单", + "rateLimitAllowlistDescription": "从速率限制中排除的 IP 地址", + "dnsOptions": "DNS 选项", + "editor": "编辑器", + "editCustomRules": "编辑自定义规则", + "savingCustomRules": "正在保存自定义规则...", + "customRulesUpdatedSuccessfully": "自定义规则成功更新", + "customRulesNotUpdated": "自定义规则无法更新", + "reorder": "重新排序", + "showHide": "显示/隐藏", + "noElementsReorderMessage": "在显示/隐藏标签页上启用一些元素,然后在这里重新排序 ", + "enablePlainDns": "启用普通 DNS", + "enablePlainDnsDescription": "默认启用普通 DNS 您可以禁用它,强制所有设备使用加密 DNS 要做到这一点,您必须至少启用一个加密 DNS 协议 ", + "date": "日期", + "loadingChangelog": "正在加载更新日志...", + "invalidIpOrUrl": "无效的 IP 地址或 URL", + "addPersistentClient": "添加为持久客户端", + "blockThisClientOnly": "仅为此客户端封锁", + "unblockThisClientOnly": "仅为此客户端解封", + "domainBlockedThisClient": "{domain} 已为此客户端封锁", + "domainUnblockedThisClient": "{domain} 已为此客户端解封", + "disallowThisClient": "禁止此客户端", + "allowThisClient": "允许此客户端", + "clientAllowedSuccessfully": "客户端成功允许", + "clientDisallowedSuccessfully": "客户端成功禁止", + "changesNotSaved": "更改无法保存", + "allowingClient": "正在允许客户端...", + "disallowingClient": "正在禁止客户端..." +} diff --git a/lib/l10n/app_zh_CN.arb b/lib/l10n/app_zh_CN.arb new file mode 100644 index 0000000..a789942 --- /dev/null +++ b/lib/l10n/app_zh_CN.arb @@ -0,0 +1,793 @@ +{ + "home": "主页", + "settings": "设置", + "connect": "连接", + "servers": "服务器", + "createConnection": "创建连接", + "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": "安全浏览", + "parentalFiltering": "家长过滤", + "safeSearch": "安全搜索", + "serverStatusNotRefreshed": "无法更新服务器状态", + "loadingStatus": "加载服务器状态...", + "errorLoadServerStatus": "无法加载服务器状态", + "topQueriedDomains": "请求域名排行", + "viewMore": "查看更多", + "topClients": "客户端排行", + "topBlockedDomains": "被拦截域名排行", + "appSettings": "App 设置", + "theme": "主题", + "light": "明亮", + "dark": "暗夜", + "systemDefined": "跟随系统", + "close": "关闭", + "connectedTo": "连接到 :", + "selectedServer": "选择的服务器 :", + "noServerSelected": "未选择服务器", + "manageServer": "管理服务器", + "allProtections": "所有保护", + "userNotEmpty": "用户名不能为空", + "passwordNotEmpty": "密码不能为空", + "examplePath": "例如: /adguard", + "helperPath": "如果使用反向代理,可填写路径", + "aboutApp": "关于本应用", + "appVersion": "App 版本", + "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": "被过滤器拦截", + "malwarePhishingBlocked": "被拦截的恶意/钓鱼网站", + "blockedAdultWebsites": "被拦截的成人网站", + "generalSettings": "常规设置", + "generalSettingsDescription": "各种不同的设置", + "hideZeroValues": "隐藏零值", + "hideZeroValuesDescription": "在主屏幕上隐藏零值块", + "webAdminPanel": "Web 管理面板", + "visitGooglePlay": "访问 Google Play 页面", + "gitHub": "开源代码可在 GitHub 上获得", + "blockClient": "拦截客户端", + "selectTags": "选择标签", + "noTagsSelected": "未选择标签", + "tags": "标签", + "identifiers": "标识符", + "identifier": "标识符", + "identifierHelper": "IP 地址、CIDR、MAC 地址或客户端 ID", + "noIdentifiers": "未添加标识符", + "useGlobalSettings": "使用全局设置", + "enableFiltering": "启用过滤", + "enableSafeBrowsing": "启用安全浏览", + "enableParentalControl": "启用家长控制", + "enableSafeSearch": "启用安全搜索", + "blockedServices": "被拦截的服务", + "selectBlockedServices": "选择要拦截的服务", + "noBlockedServicesSelected": "未选择被拦截的服务", + "services": "服务", + "servicesBlocked": "被拦截的服务", + "tagsSelected": "已选择标签", + "upstreamServers": "上游服务器", + "serverAddress": "服务器地址", + "noUpstreamServers": "无上游服务器", + "willBeUsedGeneralServers": "将使用常规上游服务器", + "added": "已添加", + "clientUpdatedSuccessfully": "客户端更新成功", + "clientNotUpdated": "无法更新客户端", + "clientDeletedSuccessfully": "客户端删除成功", + "clientNotDeleted": "无法删除客户端", + "options": "选项", + "loadingFilters": "正在加载过滤器...", + "filtersNotLoaded": "无法加载过滤器", + "whitelists": "白名单", + "blacklists": "黑名单", + "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 查询,这些查询甚至不会出现在查询日志中", + "addClientFieldDescription": "CIDR、IP 地址或客户端 ID", + "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": "禁用过滤", + "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": "选择接口", + "hardwareAddress": "硬件地址", + "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": "重置租用", + "restoreLeasesMessage": "您确定要继续吗?这将重置所有现有租用 此操作无法撤消", + "restoringLeases": "正在重置租用...", + "leasesRestored": "租用重置成功", + "leasesNotRestored": "无法重置租用", + "dhcpLeases": "DHCP 租用", + "noLeases": "无可用的 DHCP 租用", + "dnsRewrites": "DNS 重写", + "dnsRewritesDescription": "配置自定义 DNS 规则", + "loadingRewriteRules": "正在加载重写规则...", + "rewriteRulesNotLoaded": "无法加载 DNS 重写规则", + "noRewriteRules": "无 DNS 重写规则", + "answer": "响应", + "deleteDnsRewrite": "删除 DNS 重写", + "deleteDnsRewriteMessage": "您确定要删除此 DNS 重写吗?此操作无法撤消", + "dnsRewriteRuleDeleted": "DNS 重写规则删除成功", + "dnsRewriteRuleNotDeleted": "无法删除 DNS 重写规则", + "addDnsRewrite": "添加 DNS 重写", + "addingRewrite": "正在添加重写...", + "dnsRewriteRuleAdded": "DNS 重写规则添加成功", + "dnsRewriteRuleNotAdded": "无法添加 DNS 重写规则", + "logsSettings": "日志设置", + "enableLog": "启用日志", + "clearLogs": "清除日志", + "anonymizeClientIp": "匿名化客户端 IP", + "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": "上游 DNS 服务器", + "bootstrapDns": "引导 DNS 服务器", + "noUpstreamDns": "未添加上游 DNS 服务器 ", + "dnsMode": "DNS 模式", + "noDnsMode": "未选择 DNS 模式", + "loadBalancing": "负载均衡", + "parallelRequests": "并行请求", + "fastestIpAddress": "最快的 IP 地址", + "loadBalancingDescription": "每次查询一个上游服务器 AdGuard Home 使用其加权随机算法选择服务器,以便更频繁地使用最快的服务器", + "parallelRequestsDescription": "使用并行查询同时加速解析,同时查询所有上游服务器", + "fastestIpAddressDescription": "查询所有 DNS 服务器并返回所有响应中最快的 IP 地址 这会减慢 DNS 查询,因为 AdGuard Home 必须等待所有 DNS 服务器的响应,但可以改善整体连接性", + "noBootstrapDns": "未添加引导 DNS 服务器 ", + "bootstrapDnsServersInfo": "引导 DNS 服务器用于解析您指定的上游 DoH/DoT 解析器的 IP 地址 ", + "privateReverseDnsServers": "私有反向 DNS 服务器", + "privateReverseDnsServersDescription": "AdGuard Home 用于本地 PTR 查询的 DNS 服务器 这些服务器用于解析私有 IP 范围内的地址的 PTR 请求,例如 \"192.168.12.34\" 如果未设置,AdGuard Home 将使用操作系统的默认 DNS 解析器地址,但不包括 AdGuard Home 本身的地址 ", + "reverseDnsDefault": "默认情况下,AdGuard Home 使用以下默认反向 DNS 解析器", + "addItem": "添加项目", + "noServerAddressesAdded": "未添加服务器地址", + "usePrivateReverseDnsResolvers": "使用私有反向 DNS 解析器", + "usePrivateReverseDnsResolversDescription": "使用这些上游服务器执行本地提供的地址的反向 DNS 查询 如果禁用,AdGuard Home 会对所有此类 PTR 请求(除了来自 DHCP、/etc/hosts 等已知客户端)响应 NXDOMAIN", + "enableReverseResolving": "启用客户端 IP 地址的反向解析", + "enableReverseResolvingDescription": "通过向相应的解析器发送 PTR 查询,将客户端 IP 地址进行反向解析为主机名(对于本地客户端使用私有 DNS 服务器,对于具有公共 IP 地址的客户端使用上游服务器)", + "dnsServerSettings": "AdGuard Home DNS 服务器设置", + "limitRequestsSecond": "每秒速率限制", + "valueNotNumber": "值不是数字", + "enableEdns": "启用 EDNS 客户子网", + "enableEdnsDescription": "在上游请求中添加 EDNS 客户子网选项(ECS),并在查询日志中记录客户端发送的值", + "enableDnssec": "启用 DNSSEC", + "enableDnssecDescription": "在传出的 DNS 查询中设置 DNSSEC 标志并检查结果(需要启用 DNSSEC 的解析器)", + "disableResolvingIpv6": "禁用 IPv6 地址解析", + "disableResolvingIpv6Description": "丢弃所有用于 IPv6 地址(AAAA 记录)的 DNS 查询", + "blockingMode": "拦截模式", + "defaultMode": "默认", + "defaultDescription": "当按照 Adblock 样式规则被拦截时,返回零 IP 地址(0.0.0.0 对应 A;:: 对应 AAAA);当按照 /etc/hosts 样式规则被拦截时,返回规则中指定的 IP 地址", + "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": "在缓存 DNS 响应时,从上游服务器接收到的最短的存活时间值(秒)", + "overrideMaximumTtl": "覆盖最大 TTL", + "overrideMaximumTtlDescription": "为 DNS 缓存条目设置最大存活时间值(秒)", + "optimisticCaching": "乐观缓存", + "optimisticCachingDescription": "即使条目已过期,AdGuard Home 也将从缓存中响应,并尝试刷新它们", + "loadingDnsConfig": "正在加载 DNS 配置...", + "dnsConfigNotLoaded": "无法加载 DNS 配置", + "blockingIpv4": "拦截 IPv4", + "blockingIpv4Description": "拦截包含 A 记录的请求时返回的 IP 地址", + "blockingIpv6": "拦截 IPv6", + "blockingIpv6Description": "拦截包含 AAAA 记录的请求时返回的 IP 地址", + "invalidIp": "无效的 IP 地址", + "dnsConfigSaved": "DNS 服务器配置保存成功", + "dnsConfigNotSaved": "DNS 服务器配置无法保存", + "savingConfig": "正在保存配置...", + "someValueNotValid": "某些值无效", + "upstreamDnsDescription": "配置上游服务器和 DNS 模式", + "bootstrapDnsDescription": "配置引导 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": "如果启用加密,AdGuard Home 管理界面将通过 HTTPS 运行,并且 DNS 服务器将监听 DNS-over-HTTPS 和 DNS-over-TLS 请求", + "serverConfiguration": "服务器配置", + "domainName": "域名", + "domainNameDescription": "如果设置,AdGuard Home 将检测 ClientID、响应 DDR 查询并执行其他连接验证 如果未设置,这些功能将被禁用 必须与证书中的 DNS 名称之一匹配", + "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 ", + "updateNow": "立即更新", + "currentVersion": "当前版本", + "requestStartUpdateFailed": "请求启动更新失败", + "requestStartUpdateSuccessful": "请求启动更新成功", + "serverUpdated": "AdGuard Home 已是最新版", + "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": "您的服务器版本 {version} 不在支持范围,配合使用可能会存在问题\n\nAdGuard Home Manager 只适配了 AdGuard Home 服务器的稳定版本 alpha 和 beta 版本也许能用,但不保证兼容性,同时使用时可能会存在问题", + "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": "1月", + "february": "2月", + "march": "3月", + "april": "4月", + "may": "5月", + "june": "6月", + "july": "7月", + "august": "8月", + "september": "9月", + "october": "10月", + "november": "11月", + "december": "12月", + "malwarePhishing": "恶意/钓鱼网站", + "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": "主要上游", + "averageUpstreamResponseTime": "平均上游响应时间", + "dhcpNotAvailable": "DHCP 服务器不可用 ", + "osServerInstalledIncompatible": "服务器安装的操作系统与此功能不兼容 ", + "resetSettings": "重置设置", + "resetEncryptionSettingsDescription": "您确定要将加密设置重置为默认值吗?", + "resettingConfig": "正在重置配置...", + "configurationResetSuccessfully": "配置已成功重置", + "configurationResetError": "配置无法重置", + "testUpstreamDnsServers": "测试上游 DNS 服务器", + "errorTestUpstreamDns": "测试上游 DNS 服务器时出错 ", + "useCustomIpEdns": "使用 EDNS 的自定义 IP", + "useCustomIpEdnsDescription": "允许使用 EDNS 的自定义 IP", + "sortingOptions": "排序选项", + "fromHighestToLowest": "从高到低", + "fromLowestToHighest": "从低到高", + "queryLogsAndStatistics": "查询日志和统计", + "ignoreClientQueryLog": "在查询日志中忽略此客户端", + "ignoreClientStatistics": "在统计中忽略此客户端", + "savingChanges": "正在保存更改...", + "fallbackDnsServers": "备用 DNS 服务器", + "fallbackDnsServersDescription": "配置备用 DNS 服务器", + "fallbackDnsServersInfo": "当上游 DNS 服务器无响应时使用的备用 DNS 服务器列表 语法与上面的主上游字段相同 ", + "noFallbackDnsAdded": "未添加备用 DNS 服务器 ", + "blockedResponseTtl": "被阻止的响应 TTL", + "blockedResponseTtlDescription": "指定客户端应缓存过滤响应的秒数", + "invalidValue": "无效值", + "noDataChart": "没有数据显示此图表 ", + "noData": "无数据", + "unblockClient": "解除客户端封锁", + "blockingClient": "正在封锁客户端...", + "unblockingClient": "正在解除客户端封锁...", + "upstreamDnsCacheConfiguration": "DNS 上游缓存配置", + "enableDnsCachingClient": "为此客户端启用 DNS 缓存", + "dnsCacheSize": "DNS 缓存大小", + "nameInvalid": "名称是必需的", + "oneIdentifierRequired": "至少需要一个标识符", + "dnsCacheNumber": "DNS 缓存大小必须是一个数字", + "errors": "错误", + "redirectHttpsWarning": "如果您在 AdGuard Home 服务器上启用了 '自动重定向到 HTTPS',则必须选择 HTTPS 连接并使用服务器的 HTTPS 端口 ", + "logsSettingsDescription": "配置查询日志", + "ignoredDomains": "忽略的域名", + "noIgnoredDomainsAdded": "未添加忽略的域名", + "pauseServiceBlocking": "暂停服务阻止", + "newSchedule": "新计划", + "editSchedule": "编辑计划", + "timezone": "时区", + "monday": "星期一", + "tuesday": "星期二", + "wednesday": "星期三", + "thursday": "星期四", + "friday": "星期五", + "saturday": "星期六", + "sunday": "星期日", + "from": "从", + "to": "到", + "selectStartTime": "选择开始时间", + "selectEndTime": "选择结束时间", + "startTimeBeforeEndTime": "开始时间必须在结束时间之前 ", + "noBlockingScheduleThisDevice": "此设备没有阻止计划 ", + "selectTimezone": "选择时区", + "selectClientsFiltersInfo": "选择您想要显示的客户端 如果没有选择任何客户端,将显示所有客户端 ", + "noDataThisSection": "本节没有数据 ", + "statisticsSettings": "统计设置", + "statisticsSettingsDescription": "配置统计数据收集", + "loadingStatisticsSettings": "正在加载统计设置...", + "statisticsSettingsLoadError": "加载统计设置时发生错误 ", + "statisticsConfigUpdated": "统计设置成功更新", + "statisticsConfigNotUpdated": "统计设置无法更新", + "customTimeInHours": "自定义时间(以小时为单位)", + "invalidTime": "无效时间", + "removeDomain": "移除域名", + "addDomain": "添加域名", + "notLess1Hour": "时间不能少于 1 小时", + "rateLimit": "速率限制", + "subnetPrefixLengthIpv4": "IPv4 的子网前缀长度", + "subnetPrefixLengthIpv6": "IPv6 的子网前缀长度", + "rateLimitAllowlist": "速率限制白名单", + "rateLimitAllowlistDescription": "从速率限制中排除的 IP 地址", + "dnsOptions": "DNS 选项", + "editor": "编辑器", + "editCustomRules": "编辑自定义规则", + "savingCustomRules": "正在保存自定义规则...", + "customRulesUpdatedSuccessfully": "自定义规则成功更新", + "customRulesNotUpdated": "自定义规则无法更新", + "reorder": "重新排序", + "showHide": "显示/隐藏", + "noElementsReorderMessage": "在显示/隐藏标签页上启用一些元素,然后在这里重新排序 ", + "enablePlainDns": "启用普通 DNS", + "enablePlainDnsDescription": "默认启用普通 DNS 您可以禁用它,强制所有设备使用加密 DNS 要做到这一点,您必须至少启用一个加密 DNS 协议 ", + "date": "日期", + "loadingChangelog": "正在加载更新日志...", + "invalidIpOrUrl": "无效的 IP 地址或 URL", + "addPersistentClient": "添加为持久客户端", + "blockThisClientOnly": "仅为此客户端封锁", + "unblockThisClientOnly": "仅为此客户端解封", + "domainBlockedThisClient": "{domain} 已为此客户端封锁", + "domainUnblockedThisClient": "{domain} 已为此客户端解封", + "disallowThisClient": "禁止此客户端", + "allowThisClient": "允许此客户端", + "clientAllowedSuccessfully": "客户端成功允许", + "clientDisallowedSuccessfully": "客户端成功禁止", + "changesNotSaved": "更改无法保存", + "allowingClient": "正在允许客户端...", + "disallowingClient": "正在禁止客户端..." +} diff --git a/lib/main.dart b/lib/main.dart index 5ef8610..6402b70 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,35 +1,67 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_displaymode/flutter_displaymode.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:provider/provider.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; +import 'package:window_manager/window_manager.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:adguard_home_manager/base.dart'; +import 'package:adguard_home_manager/widgets/layout.dart'; +import 'package:adguard_home_manager/widgets/menu_bar.dart'; -import 'package:adguard_home_manager/classes/http_override.dart'; -import 'package:adguard_home_manager/services/database.dart'; -import 'package:adguard_home_manager/constants/colors.dart'; import 'package:adguard_home_manager/providers/logs_provider.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; +import 'package:adguard_home_manager/providers/clients_provider.dart'; +import 'package:adguard_home_manager/providers/dns_provider.dart'; +import 'package:adguard_home_manager/providers/filtering_provider.dart'; +import 'package:adguard_home_manager/providers/rewrite_rules_provider.dart'; +import 'package:adguard_home_manager/providers/dhcp_provider.dart'; +import 'package:adguard_home_manager/providers/status_provider.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart'; +import 'package:adguard_home_manager/constants/colors.dart'; +import 'package:adguard_home_manager/config/globals.dart'; import 'package:adguard_home_manager/config/theme.dart'; - +import 'package:adguard_home_manager/services/db/database.dart'; +import 'package:adguard_home_manager/classes/http_override.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - SystemChrome.setPreferredOrientations( - [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown] - ); + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + + if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { + await windowManager.ensureInitialized(); + WindowManager.instance.setMinimumSize(const Size(500, 700)); + } + + if (Platform.isWindows || Platform.isLinux) { + sqfliteFfiInit(); + databaseFactory = databaseFactoryFfi; + } + + await dotenv.load(fileName: '.env'); + + final sharedPreferences = await SharedPreferences.getInstance(); - AppConfigProvider appConfigProvider = AppConfigProvider(); - ServersProvider serversProvider = ServersProvider(); - LogsProvider logsProvider = LogsProvider(); + final AppConfigProvider appConfigProvider = AppConfigProvider( + sharedPreferencesInstance: sharedPreferences + ); + final ServersProvider serversProvider = ServersProvider(); + final StatusProvider statusProvider = StatusProvider(); + final ClientsProvider clientsProvider = ClientsProvider(); + final FilteringProvider filtersProvider = FilteringProvider(); + final DhcpProvider dhcpProvider = DhcpProvider(); + final RewriteRulesProvider rewriteRulesProvider = RewriteRulesProvider(); + final DnsProvider dnsProvider = DnsProvider(); + final LogsProvider logsProvider = LogsProvider(); DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); if (Platform.isAndroid) { @@ -41,20 +73,20 @@ void main() async { appConfigProvider.setIosInfo(iosInfo); } - final dbData = await loadDb(appConfigProvider.androidDeviceInfo != null && appConfigProvider.androidDeviceInfo!.version.sdkInt! >= 31); - - if (dbData['appConfig']['overrideSslCheck'] == 1) { + if (sharedPreferences.getBool('overrideSslCheck') == true) { HttpOverrides.global = MyHttpOverrides(); } + final dbData = await loadDb(); serversProvider.setDbInstance(dbData['dbInstance']); - appConfigProvider.saveFromDb(dbData['dbInstance'], dbData['appConfig']); serversProvider.saveFromDb(dbData['servers']); + appConfigProvider.saveFromSharedPreferences(); + PackageInfo appInfo = await PackageInfo.fromPlatform(); appConfigProvider.setAppInfo(appInfo); - runApp( + void startApp() => runApp( MultiProvider( providers: [ ChangeNotifierProvider( @@ -63,83 +95,154 @@ void main() async { ChangeNotifierProvider( create: ((context) => appConfigProvider) ), + ChangeNotifierProvider( + create: ((context) => statusProvider) + ), + ChangeNotifierProvider( + create: ((context) => clientsProvider) + ), ChangeNotifierProvider( create: ((context) => logsProvider) ), + ChangeNotifierProvider( + create: ((context) => filtersProvider) + ), + ChangeNotifierProvider( + create: ((context) => dhcpProvider) + ), + ChangeNotifierProvider( + create: ((context) => rewriteRulesProvider) + ), + ChangeNotifierProvider( + create: ((context) => dnsProvider) + ), + ChangeNotifierProxyProvider2( + create: (context) => clientsProvider, + update: (context, servers, status, clients) => clients!..update(servers, status), + ), + ChangeNotifierProxyProvider2( + create: (context) => filtersProvider, + update: (context, servers, status, filtering) => filtering!..update(servers, status), + ), + ChangeNotifierProxyProvider( + create: (context) => statusProvider, + update: (context, servers, status) => status!..update(servers), + ), + ChangeNotifierProxyProvider( + create: (context) => logsProvider, + update: (context, servers, logs) => logs!..update(servers), + ), + ChangeNotifierProxyProvider( + create: (context) => dhcpProvider, + update: (context, servers, dhcp) => dhcp!..update(servers), + ), + ChangeNotifierProxyProvider( + create: (context) => rewriteRulesProvider, + update: (context, servers, rewrite) => rewrite!..update(servers), + ), + ChangeNotifierProxyProvider( + create: (context) => dnsProvider, + update: (context, servers, dns) => dns!..update(servers), + ), ], child: const Main(), ) ); + + if ( + ( + kReleaseMode && + (dotenv.env['SENTRY_DSN'] != null && dotenv.env['SENTRY_DSN'] != "") + ) || ( + dotenv.env['ENABLE_SENTRY'] == "true" && + (dotenv.env['SENTRY_DSN'] != null && dotenv.env['SENTRY_DSN'] != "") + ) + ) { + SentryFlutter.init( + (options) { + options.dsn = dotenv.env['SENTRY_DSN']; + options.sendDefaultPii = false; + options.beforeSend = (event, hint) { + if (event.throwable is HttpException) { + return null; + } + + if (event.message?.formatted.contains("HttpException") == true) { + return null; + } + + if ( + event.message?.formatted.contains("Unexpected character") ?? false || + (event.throwable != null && event.throwable!.toString().contains("Unexpected character")) + ) { + return null; + } + + if ( + event.message?.formatted.contains("Unexpected end of input") ?? false || + (event.throwable != null && event.throwable!.toString().contains("Unexpected end of input")) + ) { + return null; + } + + return event; + }; + }, + appRunner: () => startApp() + ); + } + else { + startApp(); + } } -class Main extends StatefulWidget { +class Main extends StatelessWidget { const Main({super.key}); - @override - State
createState() => _MainState(); -} - -class _MainState extends State
{ - List modes = []; - DisplayMode? active; - DisplayMode? preferred; - - Future displayMode() async { - try { - modes = await FlutterDisplayMode.supported; - preferred = await FlutterDisplayMode.preferred; - active = await FlutterDisplayMode.active; - await FlutterDisplayMode.setHighRefreshRate(); - setState(() {}); - } catch (_) { - // ---- // - } - } - - @override - void initState() { - displayMode(); - super.initState(); - } - @override Widget build(BuildContext context) { final appConfigProvider = Provider.of(context); return DynamicColorBuilder( - builder: (lightDynamic, darkDynamic) => MaterialApp( - title: 'AdGuard Home Manager', - theme: appConfigProvider.androidDeviceInfo != null && appConfigProvider.androidDeviceInfo!.version.sdkInt! >= 31 + builder: (lightDynamic, darkDynamic) { + appConfigProvider.setSupportsDynamicTheme(lightDynamic != null && darkDynamic != null); + return MaterialApp( + title: 'AdGuard Home Manager', + theme: lightDynamic != null ? appConfigProvider.useDynamicColor == true ? lightTheme(lightDynamic) : lightThemeOldVersions(colors[appConfigProvider.staticColor]) : lightThemeOldVersions(colors[appConfigProvider.staticColor]), - darkTheme: appConfigProvider.androidDeviceInfo != null && appConfigProvider.androidDeviceInfo!.version.sdkInt! >= 31 + darkTheme: darkDynamic != null ? appConfigProvider.useDynamicColor == true ? darkTheme(darkDynamic) : darkThemeOldVersions(colors[appConfigProvider.staticColor]) : darkThemeOldVersions(colors[appConfigProvider.staticColor]), - themeMode: appConfigProvider.selectedTheme, - debugShowCheckedModeBanner: false, - localizationsDelegates: const [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - AppLocalizations.delegate, - ], - supportedLocales: const [ - Locale('en', ''), - Locale('es', '') - ], - builder: (context, child) { - return MediaQuery( - data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0), + themeMode: appConfigProvider.selectedTheme, + debugShowCheckedModeBanner: false, + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + AppLocalizations.delegate, + ], + supportedLocales: const [ + Locale('en', ''), + Locale('es', ''), + Locale('zh', ''), + Locale('zh', 'CN'), + Locale('pl', ''), + Locale('tr', ''), + Locale('ru', '') + ], + scaffoldMessengerKey: scaffoldMessengerKey, + navigatorKey: globalNavigatorKey, + builder: (context, child) => CustomMenuBar( child: child!, - ); - }, - home: Base(appConfigProvider: appConfigProvider), - ), + ), + home: const Layout(), + ); + } ); } -} - +} \ No newline at end of file diff --git a/lib/models/app_screen.dart b/lib/models/app_screen.dart index bce5cc3..4412022 100644 --- a/lib/models/app_screen.dart +++ b/lib/models/app_screen.dart @@ -4,14 +4,14 @@ class AppScreen { final String name; final IconData icon; final PreferredSizeWidget? appBar; - final Widget body; final Widget? fab; + final Widget child; const AppScreen({ required this.name, required this.icon, this.appBar, - required this.body, - this.fab + this.fab, + required this.child, }); } \ No newline at end of file diff --git a/lib/models/applied_filters.dart b/lib/models/applied_filters.dart index b281b42..f4ef1ee 100644 --- a/lib/models/applied_filters.dart +++ b/lib/models/applied_filters.dart @@ -1,11 +1,11 @@ class AppliedFiters { String selectedResultStatus = 'all'; - String? domainText; - List? clients; + String? searchText; + List clients; AppliedFiters({ required this.selectedResultStatus, - required this.domainText, + required this.searchText, required this.clients }); } \ No newline at end of file diff --git a/lib/models/blocked_services.dart b/lib/models/blocked_services.dart index cb5c78a..659bebc 100644 --- a/lib/models/blocked_services.dart +++ b/lib/models/blocked_services.dart @@ -11,12 +11,10 @@ class BlockedServicesFromApi { } class BlockedServices { - int loadStatus = 0; - List? services; + List services; BlockedServices({ - this.loadStatus = 0, - this.services + required this.services }); } diff --git a/lib/models/clients.dart b/lib/models/clients.dart index 2be193c..9527804 100644 --- a/lib/models/clients.dart +++ b/lib/models/clients.dart @@ -1,43 +1,28 @@ -import 'dart:convert'; - import 'package:adguard_home_manager/models/clients_allowed_blocked.dart'; +import 'package:adguard_home_manager/models/safe_search.dart'; class Clients { - int loadStatus; - ClientsData? data; - - Clients({ - required this.loadStatus, - this.data - }); -} - -ClientsData clientsFromJson(String str) => ClientsData.fromJson(json.decode(str)); - -String clientsToJson(ClientsData data) => json.encode(data.toJson()); - -class ClientsData { List clients; - final List autoClientsData; + final List autoClients; final List supportedTags; ClientsAllowedBlocked? clientsAllowedBlocked; - ClientsData({ + Clients({ required this.clients, - required this.autoClientsData, + required this.autoClients, required this.supportedTags, this.clientsAllowedBlocked }); - factory ClientsData.fromJson(Map json) => ClientsData( + factory Clients.fromJson(Map json) => Clients( clients: json["clients"] != null ? List.from(json["clients"].map((x) => Client.fromJson(x))) : [], - autoClientsData: json["auto_clients"] != null ? List.from(json["auto_clients"].map((x) => AutoClient.fromJson(x))) : [], + autoClients: json["auto_clients"] != null ? List.from(json["auto_clients"].map((x) => AutoClient.fromJson(x))) : [], supportedTags: json["supported_tags"] != null ? List.from(json["supported_tags"].map((x) => x)) : [], ); Map toJson() => { "clients": List.from(clients.map((x) => x.toJson())), - "auto_clients": List.from(autoClientsData.map((x) => x.toJson())), + "auto_clients": List.from(autoClients.map((x) => x.toJson())), "supported_tags": List.from(supportedTags.map((x) => x)), }; } @@ -99,9 +84,14 @@ class Client { final bool filteringEnabled; final bool parentalEnabled; final bool safebrowsingEnabled; - final bool safesearchEnabled; final bool useGlobalBlockedServices; final bool useGlobalSettings; + final SafeSearch? safeSearch; + final bool? ignoreQuerylog; + final bool? ignoreStatistics; + final bool? upstreamsCacheEnabled; + final int? upstreamsCacheSize; + final BlockedServicesSchedule? blockedServicesSchedule; Client({ required this.name, @@ -112,9 +102,14 @@ class Client { required this.filteringEnabled, required this.parentalEnabled, required this.safebrowsingEnabled, - required this.safesearchEnabled, required this.useGlobalBlockedServices, required this.useGlobalSettings, + required this.safeSearch, + required this.ignoreQuerylog, + required this.ignoreStatistics, + required this.upstreamsCacheEnabled, + required this.upstreamsCacheSize, + required this.blockedServicesSchedule, }); factory Client.fromJson(Map json) => Client( @@ -126,9 +121,18 @@ class Client { filteringEnabled: json["filtering_enabled"], parentalEnabled: json["parental_enabled"], safebrowsingEnabled: json["safebrowsing_enabled"], - safesearchEnabled: json["safesearch_enabled"], useGlobalBlockedServices: json["use_global_blocked_services"], useGlobalSettings: json["use_global_settings"], + safeSearch: json["safe_search"] != null + ? SafeSearch.fromJson(json["safe_search"]) + : null, + ignoreQuerylog: json["ignore_querylog"], + ignoreStatistics: json["ignore_statistics"], + upstreamsCacheEnabled: json["upstreams_cache_enabled"], + upstreamsCacheSize: json["upstreams_cache_size"], + blockedServicesSchedule: json["blocked_services_schedule"] != null + ? BlockedServicesSchedule.fromJson(json["blocked_services_schedule"]) + : null ); Map toJson() => { @@ -140,8 +144,77 @@ class Client { "filtering_enabled": filteringEnabled, "parental_enabled": parentalEnabled, "safebrowsing_enabled": safebrowsingEnabled, - "safesearch_enabled": safesearchEnabled, + "safe_search": safeSearch, "use_global_blocked_services": useGlobalBlockedServices, "use_global_settings": useGlobalSettings, + "ignore_querylog": ignoreQuerylog, + "ignore_statistics": ignoreStatistics, + "upstreams_cache_enabled": upstreamsCacheEnabled, + "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 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 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 json) => BlockedServicesScheduleDay( + start: json["start"], + end: json["end"], + ); + + Map toJson() => { + "start": start, + "end": end, }; } \ No newline at end of file diff --git a/lib/models/dhcp.dart b/lib/models/dhcp.dart index 4910e68..7953f47 100644 --- a/lib/models/dhcp.dart +++ b/lib/models/dhcp.dart @@ -1,20 +1,9 @@ import 'dart:convert'; - class DhcpModel { - int loadStatus = 0; - DhcpData? data; + List networkInterfaces; + DhcpStatus? dhcpStatus; DhcpModel({ - required this.loadStatus, - this.data, - }); -} - -class DhcpData { - List networkInterfaces; - DhcpStatus dhcpStatus; - - DhcpData({ required this.networkInterfaces, required this.dhcpStatus, }); @@ -28,7 +17,7 @@ class NetworkInterface { String name; String hardwareAddress; List flags; - String gatewayIp; + String? gatewayIp; List ipv4Addresses; List ipv6Addresses; @@ -65,9 +54,9 @@ DhcpStatus dhcpStatusFromJson(String str) => DhcpStatus.fromJson(json.decode(str String dhcpStatusToJson(DhcpStatus data) => json.encode(data.toJson()); class DhcpStatus { - String interfaceName; - IpVersion v4; - IpVersion v6; + String? interfaceName; + IpVersion? v4; + IpVersion? v6; List leases; List staticLeases; bool enabled; @@ -83,17 +72,17 @@ class DhcpStatus { factory DhcpStatus.fromJson(Map json) => DhcpStatus( interfaceName: json["interface_name"], - v4: IpVersion.fromJson(json["v4"]), - v6: IpVersion.fromJson(json["v6"]), + v4: json["v4"] != null ? IpVersion.fromJson(json["v4"]) : null, + v6: json["v6"] != null ? IpVersion.fromJson(json["v6"]) : null, leases: List.from(json["leases"].map((x) => Lease.fromJson(x))), staticLeases: List.from(json["static_leases"].map((x) => Lease.fromJson(x))), - enabled: json["enabled"], + enabled: json["enabled"] ?? false, ); Map toJson() => { "interface_name": interfaceName, - "v4": v4.toJson(), - "v6": v6.toJson(), + "v4": v4?.toJson(), + "v6": v6?.toJson(), "leases": List.from(leases.map((x) => x)), "static_leases": List.from(staticLeases.map((x) => x)), "enabled": enabled, diff --git a/lib/models/dns_info.dart b/lib/models/dns_info.dart index de2e308..7007644 100644 --- a/lib/models/dns_info.dart +++ b/lib/models/dns_info.dart @@ -1,49 +1,44 @@ -import 'dart:convert'; - class DnsInfo { - int loadStatus = 0; - DnsInfoData? data; - - DnsInfo({ - required this.loadStatus, - this.data - }); -} - -DnsInfoData dnsInfoDataFromJson(String str) => DnsInfoData.fromJson(json.decode(str)); - -String dnsInfoDataToJson(DnsInfoData data) => json.encode(data.toJson()); - -class DnsInfoData { List upstreamDns; - String upstreamDnsFile; + String? upstreamDnsFile; List bootstrapDns; + List? fallbackDns; bool protectionEnabled; int ratelimit; String blockingMode; bool ednsCsEnabled; + bool? ednsCsUseCustom; + String? ednsCsCustomIp; bool dnssecEnabled; bool disableIpv6; - String upstreamMode; - int cacheSize; - int cacheTtlMin; - int cacheTtlMax; - bool cacheOptimistic; - bool resolveClients; - bool usePrivatePtrResolvers; + String? upstreamMode; + int? cacheSize; + int? cacheTtlMin; + int? cacheTtlMax; + bool? cacheOptimistic; + bool? resolveClients; + bool? usePrivatePtrResolvers; List localPtrUpstreams; String blockingIpv4; String blockingIpv6; - List defaultLocalPtrUpstreams; - DnsInfoData({ + int? blockedResponseTtl; + int? ratelimitSubnetLenIpv4; + int? ratelimitSubnetLenIpv6; + List? ratelimitWhitelist; + int? upstreamTimeout; + + DnsInfo({ required this.upstreamDns, required this.upstreamDnsFile, required this.bootstrapDns, + required this.fallbackDns, required this.protectionEnabled, required this.ratelimit, required this.blockingMode, required this.ednsCsEnabled, + required this.ednsCsUseCustom, + required this.ednsCsCustomIp, required this.dnssecEnabled, required this.disableIpv6, required this.upstreamMode, @@ -57,16 +52,24 @@ class DnsInfoData { required this.blockingIpv4, required this.blockingIpv6, required this.defaultLocalPtrUpstreams, + required this.blockedResponseTtl, + required this.ratelimitSubnetLenIpv4, + required this.ratelimitSubnetLenIpv6, + required this.ratelimitWhitelist, + required this.upstreamTimeout, }); - factory DnsInfoData.fromJson(Map json) => DnsInfoData( + factory DnsInfo.fromJson(Map json) => DnsInfo( upstreamDns: json["upstream_dns"] != null ? List.from(json["upstream_dns"].map((x) => x)) : [], upstreamDnsFile: json["upstream_dns_file"], - bootstrapDns: List.from(json["bootstrap_dns"].map((x) => x)), + bootstrapDns: json["bootstrap_dns"] != null ? List.from(json["bootstrap_dns"].map((x) => x)) : [], + fallbackDns: json["fallback_dns"] != null ? List.from(json["fallback_dns"].map((x) => x)) : [], protectionEnabled: json["protection_enabled"], ratelimit: json["ratelimit"], blockingMode: json["blocking_mode"], ednsCsEnabled: json["edns_cs_enabled"], + ednsCsUseCustom: json["edns_cs_use_custom"], + ednsCsCustomIp: json["edns_cs_custom_ip"], dnssecEnabled: json["dnssec_enabled"], disableIpv6: json["disable_ipv6"], upstreamMode: json["upstream_mode"], @@ -80,16 +83,24 @@ class DnsInfoData { blockingIpv4: json["blocking_ipv4"], blockingIpv6: json["blocking_ipv6"], defaultLocalPtrUpstreams: json["default_local_ptr_upstreams"] != null ? List.from(json["default_local_ptr_upstreams"].map((x) => x)) : [], + blockedResponseTtl: json["blocked_response_ttl"], + ratelimitSubnetLenIpv4: json["ratelimit_subnet_len_ipv4"], + ratelimitSubnetLenIpv6: json["ratelimit_subnet_len_ipv6"], + ratelimitWhitelist: json["ratelimit_whitelist"] != null ? List.from(json["ratelimit_whitelist"].map((x) => x)) : [], + upstreamTimeout: json["upstream_timeout"], ); Map toJson() => { "upstream_dns": List.from(upstreamDns.map((x) => x)), "upstream_dns_file": upstreamDnsFile, "bootstrap_dns": List.from(bootstrapDns.map((x) => x)), + "fallback_dns": List.from(bootstrapDns.map((x) => x)), "protection_enabled": protectionEnabled, "ratelimit": ratelimit, "blocking_mode": blockingMode, "edns_cs_enabled": ednsCsEnabled, + "edns_cs_use_custom": ednsCsUseCustom, + "edns_cs_custom_ip": ednsCsCustomIp, "dnssec_enabled": dnssecEnabled, "disable_ipv6": disableIpv6, "upstream_mode": upstreamMode, @@ -103,5 +114,10 @@ class DnsInfoData { "blocking_ipv4": blockingIpv4, "blocking_ipv6": blockingIpv6, "default_local_ptr_upstreams": List.from(defaultLocalPtrUpstreams.map((x) => x)), + "blocked_response_ttl": blockedResponseTtl, + "ratelimit_subnet_len_ipv4": ratelimitSubnetLenIpv4, + "ratelimit_subnet_len_ipv6": ratelimitSubnetLenIpv6, + "ratelimit_whitelist": ratelimitWhitelist != null ? List.from(ratelimitWhitelist!.map((x) => x)) : null, + "upstream_timeout": upstreamTimeout, }; } diff --git a/lib/models/dns_statistics.dart b/lib/models/dns_statistics.dart index 45993bd..ed787ce 100644 --- a/lib/models/dns_statistics.dart +++ b/lib/models/dns_statistics.dart @@ -5,10 +5,12 @@ DnsStatistics dnsStatisticsFromJson(String str) => DnsStatistics.fromJson(json.d String dnsStatisticsToJson(DnsStatistics data) => json.encode(data.toJson()); class DnsStatistics { - final String timeUnits; + final String? timeUnits; final List> topQueriedDomains; final List> topClients; final List> topBlockedDomains; + final List>? topUpstreamResponses; + final List>? topUpstreamsAvgTime; final List dnsQueries; final List blockedFiltering; final List replacedSafebrowsing; @@ -25,6 +27,8 @@ class DnsStatistics { required this.topQueriedDomains, required this.topClients, required this.topBlockedDomains, + required this.topUpstreamResponses, + required this.topUpstreamsAvgTime, required this.dnsQueries, required this.blockedFiltering, required this.replacedSafebrowsing, @@ -39,9 +43,11 @@ class DnsStatistics { factory DnsStatistics.fromJson(Map json) => DnsStatistics( timeUnits: json["time_units"], - topQueriedDomains: List>.from(json["top_queried_domains"].map((x) => Map.from(x).map((k, v) => MapEntry(k, v)))), - topClients: List>.from(json["top_clients"].map((x) => Map.from(x).map((k, v) => MapEntry(k, v)))), - topBlockedDomains: List>.from(json["top_blocked_domains"].map((x) => Map.from(x).map((k, v) => MapEntry(k, v)))), + topQueriedDomains: json["top_queried_domains"] != null ? List>.from(json["top_queried_domains"].map((x) => Map.from(x).map((k, v) => MapEntry(k, v)))) : [], + topClients: json["top_clients"] != null ? List>.from(json["top_clients"].map((x) => Map.from(x).map((k, v) => MapEntry(k, v)))) : [], + topBlockedDomains: json["top_blocked_domains"] != null ? List>.from(json["top_blocked_domains"].map((x) => Map.from(x).map((k, v) => MapEntry(k, v)))): [], + topUpstreamResponses: json["top_upstreams_responses"] != null ? List>.from(json["top_upstreams_responses"].map((x) => Map.from(x).map((k, v) => MapEntry(k, v)))) : null, + topUpstreamsAvgTime: json["top_upstreams_avg_time"] != null ? List>.from(json["top_upstreams_avg_time"].map((x) => Map.from(x).map((k, v) => MapEntry(k, v)))) : null, dnsQueries: List.from(json["dns_queries"].map((x) => x)), blockedFiltering: List.from(json["blocked_filtering"].map((x) => x)), replacedSafebrowsing: List.from(json["replaced_safebrowsing"].map((x) => x)), @@ -59,6 +65,8 @@ class DnsStatistics { "top_queried_domains": List.from(topQueriedDomains.map((x) => Map.from(x).map((k, v) => MapEntry(k, v)))), "top_clients": List.from(topClients.map((x) => Map.from(x).map((k, v) => MapEntry(k, v)))), "top_blocked_domains": List.from(topBlockedDomains.map((x) => Map.from(x).map((k, v) => MapEntry(k, v)))), + "top_upstreams_responses": topUpstreamResponses != null ? List.from(topUpstreamResponses!.map((x) => Map.from(x).map((k, v) => MapEntry(k, v)))) : null, + "top_upstreams_avg_time": topUpstreamsAvgTime != null ? List.from(topUpstreamsAvgTime!.map((x) => Map.from(x).map((k, v) => MapEntry(k, v)))) : null, "dns_queries": List.from(dnsQueries.map((x) => x)), "blocked_filtering": List.from(blockedFiltering.map((x) => x)), "replaced_safebrowsing": List.from(replacedSafebrowsing.map((x) => x)), diff --git a/lib/models/encryption.dart b/lib/models/encryption.dart index e0e0a4d..5fff3e0 100644 --- a/lib/models/encryption.dart +++ b/lib/models/encryption.dart @@ -27,18 +27,19 @@ class EncryptionData { final bool validPair; final bool enabled; final String? serverName; - final bool forceHttps; + final bool? forceHttps; final int? portHttps; final int? portDnsOverTls; final int? portDnsOverQuic; - final int portDnscrypt; - final String dnscryptConfigFile; + final int? portDnscrypt; + final String? dnscryptConfigFile; final bool allowUnencryptedDoh; final String certificateChain; final String privateKey; final String certificatePath; final String privateKeyPath; final bool privateKeySaved; + final bool? servePlainDns; EncryptionData({ required this.validCert, @@ -65,6 +66,7 @@ class EncryptionData { required this.certificatePath, required this.privateKeyPath, required this.privateKeySaved, + required this.servePlainDns, }); @@ -81,7 +83,7 @@ class EncryptionData { validPair: json["valid_pair"], enabled: json["enabled"], serverName: json["server_name"], - forceHttps: json["force_https"], + forceHttps: json["force_https"] ?? false, portHttps: json["port_https"], portDnsOverTls: json["port_dns_over_tls"], portDnsOverQuic: json["port_dns_over_quic"], @@ -93,6 +95,7 @@ class EncryptionData { certificatePath: json["certificate_path"], privateKeyPath: json["private_key_path"], privateKeySaved: json["private_key_saved"], + servePlainDns: json["serve_plain_dns"], ); Map toJson() => { @@ -120,5 +123,131 @@ class EncryptionData { "certificate_path": certificatePath, "private_key_path": privateKeyPath, "private_key_saved": privateKeySaved, + "serve_plain_dns": servePlainDns, + }; +} + + +class EncryptionValidationResult { + final bool isObject; + final EncryptionValidation? encryptionValidation; + final String? message; + + const EncryptionValidationResult({ + required this.isObject, + this.encryptionValidation, + this.message + }); +} + +class EncryptionValidation { + final String? subject; + final String? issuer; + final String? keyType; + final DateTime? notBefore; + final DateTime? notAfter; + final String? warningValidation; + final List? dnsNames; + final bool? validCert; + final bool? validChain; + final bool? validKey; + final bool? validPair; + final bool? enabled; + final String? serverName; + final bool? forceHttps; + final int? portHttps; + final int? portDnsOverTls; + final int? portDnsOverQuic; + final int? portDnscrypt; + final String? dnscryptConfigFile; + final bool? allowUnencryptedDoh; + final String? certificateChain; + final String? privateKey; + final String? certificatePath; + final String? privateKeyPath; + final bool? privateKeySaved; + + EncryptionValidation({ + this.subject, + this.issuer, + this.keyType, + this.notBefore, + this.notAfter, + this.warningValidation, + this.dnsNames, + this.validCert, + this.validChain, + this.validKey, + this.validPair, + this.enabled, + this.serverName, + this.forceHttps, + this.portHttps, + this.portDnsOverTls, + this.portDnsOverQuic, + this.portDnscrypt, + this.dnscryptConfigFile, + this.allowUnencryptedDoh, + this.certificateChain, + this.privateKey, + this.certificatePath, + this.privateKeyPath, + this.privateKeySaved, + }); + + factory EncryptionValidation.fromJson(Map json) => EncryptionValidation( + subject: json["subject"], + issuer: json["issuer"], + keyType: json["key_type"], + notBefore: json["not_before"] == null ? null : DateTime.parse(json["not_before"]), + notAfter: json["not_after"] == null ? null : DateTime.parse(json["not_after"]), + warningValidation: json["warning_validation"], + dnsNames: json["dns_names"] == null ? [] : List.from(json["dns_names"]!.map((x) => x)), + validCert: json["valid_cert"], + validChain: json["valid_chain"], + validKey: json["valid_key"], + validPair: json["valid_pair"], + enabled: json["enabled"], + serverName: json["server_name"], + forceHttps: json["force_https"], + portHttps: json["port_https"], + portDnsOverTls: json["port_dns_over_tls"], + portDnsOverQuic: json["port_dns_over_quic"], + portDnscrypt: json["port_dnscrypt"], + dnscryptConfigFile: json["dnscrypt_config_file"], + allowUnencryptedDoh: json["allow_unencrypted_doh"], + certificateChain: json["certificate_chain"], + privateKey: json["private_key"], + certificatePath: json["certificate_path"], + privateKeyPath: json["private_key_path"], + privateKeySaved: json["private_key_saved"], + ); + + Map toJson() => { + "subject": subject, + "issuer": issuer, + "key_type": keyType, + "not_before": notBefore?.toIso8601String(), + "not_after": notAfter?.toIso8601String(), + "warning_validation": warningValidation, + "dns_names": dnsNames == null ? [] : List.from(dnsNames!.map((x) => x)), + "valid_cert": validCert, + "valid_chain": validChain, + "valid_key": validKey, + "valid_pair": validPair, + "enabled": enabled, + "server_name": serverName, + "force_https": forceHttps, + "port_https": portHttps, + "port_dns_over_tls": portDnsOverTls, + "port_dns_over_quic": portDnsOverQuic, + "port_dnscrypt": portDnscrypt, + "dnscrypt_config_file": dnscryptConfigFile, + "allow_unencrypted_doh": allowUnencryptedDoh, + "certificate_chain": certificateChain, + "private_key": privateKey, + "certificate_path": certificatePath, + "private_key_path": privateKeyPath, + "private_key_saved": privateKeySaved, }; } diff --git a/lib/models/filtering.dart b/lib/models/filtering.dart index 20ca948..5e43bf4 100644 --- a/lib/models/filtering.dart +++ b/lib/models/filtering.dart @@ -1,28 +1,12 @@ -import 'dart:convert'; - -FilteringData filteringFromJson(String str) => FilteringData.fromJson(json.decode(str)); - -String filteringToJson(FilteringData data) => json.encode(data.toJson()); - class Filtering { - int loadStatus = 0; - FilteringData? data; - - Filtering({ - required this.loadStatus, - this.data - }); -} - -class FilteringData { - final List filters; - final List whitelistFilters; + List filters; + List whitelistFilters; List userRules; List blockedServices; int interval; bool enabled; - FilteringData({ + Filtering({ required this.filters, required this.whitelistFilters, required this.userRules, @@ -31,7 +15,7 @@ class FilteringData { required this.enabled, }); - factory FilteringData.fromJson(Map json) => FilteringData( + factory Filtering.fromJson(Map json) => Filtering( filters: json["filters"] != null ? List.from(json["filters"].map((x) => Filter.fromJson(x))) : [], whitelistFilters: json["whitelist_filters"] != null ? List.from(json["whitelist_filters"].map((x) => Filter.fromJson(x))) : [], userRules: json["user_rules"] != null ? List.from(json["user_rules"].map((x) => x)).where((i) => i != '').toList() : [], @@ -56,7 +40,7 @@ class Filter { final DateTime? lastUpdated; final int id; final int rulesCount; - final bool enabled; + bool enabled; Filter({ required this.url, @@ -79,9 +63,19 @@ class Filter { Map toJson() => { "url": url, "name": name, - "last_updated": lastUpdated != null ? lastUpdated!.toIso8601String() : null, + "last_updated": lastUpdated?.toIso8601String(), "id": id, "rules_count": rulesCount, "enabled": enabled, }; } + +class ProcessedList { + final Filter list; + final bool successful; + + const ProcessedList({ + required this.list, + required this.successful + }); +} \ No newline at end of file diff --git a/lib/models/filtering_status.dart b/lib/models/filtering_status.dart index bfd219d..83908c3 100644 --- a/lib/models/filtering_status.dart +++ b/lib/models/filtering_status.dart @@ -65,7 +65,7 @@ class Filter { Map toJson() => { "url": url, "name": name, - "last_updated": lastUpdated != null ? lastUpdated!.toIso8601String() : null, + "last_updated": lastUpdated?.toIso8601String(), "id": id, "rules_count": rulesCount, "enabled": enabled, diff --git a/lib/models/logs.dart b/lib/models/logs.dart index 7061318..b4991be 100644 --- a/lib/models/logs.dart +++ b/lib/models/logs.dart @@ -30,30 +30,30 @@ class LogsData { Map toJson() => { "data": List.from(data.map((x) => x.toJson())), - "oldest": oldest != null ? oldest!.toIso8601String() : null, + "oldest": oldest?.toIso8601String(), }; } class Log { - final bool answerDnssec; + final bool? answerDnssec; final bool cached; final String client; final ClientInfo? clientInfo; - final String clientProto; + final String? clientProto; final String elapsedMs; final Question question; final String reason; final List rules; - final String status; + final String? status; final DateTime time; - final String upstream; + final String? upstream; final List answer; final int? filterId; final String? rule; final List? originalAnswer; Log({ - required this.answerDnssec, + this.answerDnssec, required this.cached, required this.client, this.clientInfo, @@ -62,7 +62,7 @@ class Log { required this.question, required this.reason, required this.rules, - required this.status, + this.status, required this.time, required this.upstream, required this.answer, @@ -73,14 +73,14 @@ class Log { factory Log.fromJson(Map json) => Log( answerDnssec: json["answer_dnssec"], - cached: json["cached"], + cached: json["cached"] ?? false, client: json["client"], clientInfo: json["client_info"] != null ? ClientInfo.fromJson(json["client_info"]) : null, clientProto: json["client_proto"], elapsedMs: json["elapsedMs"], question: Question.fromJson(json["question"]), reason: json["reason"], - rules: List.from(json["rules"].map((x) => Rule.fromJson(x))), + rules: json["rules"] != null ? List.from(json["rules"].map((x) => Rule.fromJson(x))) : [], status: json["status"], time: DateTime.parse(json["time"]), upstream: json["upstream"], @@ -172,7 +172,7 @@ class Whois { class Question { final String questionClass; - final String name; + final String? name; final String type; Question({ diff --git a/lib/models/menu_option.dart b/lib/models/menu_option.dart new file mode 100644 index 0000000..e898514 --- /dev/null +++ b/lib/models/menu_option.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class MenuOption { + final IconData? icon; + final String title; + final void Function() action; + final bool? disabled; + + const MenuOption({ + required this.title, + required this.action, + this.icon, + this.disabled + }); +} \ No newline at end of file diff --git a/lib/models/querylog_config.dart b/lib/models/querylog_config.dart new file mode 100644 index 0000000..dfb80b4 --- /dev/null +++ b/lib/models/querylog_config.dart @@ -0,0 +1,27 @@ +class QueryLogConfig { + final List? ignored; + final int? interval; + final bool? enabled; + final bool? anonymizeClientIp; + + QueryLogConfig({ + this.ignored, + this.interval, + this.enabled, + this.anonymizeClientIp, + }); + + factory QueryLogConfig.fromJson(Map json) => QueryLogConfig( + ignored: json["ignored"] == null ? [] : List.from(json["ignored"]!.map((x) => x)), + interval: json["interval"], + enabled: json["enabled"], + anonymizeClientIp: json["anonymize_client_ip"], + ); + + Map toJson() => { + "ignored": ignored == null ? [] : List.from(ignored!.map((x) => x)), + "interval": interval, + "enabled": enabled, + "anonymize_client_ip": anonymizeClientIp, + }; +} diff --git a/lib/models/rewrite_rules.dart b/lib/models/rewrite_rules.dart index 44bef8d..5af2851 100644 --- a/lib/models/rewrite_rules.dart +++ b/lib/models/rewrite_rules.dart @@ -1,29 +1,13 @@ -import 'dart:convert'; - class RewriteRules { - int loadStatus = 0; - List? data; - - RewriteRules({ - required this.loadStatus, - this.data - }); -} - -List rewriteRulesDataFromJson(String str) => List.from(json.decode(str).map((x) => RewriteRulesData.fromJson(x))); - -String rewriteRulesDataToJson(List data) => json.encode(List.from(data.map((x) => x.toJson()))); - -class RewriteRulesData { final String domain; final String answer; - RewriteRulesData({ + RewriteRules({ required this.domain, required this.answer, }); - factory RewriteRulesData.fromJson(Map json) => RewriteRulesData( + factory RewriteRules.fromJson(Map json) => RewriteRules( domain: json["domain"], answer: json["answer"], ); diff --git a/lib/models/safe_search.dart b/lib/models/safe_search.dart new file mode 100644 index 0000000..4b960c9 --- /dev/null +++ b/lib/models/safe_search.dart @@ -0,0 +1,39 @@ +class SafeSearch { + bool enabled = false; + bool bing = false; + bool duckduckgo = false; + bool google = false; + bool pixabay = false; + bool yandex = false; + bool youtube = false; + + SafeSearch({ + required this.enabled, + required this.bing, + required this.duckduckgo, + required this.google, + required this.pixabay, + required this.yandex, + required this.youtube, + }); + + factory SafeSearch.fromJson(Map json) => SafeSearch( + enabled: json["enabled"], + bing: json["bing"], + duckduckgo: json["duckduckgo"], + google: json["google"], + pixabay: json["pixabay"], + yandex: json["yandex"], + youtube: json["youtube"], + ); + + Map toJson() => { + "enabled": enabled, + "bing": bing, + "duckduckgo": duckduckgo, + "google": google, + "pixabay": pixabay, + "yandex": yandex, + "youtube": youtube, + }; +} diff --git a/lib/models/server.dart b/lib/models/server.dart index 162cef7..6c50164 100644 --- a/lib/models/server.dart +++ b/lib/models/server.dart @@ -5,10 +5,10 @@ class Server { String domain; String? path; int? port; - String user; - String password; + String? user; + String? password; bool defaultServer; - String authToken; + String? authToken; bool runningOnHa; Server({ @@ -18,10 +18,10 @@ class Server { required this.domain, this.path, this.port, - required this.user, - required this.password, + this.user, + this.password, required this.defaultServer, - required this.authToken, + this.authToken, required this.runningOnHa, }); } \ No newline at end of file diff --git a/lib/models/server_info.dart b/lib/models/server_info.dart index 6e170a5..8d26857 100644 --- a/lib/models/server_info.dart +++ b/lib/models/server_info.dart @@ -1,7 +1,9 @@ import 'dart:convert'; +import 'package:adguard_home_manager/constants/enums.dart'; + class ServerInfo { - int loadStatus = 0; + LoadStatus loadStatus = LoadStatus.loading; ServerInfoData? data; ServerInfo({ @@ -41,7 +43,7 @@ class ServerInfoData { dnsPort: json["dns_port"], httpPort: json["http_port"], protectionEnabled: json["protection_enabled"], - dhcpAvailable: json["dhcp_available"], + dhcpAvailable: json["dhcp_available"] ?? false, running: json["running"], version: json["version"], language: json["language"], diff --git a/lib/models/server_status.dart b/lib/models/server_status.dart index fec00b3..16e20b4 100644 --- a/lib/models/server_status.dart +++ b/lib/models/server_status.dart @@ -1,45 +1,71 @@ +import 'package:adguard_home_manager/functions/time_server_disabled.dart'; import 'package:adguard_home_manager/models/clients.dart'; import 'package:adguard_home_manager/models/dns_statistics.dart'; import 'package:adguard_home_manager/models/filtering_status.dart'; class ServerStatus { - int loadStatus; - ServerStatusData? data; - - ServerStatus({ - required this.loadStatus, - this.data - }); -} -class ServerStatusData { final DnsStatistics stats; final List clients; final FilteringStatus filteringStatus; + int timeGeneralDisabled; + DateTime? disabledUntil; bool generalEnabled; bool filteringEnabled; bool safeSearchEnabled; bool safeBrowsingEnabled; bool parentalControlEnabled; + final String serverVersion; + bool? safeSeachBing; + bool? safeSearchGoogle; + bool? safeSearchDuckduckgo; + bool? safeSearchPixabay; + bool? safeSearchYandex; + bool? safeSearchYoutube; + bool dhcpAvailable; - ServerStatusData({ + ServerStatus({ required this.stats, required this.clients, required this.filteringStatus, + required this.timeGeneralDisabled, + this.disabledUntil, required this.generalEnabled, required this.filteringEnabled, required this.safeSearchEnabled, required this.safeBrowsingEnabled, - required this.parentalControlEnabled + required this.parentalControlEnabled, + required this.serverVersion, + required this.safeSeachBing, + required this.safeSearchGoogle, + required this.safeSearchDuckduckgo, + required this.safeSearchPixabay, + required this.safeSearchYandex, + required this.safeSearchYoutube, + required this.dhcpAvailable, }); - factory ServerStatusData.fromJson(Map json) => ServerStatusData( + factory ServerStatus.fromJson(Map json) => ServerStatus( stats: DnsStatistics.fromJson(json['stats']), clients: json["clients"] != null ? List.from(json["clients"].map((x) => Client.fromJson(x))) : [], - generalEnabled: json['generalEnabled']['protection_enabled'], + generalEnabled: json['status']['protection_enabled'], + timeGeneralDisabled: json['status']['protection_disabled_duration'] ?? 0, + disabledUntil: json['status']['protection_disabled_duration'] != null + ? json['status']['protection_disabled_duration'] > 0 + ? generateTimeDeadline(json['status']['protection_disabled_duration']) + : null + : null, filteringStatus: FilteringStatus.fromJson(json['filtering']), filteringEnabled: json['filtering']['enabled'], - safeSearchEnabled: json['safeSearchEnabled']['enabled'], + safeSearchEnabled: json['safeSearch']['enabled'], safeBrowsingEnabled: json['safeBrowsingEnabled']['enabled'], - parentalControlEnabled: json['parentalControlEnabled']['enabled'] + parentalControlEnabled: json['parentalControlEnabled']['enabled'], + serverVersion: json['status']['version'], + safeSeachBing: json['safeSearch']['bing'], + safeSearchDuckduckgo: json['safeSearch']['duckduckgo'], + safeSearchGoogle: json['safeSearch']['google'], + safeSearchPixabay: json['safeSearch']['pixabay'], + safeSearchYandex: json['safeSearch']['yandex'], + safeSearchYoutube: json['safeSearch']['youtube'], + dhcpAvailable: json['status']['dhcp_available'] ?? false ); } \ No newline at end of file diff --git a/lib/models/statistics_config.dart b/lib/models/statistics_config.dart new file mode 100644 index 0000000..f504708 --- /dev/null +++ b/lib/models/statistics_config.dart @@ -0,0 +1,23 @@ +class StatisticsConfig { + final List? ignored; + final int? interval; + final bool? enabled; + + StatisticsConfig({ + this.ignored, + this.interval, + this.enabled, + }); + + factory StatisticsConfig.fromJson(Map json) => StatisticsConfig( + ignored: json["ignored"] == null ? [] : List.from(json["ignored"]!.map((x) => x)), + interval: json["interval"], + enabled: json["enabled"], + ); + + Map toJson() => { + "ignored": ignored == null ? [] : List.from(ignored!.map((x) => x)), + "interval": interval, + "enabled": enabled, + }; +} diff --git a/lib/models/update_available.dart b/lib/models/update_available.dart new file mode 100644 index 0000000..890e99c --- /dev/null +++ b/lib/models/update_available.dart @@ -0,0 +1,60 @@ +import 'package:adguard_home_manager/constants/enums.dart'; + +class UpdateAvailable { + LoadStatus loadStatus = LoadStatus.loading; + UpdateAvailableData? data; + + UpdateAvailable({ + required this.loadStatus, + required this.data + }); +} + +class UpdateAvailableData { + String currentVersion; + String? newVersion; + final String? announcement; + final String? announcementUrl; + final bool? canAutoupdate; + final bool? disabled; + String? changelog; + + UpdateAvailableData({ + required this.currentVersion, + this.newVersion, + required this.announcement, + required this.announcementUrl, + required this.canAutoupdate, + required this.disabled, + this.changelog, + }); + + factory UpdateAvailableData.fromJson(Map json) => UpdateAvailableData( + currentVersion: json["current_version"], + newVersion: json["new_version"], + announcement: json["announcement"], + announcementUrl: json["announcement_url"], + canAutoupdate: json["can_autoupdate"], + disabled: json["disabled"], + changelog: json["changelog"], + ); + + factory UpdateAvailableData.fromJsonUpdate(Map json) => UpdateAvailableData( + currentVersion: json["current_version"], + newVersion: json["new_version"], + announcement: json["announcement"], + announcementUrl: json["announcement_url"], + canAutoupdate: json["can_autoupdate"], + disabled: json["disabled"], + ); + + Map toJson() => { + "current_version": currentVersion, + "new_version": newVersion, + "announcement": announcement, + "announcement_url": announcementUrl, + "can_autoupdate": canAutoupdate, + "disabled": disabled, + "changelog": changelog, + }; +} diff --git a/lib/providers/app_config_provider.dart b/lib/providers/app_config_provider.dart index 262a448..bb6887c 100644 --- a/lib/providers/app_config_provider.dart +++ b/lib/providers/app_config_provider.dart @@ -1,14 +1,22 @@ + import 'package:flutter/material.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/scheduler.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import 'package:sqflite/sqlite_api.dart'; -import 'package:adguard_home_manager/functions/conversions.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/config/home_top_items_default_order.dart'; +import 'package:adguard_home_manager/models/github_release.dart'; import 'package:adguard_home_manager/models/app_log.dart'; class AppConfigProvider with ChangeNotifier { - Database? _dbInstance; + final SharedPreferences sharedPreferencesInstance; + + AppConfigProvider({ + required this.sharedPreferencesInstance + }); PackageInfo? _appInfo; AndroidDeviceInfo? _androidDeviceInfo; @@ -16,26 +24,39 @@ class AppConfigProvider with ChangeNotifier { int _selectedScreen = 0; + int? _selectedSettingsScreen; + bool _showingSnackbar = false; + bool _supportsDynamicTheme = true; int _selectedTheme = 0; bool _useDynamicColor = true; int _staticColor = 0; - bool _useThemeColorForStatus = false; + final bool _useThemeColorForStatus = false; int _selectedClientsTab = 0; int _selectedFiltersTab = 0; + List _homeTopItemsOrder = homeTopItemsDefaultOrder; + + bool _hideServerAddress = false; + final List _logs = []; - int _overrideSslCheck = 0; + bool _overrideSslCheck = false; - int _hideZeroValues = 0; + bool _hideZeroValues = false; - int _showNameTimeLogs = 0; + bool _showTimeLogs = false; + + bool _showIpLogs = false; + + bool _combinedChartHome = false; String? _doNotRememberVersion; + GitHubRelease? _appUpdatesAvailable; + PackageInfo? get getAppInfo { return _appInfo; } @@ -66,6 +87,10 @@ class AppConfigProvider with ChangeNotifier { } } + bool get supportsDynamicTheme { + return _supportsDynamicTheme; + } + int get selectedThemeNumber { return _selectedTheme; } @@ -83,11 +108,11 @@ class AppConfigProvider with ChangeNotifier { } bool get overrideSslCheck { - return _overrideSslCheck == 1 ? true : false; + return _overrideSslCheck; } bool get hideZeroValues { - return _hideZeroValues == 1 ? true : false; + return _hideZeroValues; } int get selectedScreen { @@ -110,16 +135,40 @@ class AppConfigProvider with ChangeNotifier { return _useThemeColorForStatus; } - bool get showNameTimeLogs { - return _showNameTimeLogs == 1 ? true : false; + bool get showTimeLogs { + return _showTimeLogs; + } + + bool get showIpLogs { + return _showIpLogs; + } + + bool get combinedChartHome { + return _combinedChartHome; } String? get doNotRememberVersion { return _doNotRememberVersion; } - void setDbInstance(Database db) { - _dbInstance = db; + int? get selectedSettingsScreen { + return _selectedSettingsScreen; + } + + GitHubRelease? get appUpdatesAvailable { + return _appUpdatesAvailable; + } + + List get homeTopItemsOrder { + return _homeTopItemsOrder; + } + + bool get hideServerAddress { + return _hideServerAddress; + } + + void setSupportsDynamicTheme(bool value) { + _supportsDynamicTheme = value; } void setAppInfo(PackageInfo appInfo) { @@ -159,207 +208,183 @@ class AppConfigProvider with ChangeNotifier { notifyListeners(); } + void setSelectedSettingsScreen({required int? screen, bool? notify}) { + _selectedSettingsScreen = screen; + if (notify == true) { + notifyListeners(); + } + } + + void setAppUpdatesAvailable(GitHubRelease? value) { + _appUpdatesAvailable = value; + notifyListeners(); + } + Future setOverrideSslCheck(bool status) async { - final updated = await _updateOverrideSslCheck(status == true ? 1 : 0); - if (updated == true) { - _overrideSslCheck = status == true ? 1 : 0; + try { + sharedPreferencesInstance.setBool('overrideSslCheck', status); + _overrideSslCheck = status; notifyListeners(); return true; - } - else { + } catch (e, stackTrace) { + Sentry.captureException(e, stackTrace: stackTrace); return false; } } Future setHideZeroValues(bool status) async { - final updated = await _updateSetHideZeroValues(status == true ? 1 : 0); - if (updated == true) { - _hideZeroValues = status == true ? 1 : 0; + try { + sharedPreferencesInstance.setBool('hideZeroValues', status); + _hideZeroValues = status; notifyListeners(); return true; - } - else { + } catch (e, stackTrace) { + Sentry.captureException(e, stackTrace: stackTrace); return false; } } - Future setShowNameTimeLogs(bool status) async { - final updated = await _updateShowNameTimeLogsDb(status == true ? 1 : 0); - if (updated == true) { - _showNameTimeLogs = status == true ? 1 : 0; + Future setshowTimeLogs(bool status) async { + try { + sharedPreferencesInstance.setBool('showTimeLogs', status); + _showTimeLogs = status; notifyListeners(); return true; + } catch (e, stackTrace) { + Sentry.captureException(e, stackTrace: stackTrace); + return false; } - else { + } + + Future setShowIpLogs(bool status) async { + try { + sharedPreferencesInstance.setBool('showIpLogs', status); + _showIpLogs = status; + notifyListeners(); + return true; + } catch (e, stackTrace) { + Sentry.captureException(e, stackTrace: stackTrace); return false; } } Future setSelectedTheme(int value) async { - final updated = await _updateThemeDb(value); - if (updated == true) { + try { + sharedPreferencesInstance.setInt('selectedTheme', value); _selectedTheme = value; notifyListeners(); return true; - } - else { + } catch (e, stackTrace) { + Sentry.captureException(e, stackTrace: stackTrace); return false; } } Future setUseDynamicColor(bool value) async { - final updated = await _updateDynamicColorDb(value == true ? 1 : 0); - if (updated == true) { + try { + sharedPreferencesInstance.setBool('useDynamicColor', value); _useDynamicColor = value; notifyListeners(); return true; - } - else { + } catch (e, stackTrace) { + Sentry.captureException(e, stackTrace: stackTrace); return false; } } - Future setUseThemeColorForStatus(bool value) async { - final updated = await _updateUseThemeColorForStatusDb(value == true ? 1 : 0); - if (updated == true) { - _useThemeColorForStatus = value; + Future setCombinedChartHome(bool value) async { + try { + sharedPreferencesInstance.setBool('combinedChart', value); + _combinedChartHome = value; notifyListeners(); return true; - } - else { + } catch (e, stackTrace) { + Sentry.captureException(e, stackTrace: stackTrace); return false; } } Future setStaticColor(int value) async { - final updated = await _updateStaticColorDb(value); - if (updated == true) { + try { + sharedPreferencesInstance.setInt('staticColor', value); _staticColor = value; notifyListeners(); return true; + } catch (e, stackTrace) { + Sentry.captureException(e, stackTrace: stackTrace); + return false; } - else { + } + + Future setHomeTopItemsOrder(List order) async { + try { + sharedPreferencesInstance.setStringList('homeTopItemsOrder', List.from(order.map((e) => e.name))); + _homeTopItemsOrder = order; + notifyListeners(); + return true; + } catch (e, stackTrace) { + Sentry.captureException(e, stackTrace: stackTrace); + return false; + } + } + + Future setHideServerAddress(bool value) async { + try { + sharedPreferencesInstance.setBool('hideServerAddress', value); + _hideServerAddress = value; + notifyListeners(); + return true; + } catch (e, stackTrace) { + Sentry.captureException(e, stackTrace: stackTrace); return false; } } Future setDoNotRememberVersion(String value) async { - try { - return await _dbInstance!.transaction((txn) async { - await txn.rawUpdate( - 'UPDATE appConfig SET doNotRememberVersion = "$value"', - ); - _doNotRememberVersion = value; - notifyListeners(); - return true; - }); - } catch (e) { - return false; - } + final updated = await sharedPreferencesInstance.setString('hideServerAddress', value); + return updated; } + + void saveFromSharedPreferences() { + _selectedTheme = sharedPreferencesInstance.getInt('selectedTheme') ?? 0; + _overrideSslCheck = sharedPreferencesInstance.getBool('overrideSslCheck') ?? false; + _hideZeroValues = sharedPreferencesInstance.getBool('hideZeroValues') ?? false; + _useDynamicColor = sharedPreferencesInstance.getBool('useDynamicColor') ?? true; + _staticColor = sharedPreferencesInstance.getInt('staticColor') ?? 0; + _showTimeLogs = sharedPreferencesInstance.getBool('showTimeLogs') ?? false; + _doNotRememberVersion = sharedPreferencesInstance.getString('doNotRememberVersion'); + _showIpLogs = sharedPreferencesInstance.getBool('showIpLogs') ?? false; + _combinedChartHome = sharedPreferencesInstance.getBool('combinedChart') ?? false; + _hideServerAddress = sharedPreferencesInstance.getBool('hideServerAddress') ?? false; + if (sharedPreferencesInstance.getStringList('homeTopItemsOrder') != null) { + try { + _homeTopItemsOrder = List.from( + List.from(sharedPreferencesInstance.getStringList('homeTopItemsOrder')!).map((e) { + switch (e) { + case 'queriedDomains': + return HomeTopItems.queriedDomains; - Future _updateThemeDb(int value) async { - try { - return await _dbInstance!.transaction((txn) async { - await txn.rawUpdate( - 'UPDATE appConfig SET theme = $value', + case 'blockedDomains': + return HomeTopItems.blockedDomains; + + case 'recurrentClients': + return HomeTopItems.recurrentClients; + + case 'topUpstreams': + return HomeTopItems.topUpstreams; + + case 'avgUpstreamResponseTime': + return HomeTopItems.avgUpstreamResponseTime; + + default: + return null; + } + }).where((e) => e != null).toList() ); - return true; - }); - } catch (e) { - return false; + } catch (e) { + Sentry.captureException(e); + _homeTopItemsOrder = homeTopItemsDefaultOrder; + } } } - - Future _updateDynamicColorDb(int value) async { - try { - return await _dbInstance!.transaction((txn) async { - await txn.rawUpdate( - 'UPDATE appConfig SET useDynamicColor = $value', - ); - return true; - }); - } catch (e) { - return false; - } - } - - Future _updateUseThemeColorForStatusDb(int value) async { - try { - return await _dbInstance!.transaction((txn) async { - await txn.rawUpdate( - 'UPDATE appConfig SET useThemeColorForStatus = $value', - ); - return true; - }); - } catch (e) { - return false; - } - } - - Future _updateStaticColorDb(int value) async { - try { - return await _dbInstance!.transaction((txn) async { - await txn.rawUpdate( - 'UPDATE appConfig SET staticColor = $value', - ); - return true; - }); - } catch (e) { - return false; - } - } - - Future _updateOverrideSslCheck(int value) async { - try { - return await _dbInstance!.transaction((txn) async { - await txn.rawUpdate( - 'UPDATE appConfig SET overrideSslCheck = $value', - ); - return true; - }); - } catch (e) { - return false; - } - } - - Future _updateSetHideZeroValues(int value) async { - try { - return await _dbInstance!.transaction((txn) async { - await txn.rawUpdate( - 'UPDATE appConfig SET hideZeroValues = $value', - ); - return true; - }); - } catch (e) { - return false; - } - } - - Future _updateShowNameTimeLogsDb(int value) async { - try { - return await _dbInstance!.transaction((txn) async { - await txn.rawUpdate( - 'UPDATE appConfig SET showNameTimeLogs = $value', - ); - return true; - }); - } catch (e) { - return false; - } - } - - void saveFromDb(Database dbInstance, Map dbData) { - _selectedTheme = dbData['theme']; - _overrideSslCheck = dbData['overrideSslCheck']; - _hideZeroValues = dbData['hideZeroValues']; - _useDynamicColor = convertFromIntToBool(dbData['useDynamicColor'])!; - _staticColor = dbData['staticColor']; - _useThemeColorForStatus = dbData['useThemeColorForStatus'] != null ? convertFromIntToBool(dbData['useThemeColorForStatus'])! : false; - _showNameTimeLogs = dbData['showNameTimeLogs']; - _doNotRememberVersion = dbData['doNotRememberVersion']; - - _dbInstance = dbInstance; - notifyListeners(); - } } \ No newline at end of file diff --git a/lib/providers/clients_provider.dart b/lib/providers/clients_provider.dart new file mode 100644 index 0000000..36185cc --- /dev/null +++ b/lib/providers/clients_provider.dart @@ -0,0 +1,287 @@ +import 'package:flutter/material.dart'; + +import 'package:adguard_home_manager/services/api_client.dart'; +import 'package:adguard_home_manager/models/clients.dart'; +import 'package:adguard_home_manager/functions/maps_fns.dart'; +import 'package:adguard_home_manager/providers/status_provider.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; +import 'package:adguard_home_manager/models/clients_allowed_blocked.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; + +enum AccessSettingsList { allowed, disallowed, domains } + +class ClientsProvider with ChangeNotifier { + ServersProvider? _serversProvider; + + update(ServersProvider? servers, StatusProvider? status) { + _serversProvider = servers; + } + + LoadStatus _loadStatus = LoadStatus.loading; + Clients? _clients; + String? _searchTermClients; + List _filteredActiveClients = []; + List _filteredAddedClients = []; + + LoadStatus get loadStatus { + return _loadStatus; + } + + Clients? get clients { + return _clients; + } + + String? get searchTermClients { + return _searchTermClients; + } + + List get filteredActiveClients { + return _filteredActiveClients; + } + + List get filteredAddedClients { + return _filteredAddedClients; + } + + void setClientsLoadStatus(LoadStatus status, bool notify) { + _loadStatus = status; + if (notify == true) { + notifyListeners(); + } + } + + void setClientsData(Clients data, bool notify) { + _clients = data; + if (_searchTermClients != null && _searchTermClients != '') { + _filteredActiveClients = _clients!.autoClients.where( + (client) => client.ip.contains(_searchTermClients!.toLowerCase()) || (client.name != null ? client.name!.contains(_searchTermClients!.toLowerCase()) : false) + ).toList(); + _filteredAddedClients = _clients!.clients.where( + (client) { + isContained(String value) => value.contains(value.toLowerCase()); + return client.ids.any(isContained); + } + ).toList(); + } + else { + _filteredActiveClients = data.autoClients; + _filteredAddedClients = data.clients; + } + if (notify == true) notifyListeners(); + } + + void setSearchTermClients(String? value) { + _searchTermClients = value; + if (value != null && value != '') { + if (_clients != null) { + _filteredActiveClients = _clients!.autoClients.where( + (client) => client.ip.contains(value.toLowerCase()) || (client.name != null ? client.name!.contains(value.toLowerCase()) : false) + ).toList(); + _filteredAddedClients = _clients!.clients.where( + (client) { + isContained(String value) => value.contains(value.toLowerCase()); + return client.ids.any(isContained); + } + ).toList(); + } + } + else { + if (_clients != null) _filteredActiveClients = _clients!.autoClients; + if (_clients != null) _filteredAddedClients = _clients!.clients; + } + notifyListeners(); + } + + void setAllowedDisallowedClientsBlockedDomains(ClientsAllowedBlocked data) { + _clients?.clientsAllowedBlocked = data; + notifyListeners(); + } + + Future fetchClients({ + bool? updateLoading + }) async { + if (updateLoading == true) { + _loadStatus = LoadStatus.loading; + } + final result = await _serversProvider!.apiClient2!.getClients(); + if (result.successful == true) { + setClientsData(result.content as Clients, false); + _loadStatus = LoadStatus.loaded; + notifyListeners(); + return true; + } + else { + if (updateLoading == true) { + _loadStatus = LoadStatus.error; + notifyListeners(); + } + return false; + } + } + + Future deleteClient(Client client) async { + final result = await _serversProvider!.apiClient2!.postDeleteClient(name: client.name); + + if (result.successful == true) { + Clients clientsData = clients!; + clientsData.clients = clientsData.clients.where((c) => c.name != client.name).toList(); + setClientsData(clientsData, false); + + notifyListeners(); + return true; + } + else { + return false; + } + } + + Future editClient(Client client) async { + final result = await _serversProvider!.apiClient2!.postUpdateClient( + data: { + 'name': client.name, + 'data': removePropFromMap(client.toJson(), 'safe_search') + } + ); + + if (result.successful == true) { + Clients clientsData = clients!; + clientsData.clients = clientsData.clients.map((e) { + if (e.name == client.name) { + return client; + } + else { + return e; + } + }).toList(); + setClientsData(clientsData, false); + + notifyListeners(); + return true; + } + else { + notifyListeners(); + return false; + } + } + + Future addClient(Client client) async { + final result = await _serversProvider!.apiClient2!.postAddClient( + data: removePropFromMap(client.toJson(), 'safe_search') + ); + + if (result.successful == true) { + Clients clientsData = clients!; + clientsData.clients.add(client); + setClientsData(clientsData, false); + + notifyListeners(); + return true; + } + else { + notifyListeners(); + return false; + } + } + + Future addClientList(String item, AccessSettingsList type) async { + Map> body = { + "allowed_clients": clients!.clientsAllowedBlocked?.allowedClients ?? [], + "disallowed_clients": clients!.clientsAllowedBlocked?.disallowedClients ?? [], + "blocked_hosts": clients!.clientsAllowedBlocked?.blockedHosts ?? [], + }; + + if (body['allowed_clients']!.contains(item)) { + body['allowed_clients'] = body['allowed_clients']!.where((e) => e != item).toList(); + } + else if (body['disallowed_clients']!.contains(item)) { + body['disallowed_clients'] = body['disallowed_clients']!.where((e) => e != item).toList(); + } + else if (body['blocked_hosts']!.contains(item)) { + body['blocked_hosts'] = body['blocked_hosts']!.where((e) => e != item).toList(); + } + + if (type == AccessSettingsList.allowed) { + body['allowed_clients']!.add(item); + } + else if (type == AccessSettingsList.disallowed) { + body['disallowed_clients']!.add(item); + } + else if (type == AccessSettingsList.domains) { + body['blocked_hosts']!.add(item); + } + + final result = await _serversProvider!.apiClient2!.requestAllowedBlockedClientsHosts( + body: body + ); + + if (result.successful == true) { + _clients?.clientsAllowedBlocked = ClientsAllowedBlocked( + allowedClients: body['allowed_clients'] ?? [], + disallowedClients: body['disallowed_clients'] ?? [], + blockedHosts: body['blocked_hosts'] ?? [], + ); + notifyListeners(); + return result; + } + else if (result.successful == false && result.content == 'client_another_list') { + notifyListeners(); + return result; + } + else { + notifyListeners(); + return result; + } + } + + AccessSettingsList? checkClientList(String client) { + if (_clients!.clientsAllowedBlocked!.allowedClients.contains(client)) { + return AccessSettingsList.allowed; + } + else if (_clients!.clientsAllowedBlocked!.disallowedClients.contains(client)) { + return AccessSettingsList.disallowed; + } + else { + return null; + } + } + + Future removeClientList(String client, AccessSettingsList type) async { + Map> body = { + "allowed_clients": clients!.clientsAllowedBlocked?.allowedClients ?? [], + "disallowed_clients": clients!.clientsAllowedBlocked?.disallowedClients ?? [], + "blocked_hosts": clients!.clientsAllowedBlocked?.blockedHosts ?? [], + }; + + if (type == AccessSettingsList.allowed) { + body['allowed_clients'] = body['allowed_clients']!.where((c) => c != client).toList(); + } + else if (type == AccessSettingsList.disallowed) { + body['disallowed_clients'] = body['disallowed_clients']!.where((c) => c != client).toList(); + } + else if (type == AccessSettingsList.domains) { + body['blocked_hosts'] = body['blocked_hosts']!.where((c) => c != client).toList(); + } + + final result = await _serversProvider!.apiClient2!.requestAllowedBlockedClientsHosts( + body: body + ); + + if (result.successful == true) { + _clients?.clientsAllowedBlocked = ClientsAllowedBlocked( + allowedClients: body['allowed_clients'] ?? [], + disallowedClients: body['disallowed_clients'] ?? [], + blockedHosts: body['blocked_hosts'] ?? [], + ); + notifyListeners(); + return result; + } + else if (result.successful == false && result.content == 'client_another_list') { + notifyListeners(); + return result; + } + else { + notifyListeners(); + return result; + } + } +} \ No newline at end of file diff --git a/lib/providers/dhcp_provider.dart b/lib/providers/dhcp_provider.dart new file mode 100644 index 0000000..4fbd66b --- /dev/null +++ b/lib/providers/dhcp_provider.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; + +import 'package:adguard_home_manager/services/api_client.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/models/dhcp.dart'; + +class DhcpProvider with ChangeNotifier { + ServersProvider? _serversProvider; + + update(ServersProvider? provider) { + _serversProvider = provider; + } + + LoadStatus _loadStatus = LoadStatus.loading; + DhcpModel? _dhcp; + + DhcpModel? get dhcp { + return _dhcp; + } + + LoadStatus get loadStatus { + return _loadStatus; + } + + void setDhcpData(DhcpModel data) { + _dhcp = data; + notifyListeners(); + } + + void setDhcpLoadStatus(LoadStatus status, bool notify) { + _loadStatus = status; + if (notify == true) { + notifyListeners(); + } + } + + Future loadDhcpStatus({ + bool? showLoading + }) async { + if (showLoading == true) { + _loadStatus = LoadStatus.loading; + notifyListeners(); + } + final result = await _serversProvider!.apiClient2!.getDhcpData(); + if (result.successful == true) { + _dhcp = result.content as DhcpModel; + _loadStatus = LoadStatus.loaded; + notifyListeners(); + return true; + } + else { + if (showLoading == true) { + _loadStatus = LoadStatus.error; + notifyListeners(); + } + return false; + } + } + + Future deleteLease(Lease lease) async { + final result = await _serversProvider!.apiClient2!.deleteStaticLease( + data: { + "mac": lease.mac, + "ip": lease.ip, + "hostname": lease.hostname + } + ); + + if (result.successful == true) { + DhcpModel data = dhcp!; + data.dhcpStatus!.staticLeases = data.dhcpStatus!.staticLeases.where((l) => l.mac != lease.mac).toList(); + setDhcpData(data); + return true; + } + else { + notifyListeners(); + return false; + } + } + + Future createLease(Lease lease) async { + final result = await _serversProvider!.apiClient2!.createStaticLease( + data: { + "mac": lease.mac, + "ip": lease.ip, + "hostname": lease.hostname, + } + ); + + if (result.successful == true) { + DhcpModel data = dhcp!; + data.dhcpStatus!.staticLeases.add(lease); + setDhcpData(data); + return result; + } + else { + return result; + } + } +} \ No newline at end of file diff --git a/lib/providers/dns_provider.dart b/lib/providers/dns_provider.dart new file mode 100644 index 0000000..a59c3c5 --- /dev/null +++ b/lib/providers/dns_provider.dart @@ -0,0 +1,181 @@ +import 'package:flutter/material.dart'; + +import 'package:adguard_home_manager/services/api_client.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/models/dns_info.dart'; + +class DnsProvider with ChangeNotifier { + ServersProvider? _serversProvider; + + update(ServersProvider? provider) { + _serversProvider = provider; + } + + LoadStatus _loadStatus = LoadStatus.loading; + DnsInfo? _dnsInfo; + + LoadStatus get loadStatus { + return _loadStatus; + } + + DnsInfo? get dnsInfo { + return _dnsInfo; + } + + void setDnsInfoData(DnsInfo data) { + _dnsInfo = data; + notifyListeners(); + } + + void setDnsInfoLoadStatus(LoadStatus status, bool notify) { + _loadStatus = status; + if (notify == true) { + notifyListeners(); + } + } + + Future fetchDnsData({ + bool? showLoading + }) async { + if (showLoading == true) { + _loadStatus = LoadStatus.loading; + } + + final result = await _serversProvider!.apiClient2!.getDnsInfo(); + + if (result.successful == true) { + _dnsInfo = result.content as DnsInfo; + _loadStatus = LoadStatus.loaded; + notifyListeners(); + return true; + } + else { + if (showLoading == false) { + _loadStatus = LoadStatus.loaded; + notifyListeners(); + } + return false; + } + } + + Future savePrivateReverseServersConfig(Map value) async { + final result = await _serversProvider!.apiClient2!.setDnsConfig( + data: value + ); + + if (result.successful == true) { + DnsInfo data = dnsInfo!; + if (value['local_ptr_upstreams'] != null) { + data.localPtrUpstreams = value['local_ptr_upstreams']; + } + data.usePrivatePtrResolvers = value['use_private_ptr_resolvers']; + data.resolveClients = value['resolve_clients']; + setDnsInfoData(data); + return result; + } + else { + return result; + } + } + + Future saveUpstreamDnsConfig(Map value) async { + final result = await _serversProvider!.apiClient2!.setDnsConfig( + data: value + ); + + if (result.successful == true) { + DnsInfo data = dnsInfo!; + data.upstreamDns = List.from(value['upstream_dns']); + data.upstreamMode = value['upstream_mode']; + setDnsInfoData(data); + return result; + } + else { + return result; + } + } + + Future saveBootstrapDnsConfig(Map value) async { + final result = await _serversProvider!.apiClient2!.setDnsConfig( + data: value + ); + + if (result.successful == true) { + DnsInfo data = dnsInfo!; + data.bootstrapDns = List.from(value['bootstrap_dns']); + setDnsInfoData(data); + return result; + } + else { + return result; + } + } + + Future saveFallbackDnsConfig(Map value) async { + final result = await _serversProvider!.apiClient2!.setDnsConfig( + data: value + ); + + if (result.successful == true) { + DnsInfo data = dnsInfo!; + data.fallbackDns = List.from(value['fallback_dns']); + setDnsInfoData(data); + return result; + } + else { + return result; + } + } + + Future saveCacheCacheConfig(Map value) async { + final result = await _serversProvider!.apiClient2!.setDnsConfig( + data: value + ); + + if (result.successful == true) { + DnsInfo data = dnsInfo!; + data.cacheSize = value['cache_size']; + data.cacheTtlMin = value['cache_ttl_min']; + data.cacheTtlMax = value['cache_ttl_max']; + data.cacheOptimistic = value['cache_optimistic']; + setDnsInfoData(data); + return result; + } + else { + return result; + } + } + + Future saveDnsServerConfig(Map value) async { + final result = await _serversProvider!.apiClient2!.setDnsConfig( + data: value + ); + + void updateValue(dynamic parameter, dynamic value) { + if (value != null) { + parameter = value; + } + } + + if (result.successful == true) { + DnsInfo data = dnsInfo!; + updateValue(data.ratelimit, value['ratelimit']); + updateValue(data.ednsCsEnabled, value['edns_cs_enabled']); + updateValue(data.dnssecEnabled, value['dnssec_enabled']); + updateValue(data.disableIpv6, value['disable_ipv6']); + updateValue(data.blockingMode, value['blocking_mode']); + updateValue(data.blockingIpv4, value['blocking_ipv4']); + updateValue(data.blockingIpv6, value['blocking_ipv6']); + updateValue(data.blockedResponseTtl, value['blocked_response_ttl']); + updateValue(data.ratelimitSubnetLenIpv4, value['ratelimit_subnet_len_ipv4']); + updateValue(data.ratelimitSubnetLenIpv6, value['ratelimit_subnet_len_ipv6']); + updateValue(data.ratelimitWhitelist, value['ratelimit_whitelist']); + setDnsInfoData(data); + return result; + } + else { + return result; + } + } +} \ No newline at end of file diff --git a/lib/providers/filtering_provider.dart b/lib/providers/filtering_provider.dart new file mode 100644 index 0000000..9947233 --- /dev/null +++ b/lib/providers/filtering_provider.dart @@ -0,0 +1,476 @@ +import 'package:flutter/material.dart'; + +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; +import 'package:adguard_home_manager/models/blocked_services.dart'; +import 'package:adguard_home_manager/models/filtering.dart'; +import 'package:adguard_home_manager/providers/status_provider.dart'; + +enum FilteringListActions { edit, enable, disable } + +class FilteringProvider with ChangeNotifier { + StatusProvider? _statusProvider; + ServersProvider? _serversProvider; + + update(ServersProvider? servers, StatusProvider? status) { + _serversProvider = servers; + _statusProvider = status; + } + + LoadStatus _loadStatus = LoadStatus.loading; + Filtering? _filtering; + LoadStatus _blockedServicesLoadStatus = LoadStatus.loading; + BlockedServices? _blockedServicesList; + + LoadStatus get loadStatus { + return _loadStatus; + } + + Filtering? get filtering { + return _filtering; + } + + LoadStatus get blockedServicesLoadStatus { + return _blockedServicesLoadStatus; + } + + BlockedServices? get blockedServices { + return _blockedServicesList; + } + + void setFilteringData(Filtering data) { + _filtering = data; + notifyListeners(); + } + + void setFilteringLoadStatus(LoadStatus loadStatus, bool notify) { + _loadStatus = loadStatus; + if (notify == true) { + notifyListeners(); + } + } + + void setFilteringProtectionStatus(bool status, bool notify) { + _statusProvider!.setFilteringEnabledStatus(status); + _filtering!.enabled = status; + if (notify == true) notifyListeners(); + } + + void setFiltersUpdateFrequency(int frequency) { + if (_filtering != null) { + _filtering!.interval = frequency; + notifyListeners(); + } + } + + void setBlockedServices(List blockedServices) { + if (_filtering != null) { + _filtering!.blockedServices = blockedServices; + notifyListeners(); + } + } + + void setBlockedServiceListData(List data) { + _blockedServicesList = BlockedServices(services: data); + notifyListeners(); + } + + void setBlockedServicesListLoadStatus(LoadStatus status, bool notify) { + _blockedServicesLoadStatus = status; + if (notify == true) { + notifyListeners(); + } + } + + Future getBlockedServices({ + bool? showLoader + }) async { + _blockedServicesLoadStatus = LoadStatus.loading; + if (showLoader == true) notifyListeners(); + + final result = await _serversProvider!.apiClient2!.getBlockedServices(); + if (result.successful == true) { + _blockedServicesLoadStatus = LoadStatus.loaded; + _blockedServicesList = BlockedServices(services: result.content as List); + + notifyListeners(); + return true; + } + else { + if (showLoader == true) { + _blockedServicesLoadStatus = LoadStatus.error; + notifyListeners(); + } + return false; + } + } + + Future fetchFilters({ + bool? showLoading + }) async { + if (showLoading == true) { + _loadStatus = LoadStatus.loading; + } + + final result = await _serversProvider!.apiClient2!.getFiltering(); + if (result.successful == true) { + _filtering = result.content as Filtering; + _loadStatus = LoadStatus.loaded; + notifyListeners(); + return true; + } + else { + _loadStatus = LoadStatus.error; + notifyListeners(); + return false; + } + } + + Future> updateLists() async { + final result = await _serversProvider!.apiClient2!.updateLists(); + if (result.successful == true) { + final result2 = await _serversProvider!.apiClient2!.getFiltering(); + if (result2.successful == true) { + _filtering = result2.content as Filtering; + notifyListeners(); + return { + "success": true, + "data": result.content + }; + } + else { + notifyListeners(); + return { "success": false }; + } + } + else { + notifyListeners(); + return { "success": false }; + } + } + + Future enableDisableFiltering() async { + final newValue = !_statusProvider!.serverStatus!.filteringEnabled; + final result = await _serversProvider!.apiClient2!.updateFiltering( + enable: newValue + ); + if (result.successful == true) { + setFilteringProtectionStatus(newValue, false); + notifyListeners(); + return true; + } + else { + notifyListeners(); + return false; + } + } + + Future changeUpdateFrequency(int value) async { + final result = await _serversProvider!.apiClient2!.requestChangeUpdateFrequency( + data: { + "enabled": filtering!.enabled, + "interval": value + } + ); + if (result.successful == true) { + setFiltersUpdateFrequency(value); + return true; + } + else { + notifyListeners(); + return false; + } + } + + Future removeCustomRule(String rule) async { + final List newRules = filtering!.userRules.where((r) => r != rule).toList(); + + final result = await _serversProvider!.apiClient2!.setCustomRules(rules: newRules); + + if (result.successful == true) { + Filtering filteringData = filtering!; + filteringData.userRules = newRules; + _filtering = filteringData; + + notifyListeners(); + return true; + } + else { + notifyListeners(); + return false; + } + } + + Future deleteList({ + required String listUrl, + required String type + }) async { + final result1 = await _serversProvider!.apiClient2!.deleteFilterList( + data: { + "url": listUrl, + "whitelist": type == 'whitelist' ? true : false + } + ); + + if (result1.successful == true) { + final result2 = await _serversProvider!.apiClient2!.getFiltering(); + + if (result2.successful == true) { + _filtering = result2.content as Filtering; + notifyListeners(); + return true; + } + else { + notifyListeners(); + return false; + } + } + else { + notifyListeners(); + return false; + } + } + + Future updateList({ + required Filter list, + required String type, + required FilteringListActions action + }) async { + final result1 = await _serversProvider!.apiClient2!.updateFilterList( + data: { + "data": { + "enabled": action == FilteringListActions.disable || action == FilteringListActions.enable + ? !list.enabled + : list.enabled, + "name": list.name, + "url": list.url + }, + "url": list.url, + "whitelist": type == 'whitelist' ? true : false + } + ); + + if (result1.successful == true) { + final result2 = await _serversProvider!.apiClient2!.getFiltering(); + + if (result2.successful == true) { + _filtering = result2.content as Filtering; + notifyListeners(); + return true; + } + else { + notifyListeners(); + return false; + } + } + else { + notifyListeners(); + return false; + } + } + + Future addCustomRule(String rule) async { + final List newRules = filtering!.userRules; + newRules.add(rule); + + final result = await _serversProvider!.apiClient2!.setCustomRules(rules: newRules); + + if (result.successful == true) { + Filtering filteringData = filtering!; + filteringData.userRules = newRules; + _filtering = filteringData; + notifyListeners(); + return true; + } + else { + notifyListeners(); + return false; + } + } + + Future setCustomRules(List rules) async { + final newRules = rules.where((r) => r != " " && r != "").toList(); + final result = await _serversProvider!.apiClient2!.setCustomRules(rules: newRules); + + if (result.successful == true) { + Filtering filteringData = filtering!; + filteringData.userRules = newRules; + _filtering = filteringData; + notifyListeners(); + return true; + } + else { + notifyListeners(); + return false; + } + } + + Future> addList({required String name, required String url, required String type}) async { + final result1 = await _serversProvider!.apiClient2!.addFilteringList( + data: { + 'name': name, + 'url': url, + 'whitelist': type == 'whitelist' ? true : false + } + ); + + if (result1.successful == true) { + if (result1.content.toString().contains("OK")) { + final result2 = await _serversProvider!.apiClient2!.getFiltering(); + final items = result1.content.toString().split(' ')[1]; + + if (result2.successful == true) { + _filtering = result2.content as Filtering; + notifyListeners(); + return { + 'success': true, + 'data': items + }; + } + else { + notifyListeners(); + return { + 'success': false, + 'error': null + }; + } + } + else { + notifyListeners(); + return { + 'success': false, + 'error': null + }; + } + } + else if (result1.successful == false && result1.statusCode == 400 && result1.content.toString().contains("data is HTML, not plain text")) { + notifyListeners(); + return { + 'success': false, + 'error': 'invalid_url' + }; + } + else if (result1.successful == false && result1.statusCode == 400 && result1.content.toString().contains('url already exists')) { + notifyListeners(); + return { + 'success': false, + 'error': 'url_exists' + }; + } + else { + notifyListeners(); + return { + 'success': false, + 'error': null + }; + } + } + + Future loadBlockedServices({ + bool? showLoading + }) async { + if (showLoading == true) { + _blockedServicesLoadStatus = LoadStatus.loading; + } + + final result = await _serversProvider!.apiClient2!.getBlockedServices(); + if (result.successful == true) { + _blockedServicesList = BlockedServices(services: result.content as List); + _blockedServicesLoadStatus = LoadStatus.loaded; + + notifyListeners(); + return true; + } + else { + if (showLoading == true) _blockedServicesLoadStatus = LoadStatus.error; + notifyListeners(); + return false; + } + } + + Future updateBlockedServices(List values) async { + final result = await _serversProvider!.apiClient2!.setBlockedServices( + data: values + ); + + if (result.successful == true) { + setBlockedServices(values); + return true; + } + else { + notifyListeners(); + return false; + } + } + + Future> deleteMultipleLists({ + required List blacklists, + required List whitelists + }) async { + Future deleteList({ + required Filter list, + required bool isWhitelist, + }) async { + final result = await _serversProvider!.apiClient2!.deleteFilterList( + data: { + "url": list.url, + "whitelist": isWhitelist + } + ); + if (result.successful == true) { + return ProcessedList(list: list, successful: true); + } + else { + return ProcessedList(list: list, successful: false); + } + } + + final resultWhitelists = await Future.wait(whitelists.map((e) => deleteList(list: e, isWhitelist: true))); + final resultBlacklists = await Future.wait(blacklists.map((e) => deleteList(list: e, isWhitelist: false))); + + await fetchFilters(); + + return [ + ...resultWhitelists, + ...resultBlacklists, + ]; + } + + Future> enableDisableMultipleLists({ + required List blacklists, + required List whitelists + }) async { + Future enableDisableList({ + required Filter list, + required bool isWhitelist, + }) async { + final result = await _serversProvider!.apiClient2!.updateFilterList( + data: { + "data": { + "enabled": !list.enabled, + "name": list.name, + "url": list.url + }, + "url": list.url, + "whitelist": isWhitelist + } + ); + if (result.successful == true) { + return ProcessedList(list: list, successful: true); + } + else { + return ProcessedList(list: list, successful: false); + } + } + + final resultWhitelists = await Future.wait(whitelists.map((e) => enableDisableList(list: e, isWhitelist: true))); + final resultBlacklists = await Future.wait(blacklists.map((e) => enableDisableList(list: e, isWhitelist: false))); + + await fetchFilters(); + + return [ + ...resultWhitelists, + ...resultBlacklists, + ]; + } +} \ No newline at end of file diff --git a/lib/providers/live_logs_provider.dart b/lib/providers/live_logs_provider.dart new file mode 100644 index 0000000..044dd57 --- /dev/null +++ b/lib/providers/live_logs_provider.dart @@ -0,0 +1,51 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; + +import 'package:adguard_home_manager/models/logs.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; + +class LiveLogsProvider with ChangeNotifier { + ServersProvider? _serversProvider; + + update(ServersProvider? provider) { + _serversProvider = provider; + } + + bool _isDisposed = false; + + @override + void dispose() { + _isDisposed = true; + super.dispose(); + } + + List _logs = []; + + List get logs { + return _logs; + } + + DateTime? _lastTime; + + void startFetchLogs() { + _lastTime = DateTime.now(); + _fetchLogs(); + } + + void _fetchLogs() async { + if (_lastTime == null) return; + final result = await _serversProvider!.apiClient2!.getLogs( + count: 100 + ); + if (result.successful == false || result.content == null) return; + final valid = (result.content as LogsData).data.where((e) => e.time.isAfter(_lastTime!)); + _logs = [...valid, ..._logs]; + _lastTime = DateTime.now(); + notifyListeners(); + + await Future.delayed(const Duration(seconds: 2)); + if (_isDisposed == true) return; + _fetchLogs(); + } +} \ No newline at end of file diff --git a/lib/providers/logs_provider.dart b/lib/providers/logs_provider.dart index 7c2411b..cba46c0 100644 --- a/lib/providers/logs_provider.dart +++ b/lib/providers/logs_provider.dart @@ -1,30 +1,39 @@ -import 'package:adguard_home_manager/models/clients.dart'; import 'package:flutter/material.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; import 'package:adguard_home_manager/models/applied_filters.dart'; +import 'package:adguard_home_manager/models/clients.dart'; import 'package:adguard_home_manager/models/logs.dart'; class LogsProvider with ChangeNotifier { - int _loadStatus = 0; + ServersProvider? _serversProvider; + + update(ServersProvider? provider) { + _serversProvider = provider; + } + + LoadStatus _loadStatus = LoadStatus.loading; LogsData? _logsData; List? _clients; - int _clientsLoadStatus = 0; DateTime? _logsOlderThan; String _selectedResultStatus = 'all'; - String? _domainText; - List? _selectedClients; + String? _searchText; + List _selectedClients = []; int _logsQuantity = 100; int _offset = 0; + bool _isLoadingMore = false; + AppliedFiters _appliedFilters = AppliedFiters( selectedResultStatus: 'all', - domainText: null, - clients: null + searchText: null, + clients: [] ); - int get loadStatus { + LoadStatus get loadStatus { return _loadStatus; } @@ -44,8 +53,8 @@ class LogsProvider with ChangeNotifier { return _selectedResultStatus; } - String? get domainText { - return _domainText; + String? get searchText { + return _searchText; } int get logsQuantity { @@ -56,20 +65,19 @@ class LogsProvider with ChangeNotifier { return _offset; } - List? get selectedClients { + List get selectedClients { return _selectedClients; } - int get clientsLoadStatus { - return _clientsLoadStatus; - } - AppliedFiters get appliedFilters { return _appliedFilters; } + bool get isLoadingMore { + return _isLoadingMore; + } - void setLoadStatus(int value) { + void setLoadStatus(LoadStatus value) { _loadStatus = value; notifyListeners(); } @@ -83,11 +91,6 @@ class LogsProvider with ChangeNotifier { _clients = clients; notifyListeners(); } - - void setClientsLoadStatus(int status) { - _clientsLoadStatus = status; - notifyListeners(); - } void setLogsOlderThan(DateTime? value) { _logsOlderThan = value; @@ -98,7 +101,7 @@ class LogsProvider with ChangeNotifier { _logsOlderThan = null; _offset = 0; _selectedResultStatus = 'all'; - _domainText = null; + _searchText = null; notifyListeners(); } @@ -111,18 +114,24 @@ class LogsProvider with ChangeNotifier { _offset = value; } - void setSelectedResultStatus(String value) { + void setSelectedResultStatus({ + required String value, + bool? refetch + }) { _selectedResultStatus = value; notifyListeners(); + if (refetch = true) { + filterLogs(); + } } - void setDomainText(String? value) { - _domainText = value; + void setSearchText(String? value) { + _searchText = value; notifyListeners(); } void setSelectedClients(List? clients) { - _selectedClients = clients; + _selectedClients = clients ?? []; notifyListeners(); } @@ -130,4 +139,135 @@ class LogsProvider with ChangeNotifier { _appliedFilters = value; notifyListeners(); } + + void setIsLoadingMore(bool status) { + _isLoadingMore = status; + } + + Future fetchLogs({ + int? inOffset, + bool? loadingMore, + String? responseStatus, + String? searchText, + }) async { + int offst = inOffset ?? offset; + + String resStatus = responseStatus ?? _selectedResultStatus; + + if (loadingMore != null && loadingMore == true) { + _isLoadingMore = true; + notifyListeners(); + } + + final result = await _serversProvider!.apiClient2!.getLogs( + count: logsQuantity, + offset: offst, + olderThan: logsOlderThan, + responseStatus: resStatus, + search: _searchText + ); + + if (loadingMore != null && loadingMore == true) { + _isLoadingMore = false; + notifyListeners(); + } + + if (result.successful == true) { + _offset = inOffset != null ? inOffset+logsQuantity : offset+logsQuantity; + if (loadingMore != null && loadingMore == true && logsData != null) { + LogsData newLogsData = result.content; + newLogsData.data = [...logsData!.data, ...(result.content as LogsData).data]; + if (appliedFilters.clients.isNotEmpty) { + newLogsData.data = newLogsData.data.where( + (item) => appliedFilters.clients.contains(item.client) + ).toList(); + } + _logsData = newLogsData; + } + else { + LogsData newLogsData = result.content; + if (appliedFilters.clients.isNotEmpty) { + newLogsData.data = newLogsData.data.where( + (item) => appliedFilters.clients.contains(item.client) + ).toList(); + } + _logsData = newLogsData; + } + _loadStatus = LoadStatus.loaded; + notifyListeners(); + return true; + } + else { + _loadStatus = LoadStatus.error; + notifyListeners(); + return false; + } + } + + Future requestResetFilters() async { + _loadStatus = LoadStatus.loading; + notifyListeners(); + + resetFilters(); + + final result = await _serversProvider!.apiClient2!.getLogs( + count: logsQuantity + ); + + _appliedFilters = AppliedFiters( + selectedResultStatus: 'all', + searchText: null, + clients: [] + ); + + if (result.successful == true) { + _logsData = result.content as LogsData; + _loadStatus = LoadStatus.loaded; + notifyListeners(); + return true; + } + else { + _loadStatus = LoadStatus.error; + notifyListeners(); + return false; + } + } + + Future filterLogs() async { + _loadStatus = LoadStatus.loading; + notifyListeners(); + + setOffset(0); + + final result = await _serversProvider!.apiClient2!.getLogs( + count: logsQuantity, + olderThan: logsOlderThan, + responseStatus: selectedResultStatus, + search: searchText, + ); + + _appliedFilters = AppliedFiters( + selectedResultStatus: selectedResultStatus, + searchText: searchText, + clients: selectedClients + ); + + if (result.successful == true) { + LogsData newLogsData = result.content as LogsData; + if (appliedFilters.clients.isNotEmpty) { + newLogsData.data = newLogsData.data.where( + (item) => appliedFilters.clients.contains(item.client) + ).toList(); + } + _logsData = newLogsData; + _loadStatus = LoadStatus.loaded; + notifyListeners(); + return true; + } + else { + _loadStatus = LoadStatus.error; + notifyListeners(); + return false; + } + } } \ No newline at end of file diff --git a/lib/providers/rewrite_rules_provider.dart b/lib/providers/rewrite_rules_provider.dart new file mode 100644 index 0000000..ec8ea1d --- /dev/null +++ b/lib/providers/rewrite_rules_provider.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; + +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; +import 'package:adguard_home_manager/models/rewrite_rules.dart'; + +class RewriteRulesProvider with ChangeNotifier { + ServersProvider? _serversProvider; + + update(ServersProvider? provider) { + _serversProvider = provider; + } + + LoadStatus _loadStatus = LoadStatus.loading; + List? _rewriteRules; + + LoadStatus get loadStatus { + return _loadStatus; + } + + List? get rewriteRules { + return _rewriteRules; + } + + void setRewriteRulesData(List data) { + _rewriteRules = data; + notifyListeners(); + } + + void setRewriteRulesLoadStatus(LoadStatus status, bool notify) { + _loadStatus = status; + if (notify == true) { + notifyListeners(); + } + } + + Future addDnsRewrite(RewriteRules rule) async { + final result = await _serversProvider!.apiClient2!.addDnsRewriteRule( + data: { + "domain": rule.domain, + "answer": rule.answer + } + ); + + if (result.successful == true) { + List data = rewriteRules!; + data.add(rule); + setRewriteRulesData(data); + return true; + } + else { + notifyListeners(); + return false; + } + } + + Future editDnsRewrite(RewriteRules newRule, RewriteRules oldRule) async { + final result = await _serversProvider!.apiClient2!.updateRewriteRule( + body: { + "target": { + "answer": oldRule.answer, + "domain": oldRule.domain + }, + "update": { + "answer": newRule.answer, + "domain": newRule.domain + } + } + ); + + if (result.successful == true) { + List data = rewriteRules!; + final index = data.indexOf(oldRule); + data[index] = newRule; + setRewriteRulesData(data); + return true; + } + else { + notifyListeners(); + return false; + } + } + + Future deleteDnsRewrite(RewriteRules rule) async { + final result = await _serversProvider!.apiClient2!.deleteDnsRewriteRule( + data: { + "domain": rule.domain, + "answer": rule.answer + } + ); + + if (result.successful == true) { + List data = rewriteRules!; + data = data.where((item) => item.domain != rule.domain).toList(); + setRewriteRulesData(data); + return true; + } + else { + notifyListeners(); + return false; + } + } + + Future fetchRules({ + bool? showLoading + }) async { + if (showLoading == true) { + _loadStatus = LoadStatus.loading; + } + + final result = await _serversProvider!.apiClient2!.getDnsRewriteRules(); + + if (result.successful == true) { + _rewriteRules = result.content as List; + _loadStatus = LoadStatus.loaded; + notifyListeners(); + return true; + } + else { + if (showLoading == true) { + _loadStatus = LoadStatus.error; + notifyListeners(); + } + return false; + } + } +} \ No newline at end of file diff --git a/lib/providers/servers_provider.dart b/lib/providers/servers_provider.dart index 5b8a1d1..55d6363 100644 --- a/lib/providers/servers_provider.dart +++ b/lib/providers/servers_provider.dart @@ -1,61 +1,39 @@ -import 'package:adguard_home_manager/models/blocked_services.dart'; +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:sqflite/sqflite.dart'; -import 'package:adguard_home_manager/models/filtering.dart'; -import 'package:adguard_home_manager/models/dhcp.dart'; -import 'package:adguard_home_manager/models/dns_info.dart'; -import 'package:adguard_home_manager/models/rewrite_rules.dart'; -import 'package:adguard_home_manager/models/filtering_status.dart'; -import 'package:adguard_home_manager/models/clients_allowed_blocked.dart'; -import 'package:adguard_home_manager/models/clients.dart'; -import 'package:adguard_home_manager/models/server_status.dart'; +import 'package:adguard_home_manager/models/github_release.dart'; +import 'package:adguard_home_manager/services/api_client.dart'; +import 'package:adguard_home_manager/services/external_requests.dart'; import 'package:adguard_home_manager/models/server.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; +import 'package:adguard_home_manager/models/update_available.dart'; import 'package:adguard_home_manager/functions/conversions.dart'; +import 'package:adguard_home_manager/services/db/queries.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; class ServersProvider with ChangeNotifier { Database? _dbInstance; List _serversList = []; Server? _selectedServer; - final ServerStatus _serverStatus = ServerStatus( - loadStatus: 0, // 0 = loading, 1 = loaded, 2 = error - data: null - ); // serverStatus != null means server is connected - List _protectionsManagementProcess = []; // protections that are currenty being enabled or disabled + // ApiClient? _apiClient; + ApiClientV2? _apiClient2; - final Clients _clients = Clients( - loadStatus: 0, // 0 = loading, 1 = loaded, 2 = error - data: null + bool _updatingServer = false; + + final UpdateAvailable _updateAvailable = UpdateAvailable( + loadStatus: LoadStatus.loading, + data: null, ); - final Filtering _filtering = Filtering( - loadStatus: 0, // 0 = loading, 1 = loaded, 2 = error - data: null - ); + // ApiClient? get apiClient { + // return _apiClient; + // } - final DhcpModel _dhcp = DhcpModel( - loadStatus: 0, // 0 = loading, 1 = loaded, 2 = error - data: null - ); - - final RewriteRules _rewriteRules = RewriteRules( - loadStatus: 0, // 0 = loading, 1 = loaded, 2 = error - data: null - ); - - final DnsInfo _dnsInfo = DnsInfo( - loadStatus: 0, // 0 = loading, 1 = loaded, 2 = error - data: null - ); - - final BlockedServices _blockedServicesList = BlockedServices( - loadStatus: 0, - services: null - ); - - FilteringStatus? _filteringStatus; + ApiClientV2? get apiClient2 { + return _apiClient2; + } List get serversList { return _serversList; @@ -65,40 +43,12 @@ class ServersProvider with ChangeNotifier { return _selectedServer; } - ServerStatus get serverStatus { - return _serverStatus; + UpdateAvailable get updateAvailable { + return _updateAvailable; } - List get protectionsManagementProcess { - return _protectionsManagementProcess; - } - - Clients get clients { - return _clients; - } - - FilteringStatus? get filteringStatus { - return _filteringStatus; - } - - Filtering get filtering { - return _filtering; - } - - DhcpModel get dhcp { - return _dhcp; - } - - RewriteRules get rewriteRules { - return _rewriteRules; - } - - DnsInfo get dnsInfo { - return _dnsInfo; - } - - BlockedServices get blockedServicesList { - return _blockedServicesList; + bool get updatingServer { + return _updatingServer; } void setDbInstance(Database db) { @@ -110,121 +60,40 @@ class ServersProvider with ChangeNotifier { notifyListeners(); } - void setSelectedServer(Server server) { + void setSelectedServer(Server? server) { _selectedServer = server; notifyListeners(); } - void setServerStatusData(ServerStatusData data) { - _serverStatus.data = data; - notifyListeners(); - } - - void setServerStatusLoad(int status) { - _serverStatus.loadStatus = status; - notifyListeners(); - } - - void setClientsLoadStatus(int status, bool notify) { - _clients.loadStatus = status; + void setUpdateAvailableLoadStatus(LoadStatus status, bool notify) { + _updateAvailable.loadStatus = status; if (notify == true) { notifyListeners(); } } - void setClientsData(ClientsData data) { - _clients.data = data; + void setUpdateAvailableData(UpdateAvailableData data) { + _updateAvailable.data = data; notifyListeners(); } - void setAllowedDisallowedClientsBlockedDomains(ClientsAllowedBlocked data) { - _clients.data?.clientsAllowedBlocked = data; + // void setApiClient(ApiClient client) { + // _apiClient = client; + // notifyListeners(); + // } + + void setApiClient2(ApiClientV2 client) { + _apiClient2 = client; notifyListeners(); } - void setFilteringStatus(FilteringStatus status) { - _filteringStatus = status; + void setUpdatingServer(bool status) { + _updatingServer = status; notifyListeners(); } - void setFilteringData(FilteringData data) { - _filtering.data = data; - notifyListeners(); - } - - void setFilteringLoadStatus(int loadStatus, bool notify) { - _filtering.loadStatus = loadStatus; - if (notify == true) { - notifyListeners(); - } - } - - void setFilteringProtectionStatus(bool status) { - _serverStatus.data!.filteringEnabled = status; - _filtering.data!.enabled = status; - notifyListeners(); - } - - void setFiltersUpdateFrequency(int frequency) { - _filtering.data!.interval = frequency; - notifyListeners(); - } - - void setBlockedServices(List blockedServices) { - _filtering.data!.blockedServices = blockedServices; - notifyListeners(); - } - - void setDhcpData(DhcpData data) { - _dhcp.data = data; - notifyListeners(); - } - - void setDhcpLoadStatus(int status, bool notify) { - _dhcp.loadStatus = status; - if (notify == true) { - notifyListeners(); - } - } - - void setRewriteRulesData(List data) { - _rewriteRules.data = data; - notifyListeners(); - } - - void setRewriteRulesLoadStatus(int status, bool notify) { - _rewriteRules.loadStatus = status; - if (notify == true) { - notifyListeners(); - } - } - - void setDnsInfoData(DnsInfoData data) { - _dnsInfo.data = data; - notifyListeners(); - } - - void setDnsInfoLoadStatus(int status, bool notify) { - _dnsInfo.loadStatus = status; - if (notify == true) { - notifyListeners(); - } - } - - void setBlockedServiceListData(List data) { - _blockedServicesList.services = data; - notifyListeners(); - } - - void setBlockedServicesListLoadStatus(int status, bool notify) { - _blockedServicesList.loadStatus = status; - if (notify == true) { - notifyListeners(); - } - } - Future createServer(Server server) async { - final saved = await saveServerIntoDb(server); + final saved = await saveServerQuery(_dbInstance!, server); if (saved == null) { if (server.defaultServer == true) { final defaultServer = await setDefaultServer(server); @@ -249,7 +118,7 @@ class ServersProvider with ChangeNotifier { } Future setDefaultServer(Server server) async { - final updated = await setDefaultServerDb(server.id); + final updated = await setDefaultServerQuery(_dbInstance!, server.id); if (updated == null) { List newServers = _serversList.map((s) { if (s.id == server.id) { @@ -271,7 +140,7 @@ class ServersProvider with ChangeNotifier { } Future editServer(Server server) async { - final result = await editServerDb(server); + final result = await editServerQuery(_dbInstance!, server); if (result == null) { List newServers = _serversList.map((s) { if (s.id == server.id) { @@ -282,6 +151,12 @@ class ServersProvider with ChangeNotifier { } }).toList(); _serversList = newServers; + + if (selectedServer != null &&server.id == selectedServer!.id) { + // _apiClient = ApiClient(server: server); + _apiClient2 = ApiClientV2(server: server); + } + notifyListeners(); return null; } @@ -291,9 +166,10 @@ class ServersProvider with ChangeNotifier { } Future removeServer(Server server) async { - final result = await removeFromDb(server.id); + final result = await removeServerQuery(_dbInstance!, server.id); if (result == true) { _selectedServer = null; + // _apiClient = null; List newServers = _serversList.where((s) => s.id != server.id).toList(); _serversList = newServers; notifyListeners(); @@ -304,163 +180,40 @@ class ServersProvider with ChangeNotifier { } } - Future updateBlocking(Server server, String block, bool newStatus) async { - switch (block) { - case 'general': - _protectionsManagementProcess.add('general'); - notifyListeners(); - - final result = await updateGeneralProtection(server, newStatus); - - _protectionsManagementProcess = _protectionsManagementProcess.where((e) => e != 'general').toList(); - - if (result['result'] == 'success') { - _serverStatus.data!.generalEnabled = newStatus; - notifyListeners(); - return null; - } - else { - notifyListeners(); - return result['log']; - } - - case 'filtering': - _protectionsManagementProcess.add('filtering'); - notifyListeners(); - - final result = await updateFiltering(server, newStatus); - - _protectionsManagementProcess = _protectionsManagementProcess.where((e) => e != 'filtering').toList(); - - if (result['result'] == 'success') { - _serverStatus.data!.filteringEnabled = newStatus; - notifyListeners(); - return null; - } - else { - - notifyListeners(); - return result['log']; - } - - case 'safeSearch': - _protectionsManagementProcess.add('safeSearch'); - notifyListeners(); - - final result = await updateSafeSearch(server, newStatus); - - _protectionsManagementProcess = _protectionsManagementProcess.where((e) => e != 'safeSearch').toList(); - - if (result['result'] == 'success') { - _serverStatus.data!.safeSearchEnabled = newStatus; - notifyListeners(); - return null; - } - else { - notifyListeners(); - return result['log']; - } - - case 'safeBrowsing': - _protectionsManagementProcess.add('safeBrowsing'); - notifyListeners(); - - final result = await updateSafeBrowsing(server, newStatus); - - _protectionsManagementProcess = _protectionsManagementProcess.where((e) => e != 'safeBrowsing').toList(); - - if (result['result'] == 'success') { - _serverStatus.data!.safeBrowsingEnabled = newStatus; - notifyListeners(); - return null; - } - else { - notifyListeners(); - return result['log']; - } - - case 'parentalControl': - _protectionsManagementProcess.add('parentalControl'); - notifyListeners(); - - final result = await updateParentalControl(server, newStatus); - - _protectionsManagementProcess = _protectionsManagementProcess.where((e) => e != 'parentalControl').toList(); - - if (result['result'] == 'success') { - _serverStatus.data!.parentalControlEnabled = newStatus; - notifyListeners(); - return null; - } - else { - notifyListeners(); - return result['log']; - } - - default: - return false; + void checkServerUpdatesAvailable({ + required Server server, + ApiClientV2? apiClient + }) async { + final client = apiClient ?? _apiClient2; + setUpdateAvailableLoadStatus(LoadStatus.loading, true); + final result = await client!.checkServerUpdates(); + if (result.successful == true) { + UpdateAvailableData data = UpdateAvailableData.fromJson(result.content); + final gitHubResult = await ExternalRequests.getReleaseData(releaseTag: data.newVersion ?? data.currentVersion); + if (gitHubResult.successful == true) { + data.changelog = (gitHubResult.content as GitHubRelease).body; + } + setUpdateAvailableData(data); + setUpdateAvailableLoadStatus(LoadStatus.loaded, true); + } + else { + setUpdateAvailableLoadStatus(LoadStatus.error, true); } } - Future saveServerIntoDb(Server server) async { - try { - return await _dbInstance!.transaction((txn) async { - await txn.rawInsert( - 'INSERT INTO servers (id, name, connectionMethod, domain, path, port, user, password, defaultServer, authToken, runningOnHa) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', - [server.id, server.name, server.connectionMethod, server.domain, server.path, server.port, server.user, server.password, server.defaultServer, server.authToken, convertFromBoolToInt(server.runningOnHa)] - ); - return null; - }); - } catch (e) { - return e; + Future initializateServer(Server server, /*ApiClient apiClient, */ ApiClientV2 apiClient2) async { + final serverStatus = await _apiClient2!.getServerStatus(); + if (serverStatus.successful == true) { + checkServerUpdatesAvailable( // Do not await + server: server, + apiClient: apiClient2 + ); } } - Future editServerDb(Server server) async { - try { - return await _dbInstance!.transaction((txn) async { - await txn.rawUpdate( - 'UPDATE servers SET name = ?, connectionMethod = ?, domain = ?, path = ?, port = ?, user = ?, password = ?, authToken = ?, runningOnHa = ? WHERE id = "${server.id}"', - [server.name, server.connectionMethod, server.domain, server.path, server.port, server.user, server.password, server.authToken, server.runningOnHa] - ); - return null; - }); - } catch (e) { - return e; - } - } - - Future removeFromDb(String id) async { - try { - return await _dbInstance!.transaction((txn) async { - await txn.rawDelete( - 'DELETE FROM servers WHERE id = "$id"', - ); - return true; - }); - } catch (e) { - return false; - } - } - - Future setDefaultServerDb(String id) async { - try { - return await _dbInstance!.transaction((txn) async { - await txn.rawUpdate( - 'UPDATE servers SET defaultServer = 0 WHERE defaultServer = 1', - ); - await txn.rawUpdate( - 'UPDATE servers SET defaultServer = 1 WHERE id = "$id"', - ); - return null; - }); - } catch (e) { - return e; - } - } - - void saveFromDb(List>? data) async { + Future saveFromDb(List>? data) async { if (data != null) { + Server? defaultServer; for (var server in data) { final Server serverObj = Server( id: server['id'], @@ -477,19 +230,59 @@ class ServersProvider with ChangeNotifier { ); _serversList.add(serverObj); if (convertFromIntToBool(server['defaultServer']) == true) { - _selectedServer = serverObj; - _serverStatus.loadStatus = 0; - final serverStatus = await getServerStatus(serverObj); - if (serverStatus['result'] == 'success') { - _serverStatus.data = serverStatus['data']; - _serverStatus.loadStatus = 1; - } - else { - _serverStatus.loadStatus = 2; - } + defaultServer = serverObj; } } + + notifyListeners(); + + if (defaultServer != null) { + _selectedServer = defaultServer; + // final client = ApiClient(server: defaultServer); + final client2 = ApiClientV2(server: defaultServer); + // _apiClient = client; + _apiClient2 = client2; + initializateServer(defaultServer, /*client,*/ client2); + } + } + else { + notifyListeners(); + return null; + } + } + + void recheckPeriodServerUpdated() { + if (_selectedServer != null) { + setUpdatingServer(true); + Server server = _selectedServer!; + Timer.periodic( + const Duration(seconds: 2), + (timer) async { + if (_selectedServer != null && _selectedServer == server) { + final result = await _apiClient2!.checkServerUpdates(); + if (result.successful == true) { + UpdateAvailableData data = UpdateAvailableData.fromJsonUpdate(result.content); + if (data.currentVersion == data.newVersion) { + final gitHubResult = await ExternalRequests.getReleaseData(releaseTag: data.newVersion ?? data.currentVersion); + if (gitHubResult.successful == true) { + data.changelog = (gitHubResult.content as GitHubRelease).body; + } + setUpdateAvailableData(data); + timer.cancel(); + setUpdatingServer(false); + } + } + else { + timer.cancel(); + setUpdatingServer(false); + } + } + else { + timer.cancel(); + setUpdatingServer(false); + } + } + ); } - notifyListeners(); } } \ No newline at end of file diff --git a/lib/providers/status_provider.dart b/lib/providers/status_provider.dart new file mode 100644 index 0000000..527749f --- /dev/null +++ b/lib/providers/status_provider.dart @@ -0,0 +1,342 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'package:adguard_home_manager/widgets/add_server/unsupported_version_modal.dart'; + +import 'package:adguard_home_manager/config/globals.dart'; +import 'package:adguard_home_manager/config/minimum_server_version.dart'; +import 'package:adguard_home_manager/functions/compare_versions.dart'; +import 'package:adguard_home_manager/models/server_status.dart'; +import 'package:adguard_home_manager/models/filtering_status.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; +import 'package:adguard_home_manager/functions/time_server_disabled.dart'; + +class StatusProvider with ChangeNotifier { + ServersProvider? _serversProvider; + + update(ServersProvider? provider) { + _serversProvider = provider; + } + + LoadStatus _loadStatus = LoadStatus.loading; + ServerStatus? _serverStatus; // serverStatus != null means server is connected + List _protectionsManagementProcess = []; // protections that are currenty being enabled or disabled + FilteringStatus? _filteringStatus; + + // Countdown + DateTime? _currentDeadline; + Timer? _countdown; + int _remaining = 0; + + LoadStatus get loadStatus { + return _loadStatus; + } + + ServerStatus? get serverStatus { + return _serverStatus; + } + + List get protectionsManagementProcess { + return _protectionsManagementProcess; + } + + FilteringStatus? get filteringStatus { + return _filteringStatus; + } + + int get remainingTime { + return _remaining; + } + + DateTime? get currentDeadline { + return _currentDeadline; + } + + void setServerStatusData({ + required ServerStatus data, + }) { + _serverStatus = data; + if ( + (_countdown == null ||( _countdown != null && _countdown!.isActive == false)) && + data.disabledUntil != null + ) { + startCountdown(data.disabledUntil!); + } + notifyListeners(); + } + + void setServerStatusLoad(LoadStatus status) { + _loadStatus = status; + notifyListeners(); + } + + void setFilteringStatus(FilteringStatus status) { + _filteringStatus = status; + notifyListeners(); + } + + void startCountdown(DateTime deadline) { + stopCountdown(); + + _currentDeadline = deadline; + _remaining = deadline.difference(DateTime.now()).inSeconds+1; + + _countdown = Timer.periodic( + const Duration(seconds: 1), + (Timer timer) async { + if (_remaining == 0) { + timer.cancel(); + notifyListeners(); + getServerStatus(); + } + else { + _remaining = _remaining - 1; + notifyListeners(); + } + }, + ); + } + + void stopCountdown() { + if (_countdown != null && _countdown!.isActive) { + _countdown!.cancel(); + _countdown = null; + _remaining = 0; + _currentDeadline = null; + } + } + + Future updateBlocking({ + required String block, + required bool newStatus, + int? time + }) async { + switch (block) { + case 'general': + _protectionsManagementProcess.add('general'); + notifyListeners(); + + final result = await _serversProvider!.apiClient2!.updateGeneralProtection( + enable: newStatus, + time: time + ); + + _protectionsManagementProcess = _protectionsManagementProcess.where((e) => e != 'general').toList(); + + if (result.successful == true) { + _serverStatus!.generalEnabled = newStatus; + if (time != null) { + final deadline = generateTimeDeadline(time); + _serverStatus!.timeGeneralDisabled = time; + _serverStatus!.disabledUntil = deadline; + startCountdown(deadline); + } + else { + _serverStatus!.timeGeneralDisabled = 0; + _serverStatus!.disabledUntil = null; + stopCountdown(); + } + notifyListeners(); + return true; + } + else { + return false; + } + + case 'filtering': + _protectionsManagementProcess.add('filtering'); + notifyListeners(); + + final result = await _serversProvider!.apiClient2!.updateFiltering( + enable: newStatus, + ); + + _protectionsManagementProcess = _protectionsManagementProcess.where((e) => e != 'filtering').toList(); + + if (result.successful == true) { + _serverStatus!.filteringEnabled = newStatus; + notifyListeners(); + return true; + } + else { + return false; + } + + case 'safeSearch': + _protectionsManagementProcess.add('safeSearch'); + notifyListeners(); + + final result = await _serversProvider!.apiClient2!.updateSafeSearchSettings(body: { 'enabled': newStatus }); + + _protectionsManagementProcess = _protectionsManagementProcess.where((e) => e != 'safeSearch').toList(); + + if (result.successful == true) { + _serverStatus!.safeSearchEnabled = newStatus; + notifyListeners(); + return true; + } + else { + return false; + } + + case 'safeBrowsing': + _protectionsManagementProcess.add('safeBrowsing'); + notifyListeners(); + + final result = await _serversProvider!.apiClient2!.updateSafeBrowsing(enable: newStatus); + + _protectionsManagementProcess = _protectionsManagementProcess.where((e) => e != 'safeBrowsing').toList(); + + if (result.successful == true) { + _serverStatus!.safeBrowsingEnabled = newStatus; + notifyListeners(); + return true; + } + else { + return false; + } + + case 'parentalControl': + _protectionsManagementProcess.add('parentalControl'); + notifyListeners(); + + final result = await _serversProvider!.apiClient2!.updateParentalControl(enable: newStatus); + + _protectionsManagementProcess = _protectionsManagementProcess.where((e) => e != 'parentalControl').toList(); + + if (result.successful == true) { + _serverStatus!.parentalControlEnabled = newStatus; + notifyListeners(); + return true; + } + else { + return false; + } + + default: + return false; + } + } + + void setFilteringEnabledStatus(bool status) { + _serverStatus!.filteringEnabled = status; + } + + Future getFilteringRules() async { + final result = await _serversProvider!.apiClient2!.getFilteringRules(); + if (result.successful == true) { + _filteringStatus = result.content as FilteringStatus; + notifyListeners(); + return true; + } + else { + return false; + } + } + + Future getServerStatus({ + bool? withLoadingIndicator = true, + bool? overrideCheckServerVersion + }) async { + if (withLoadingIndicator == true) { + _loadStatus = LoadStatus.loading; + } + + final result = await _serversProvider!.apiClient2!.getServerStatus(); + if (result.successful == true) { + final status = result.content as ServerStatus; + setServerStatusData( + data: status + ); + _loadStatus = LoadStatus.loaded; + notifyListeners(); + + // Check server version and launch modal if not valid + final validVersion = serverVersionIsAhead( + currentVersion: status.serverVersion, + referenceVersion: MinimumServerVersion.stable, + referenceVersionBeta: MinimumServerVersion.beta + ); + if (validVersion == false && overrideCheckServerVersion != true) { + showDialog( + context: globalNavigatorKey.currentContext!, + builder: (ctx) => UnsupportedVersionModal( + serverVersion: status.serverVersion, + onClose: () { + _serversProvider!.setSelectedServer(null); + } + ) + ); + } + return true; + } + else { + if (withLoadingIndicator == true) _loadStatus = LoadStatus.error; + notifyListeners(); + return false; + } + } + + Future blockUnblockDomain({ + required String domain, + required String newStatus + }) async { + if (_serverStatus == null) return false; + + final rules = await _serversProvider!.apiClient2!.getFilteringRules(); + + if (rules.successful == true) { + FilteringStatus oldStatus = _serverStatus!.filteringStatus; + + List newRules = (rules.content as FilteringStatus).userRules.where((d) => !d.contains(domain)).toList(); + if (newStatus == 'block') { + newRules.add("||$domain^"); + } + else if (newStatus == 'unblock') { + newRules.add("@@||$domain^"); + } + FilteringStatus newObj = _serverStatus!.filteringStatus; + newObj.userRules = newRules; + _filteringStatus = newObj; + + final result = await _serversProvider!.apiClient2!.postFilteringRules(data: {'rules': newRules}); + + if (result.successful == true) { + return true; + } + else { + _filteringStatus = oldStatus; + return false; + } + } + else { + return false; + } + } + + Future updateSafeSearchConfig(Map status) async { + final result = await _serversProvider!.apiClient2!.updateSafeSearchSettings( + body: status + ); + + if (result.successful == true) { + ServerStatus data = serverStatus!; + data.safeSearchEnabled = status['enabled'] ?? false; + data.safeSeachBing = status['bing'] ?? false; + data.safeSearchDuckduckgo = status['duckduckgo'] ?? false; + data.safeSearchGoogle = status['google'] ?? false; + data.safeSearchPixabay = status['pixabay'] ?? false; + data.safeSearchYandex = status['yandex'] ?? false; + data.safeSearchYoutube = status['youtube'] ?? false; + + setServerStatusData(data: data); + return true; + } + else { + notifyListeners(); + return false; + } + } +} \ No newline at end of file diff --git a/lib/screens/clients/added_list.dart b/lib/screens/clients/added_list.dart index 76d5a03..2cceaab 100644 --- a/lib/screens/clients/added_list.dart +++ b/lib/screens/clients/added_list.dart @@ -6,31 +6,36 @@ import 'package:flutter/rendering.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:adguard_home_manager/screens/clients/client_screen.dart'; -import 'package:adguard_home_manager/screens/clients/remove_client_modal.dart'; +import 'package:adguard_home_manager/screens/clients/clients.dart'; +import 'package:adguard_home_manager/screens/clients/client/client_screen_functions.dart'; +import 'package:adguard_home_manager/screens/clients/client/added_client_tile.dart'; +import 'package:adguard_home_manager/screens/clients/client/remove_client_modal.dart'; import 'package:adguard_home_manager/screens/clients/fab.dart'; -import 'package:adguard_home_manager/screens/clients/options_modal.dart'; +import 'package:adguard_home_manager/widgets/tab_content_list.dart'; import 'package:adguard_home_manager/functions/snackbar.dart'; +import 'package:adguard_home_manager/providers/status_provider.dart'; +import 'package:adguard_home_manager/providers/clients_provider.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; import 'package:adguard_home_manager/classes/process_modal.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; import 'package:adguard_home_manager/models/clients.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; class AddedList extends StatefulWidget { final ScrollController scrollController; - final int loadStatus; final List data; - final Future Function() fetchClients; + final void Function(Client) onClientSelected; + final Client? selectedClient; + final bool splitView; const AddedList({ - Key? key, + super.key, required this.scrollController, - required this.loadStatus, required this.data, - required this.fetchClients - }) : super(key: key); + required this.onClientSelected, + this.selectedClient, + required this.splitView + }); @override State createState() => _AddedListState(); @@ -62,44 +67,29 @@ class _AddedListState extends State { @override Widget build(BuildContext context) { - final serversProvider = Provider.of(context); + final statusProvider = Provider.of(context); + final clientsProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + void confirmEditClient(Client client) async { - ProcessModal processModal = ProcessModal(context: context); - processModal.open(AppLocalizations.of(context)!.addingClient); + ProcessModal processModal = ProcessModal(); + processModal.open(AppLocalizations.of(context)!.savingChanges); - final result = await postUpdateClient(server: serversProvider.selectedServer!, data: { - 'name': client.name, - 'data': client.toJson() - }); + final result = await clientsProvider.editClient(client); processModal.close(); - if (result['result'] == 'success') { - ClientsData clientsData = serversProvider.clients.data!; - clientsData.clients = clientsData.clients.map((e) { - if (e.name == client.name) { - return client; - } - else { - return e; - } - }).toList(); - serversProvider.setClientsData(clientsData); - - showSnacbkar( - context: context, + if (result == true) { + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.clientUpdatedSuccessfully, color: Colors.green ); } else { - appConfigProvider.addLog(result['log']); - - showSnacbkar( - context: context, + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.clientNotUpdated, color: Colors.red @@ -108,46 +98,40 @@ class _AddedListState extends State { } void deleteClient(Client client) async { - ProcessModal processModal = ProcessModal(context: context); + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.removingClient); - final result = await postDeleteClient(server: serversProvider.selectedServer!, name: client.name); + final result = await clientsProvider.deleteClient(client); processModal.close(); - if (result['result'] == 'success') { - ClientsData clientsData = serversProvider.clients.data!; - clientsData.clients = clientsData.clients.where((c) => c.name != client.name).toList(); - serversProvider.setClientsData(clientsData); - - showSnacbkar( - context: context, + if (result == true) { + if (widget.splitView == true) { + Navigator.of(clientsNavigatorKey.currentContext!).popUntil((route) => false); + } + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.clientDeletedSuccessfully, color: Colors.green ); } else { - appConfigProvider.addLog(result['log']); - - showSnacbkar( - context: context, + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.clientNotDeleted, color: Colors.red ); } - } + } void openClientModal(Client client) { - Navigator.push(context, MaterialPageRoute( - fullscreenDialog: true, - builder: (BuildContext context) => ClientScreen( - onConfirm: confirmEditClient, - onDelete: deleteClient, - client: client, - ) - )); + openClientFormModal( + context: context, + width: width, + client: client, + onConfirm: confirmEditClient, + onDelete: deleteClient + ); } void openDeleteModal(Client client) { @@ -158,191 +142,100 @@ class _AddedListState extends State { ) ); } + final clientsDisplay = clientsProvider.searchTermClients != null && clientsProvider.searchTermClients != "" + ? widget.data.where( + (c) => c.name.toLowerCase().contains(clientsProvider.searchTermClients.toString()) || c.ids.where((id) => id.contains(clientsProvider.searchTermClients.toString())).isNotEmpty + ).toList() + : widget.data; - void openOptionsModal(Client client) { - showModal( - context: context, - builder: (ctx) => OptionsModal( - onDelete: () => openDeleteModal(client), - onEdit: () => openClientModal(client), - ) - ); - } - - switch (widget.loadStatus) { - case 0: - return SizedBox( - width: double.maxFinite, - height: MediaQuery.of(context).size.height-171, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const CircularProgressIndicator(), - const SizedBox(height: 30), - Text( - AppLocalizations.of(context)!.loadingStatus, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ) - ], - ), - ); - - case 1: - return Stack( + return CustomTabContentList( + listPadding: widget.splitView == true + ? const EdgeInsets.only(top: 8) + : null, + loadingGenerator: () => SizedBox( + width: double.maxFinite, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - if (widget.data.isNotEmpty) ListView.builder( - padding: const EdgeInsets.only(top: 0), - itemCount: widget.data.length, - itemBuilder: (context, index) => ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - isThreeLine: true, - onLongPress: () => openOptionsModal(widget.data[index]), - onTap: () => openClientModal(widget.data[index]), - title: Padding( - padding: const EdgeInsets.only(bottom: 5), - child: Text( - widget.data[index].name, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.normal, - color: Theme.of(context).colorScheme.onSurface - ), - ), - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.data[index].ids.toString().replaceAll(RegExp(r'^\[|\]$'), ''), - style: TextStyle( - color: Theme.of(context).listTileTheme.textColor - ), - ), - const SizedBox(height: 7), - Row( - children: [ - Icon( - Icons.filter_list_rounded, - size: 19, - color: widget.data[index].filteringEnabled == true - ? appConfigProvider.useThemeColorForStatus == true - ? Theme.of(context).colorScheme.primary - : Colors.green - : appConfigProvider.useThemeColorForStatus == true - ? Colors.grey - : Colors.red, - ), - const SizedBox(width: 10), - Icon( - Icons.vpn_lock_rounded, - size: 18, - color: widget.data[index].safebrowsingEnabled == true - ? appConfigProvider.useThemeColorForStatus == true - ? Theme.of(context).colorScheme.primary - : Colors.green - : appConfigProvider.useThemeColorForStatus == true - ? Colors.grey - : Colors.red, - ), - const SizedBox(width: 10), - Icon( - Icons.block, - size: 18, - color: widget.data[index].parentalEnabled == true - ? appConfigProvider.useThemeColorForStatus == true - ? Theme.of(context).colorScheme.primary - : Colors.green - : appConfigProvider.useThemeColorForStatus == true - ? Colors.grey - : Colors.red, - ), - const SizedBox(width: 10), - Icon( - Icons.search_rounded, - size: 19, - color: widget.data[index].safesearchEnabled == true - ? appConfigProvider.useThemeColorForStatus == true - ? Theme.of(context).colorScheme.primary - : Colors.green - : appConfigProvider.useThemeColorForStatus == true - ? Colors.grey - : Colors.red, - ) - ], - ) - ], - ), - ) - ), - if (widget.data.isEmpty) SizedBox( - width: double.maxFinite, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - AppLocalizations.of(context)!.noClientsList, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 30), - TextButton.icon( - onPressed: widget.fetchClients, - icon: const Icon(Icons.refresh_rounded), - label: Text(AppLocalizations.of(context)!.refresh), - ) - ], + const CircularProgressIndicator(), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.loadingStatus, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, ), - ), - AnimatedPositioned( - duration: const Duration(milliseconds: 100), - curve: Curves.easeInOut, - bottom: isVisible ? - appConfigProvider.showingSnackbar - ? 70 : 20 - : -70, - right: 20, - child: const ClientsFab(tab: 1), ) ], - ); - - case 2: - return SizedBox( - width: double.maxFinite, - height: MediaQuery.of(context).size.height-171, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon( - Icons.error, - color: Colors.red, - size: 50, - ), - const SizedBox(height: 30), - Text( - AppLocalizations.of(context)!.errorLoadServerStatus, + ), + ), + itemsCount: clientsDisplay.length, + contentWidget: (index) => AddedClientTile( + selectedClient: widget.selectedClient, + client: clientsDisplay[index], + onTap: widget.onClientSelected, + onEdit: statusProvider.serverStatus != null + ? (c) => openClientModal(c) + : null, + onDelete: openDeleteModal, + splitView: widget.splitView, + ), + noData: SizedBox( + width: double.maxFinite, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + AppLocalizations.of(context)!.noClientsList, textAlign: TextAlign.center, style: TextStyle( - fontSize: 22, + fontSize: 24, color: Theme.of(context).colorScheme.onSurfaceVariant, ), - ) - ], - ), - ); - - default: - return const SizedBox(); - } - + ), + ), + const SizedBox(height: 30), + TextButton.icon( + onPressed: () => clientsProvider.fetchClients(updateLoading: true), + icon: const Icon(Icons.refresh_rounded), + label: Text(AppLocalizations.of(context)!.refresh), + ) + ], + ), + ), + errorGenerator: () => SizedBox( + width: double.maxFinite, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon( + Icons.error, + color: Colors.red, + size: 50, + ), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.errorLoadServerStatus, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ), + loadStatus: statusProvider.loadStatus == LoadStatus.loading || clientsProvider.loadStatus == LoadStatus.loading + ? LoadStatus.loading + : clientsProvider.loadStatus, + onRefresh: () => clientsProvider.fetchClients(updateLoading: false), + fab: const ClientsFab(), + fabVisible: isVisible, + ); } } \ No newline at end of file diff --git a/lib/screens/clients/client/active_client_tile.dart b/lib/screens/clients/client/active_client_tile.dart new file mode 100644 index 0000000..7a7a825 --- /dev/null +++ b/lib/screens/clients/client/active_client_tile.dart @@ -0,0 +1,132 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/widgets/options_menu.dart'; +import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; + +import 'package:adguard_home_manager/models/menu_option.dart'; +import 'package:adguard_home_manager/functions/copy_clipboard.dart'; +import 'package:adguard_home_manager/models/clients.dart'; + +class ActiveClientTile extends StatelessWidget { + final AutoClient client; + final void Function(AutoClient) onTap; + final bool splitView; + final AutoClient? selectedClient; + + const ActiveClientTile({ + super.key, + required this.client, + required this.onTap, + required this.splitView, + this.selectedClient + }); + + @override + Widget build(BuildContext context) { + if (splitView == true) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: OptionsMenu( + options: (_) => [ + MenuOption( + icon: Icons.copy_rounded, + title: AppLocalizations.of(context)!.copyClipboard, + action: () => copyToClipboard( + value: client.name != '' + ? client.name! + : client.ip, + successMessage: AppLocalizations.of(context)!.copiedClipboard, + ) + ) + ], + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(28), + child: InkWell( + borderRadius: BorderRadius.circular(28), + onTap: () => onTap(client), + child: Container( + width: double.maxFinite, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(28), + color: client == selectedClient + ? Theme.of(context).colorScheme.primaryContainer + : null + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + client.name != '' + ? client.name! + : client.ip, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + if (client.name != '') Text(client.ip) + ], + ), + ) + ], + ), + ), + Text( + client.source, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface + ), + ), + ], + ) + ), + ), + ), + ), + ); + } + else { + return OptionsMenu( + options: (_) => [ + MenuOption( + icon: Icons.copy_rounded, + title: AppLocalizations.of(context)!.copyClipboard, + action: () => copyToClipboard( + value: client.name != '' + ? client.name! + : client.ip, + successMessage: AppLocalizations.of(context)!.copiedClipboard, + ) + ) + ], + child: CustomListTile( + title: client.name != '' + ? client.name! + : client.ip, + subtitle: client.name != '' + ? client.ip + : null, + trailing: Text( + client.source, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface + ), + ), + onTap: () => onTap(client), + ), + ); + } + } +} \ No newline at end of file diff --git a/lib/screens/clients/client/added_client_tile.dart b/lib/screens/clients/client/added_client_tile.dart new file mode 100644 index 0000000..5af9e46 --- /dev/null +++ b/lib/screens/clients/client/added_client_tile.dart @@ -0,0 +1,261 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/widgets/options_menu.dart'; +import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; + +import 'package:adguard_home_manager/models/menu_option.dart'; +import 'package:adguard_home_manager/functions/copy_clipboard.dart'; +import 'package:adguard_home_manager/models/clients.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; + +class AddedClientTile extends StatefulWidget { + final Client client; + final void Function(Client) onTap; + final void Function(Client)? onEdit; + final void Function(Client) onDelete; + final Client? selectedClient; + final bool? splitView; + + const AddedClientTile({ + super.key, + required this.client, + required this.onTap, + this.onEdit, + required this.onDelete, + this.selectedClient, + required this.splitView, + }); + + @override + State createState() => _AddedClientTileState(); +} + +class _AddedClientTileState extends State { + bool _isHover = false; + + @override + Widget build(BuildContext context) { + final appConfigProvider = Provider.of(context); + + if (widget.splitView == true) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(28), + child: OptionsMenu( + options: (_) => [ + MenuOption( + icon: Icons.copy_rounded, + title: AppLocalizations.of(context)!.copyClipboard, + action: () => copyToClipboard( + value: widget.client.ids.toString().replaceAll(RegExp(r'^\[|\]$'), ''), + successMessage: AppLocalizations.of(context)!.copiedClipboard, + ) + ), + ], + child: InkWell( + borderRadius: BorderRadius.circular(28), + onTap: () => widget.onTap(widget.client), + onHover: (v) => setState(() => _isHover = v), + child: Container( + width: double.maxFinite, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(28), + color: widget.client == widget.selectedClient + ? Theme.of(context).colorScheme.primaryContainer + : null + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.client.name, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + const SizedBox(height: 8), + Text( + widget.client.ids.toString().replaceAll(RegExp(r'^\[|\]$'), ''), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Icon( + Icons.filter_list_rounded, + size: 19, + color: widget.client.filteringEnabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red, + ), + const SizedBox(width: 10), + Icon( + Icons.vpn_lock_rounded, + size: 18, + color: widget.client.safebrowsingEnabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red, + ), + const SizedBox(width: 10), + Icon( + Icons.block, + size: 18, + color: widget.client.parentalEnabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red, + ), + const SizedBox(width: 10), + Icon( + Icons.search_rounded, + size: 19, + color: widget.client.safeSearch != null && widget.client.safeSearch!.enabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red + ) + ], + ) + ], + ), + ) + ], + ), + ), + if (widget.onEdit != null && _isHover == true) ...[ + const SizedBox(width: 8), + IconButton( + onPressed: () => widget.onEdit!(widget.client), + icon: const Icon(Icons.file_open_rounded), + tooltip: AppLocalizations.of(context)!.seeDetails, + ) + ] + ], + ) + ), + ), + ), + ), + ); + } + else { + return OptionsMenu( + options: (_) => [ + if (widget.onEdit != null) MenuOption( + title: AppLocalizations.of(context)!.seeDetails, + icon: Icons.file_open_rounded, + action: () => widget.onEdit!(widget.client) + ), + MenuOption( + icon: Icons.copy_rounded, + title: AppLocalizations.of(context)!.copyClipboard, + action: () => copyToClipboard( + value: widget.client.ids.toString().replaceAll(RegExp(r'^\[|\]$'), ''), + successMessage: AppLocalizations.of(context)!.copiedClipboard, + ) + ), + ], + child: CustomListTile( + onTap: () => widget.onTap(widget.client), + title: widget.client.name, + subtitleWidget: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.client.ids.toString().replaceAll(RegExp(r'^\[|\]$'), ''), + style: TextStyle( + color: Theme.of(context).listTileTheme.textColor + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Icon( + Icons.filter_list_rounded, + size: 19, + color: widget.client.filteringEnabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red, + ), + const SizedBox(width: 10), + Icon( + Icons.vpn_lock_rounded, + size: 18, + color: widget.client.safebrowsingEnabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red, + ), + const SizedBox(width: 10), + Icon( + Icons.block, + size: 18, + color: widget.client.parentalEnabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red, + ), + const SizedBox(width: 10), + Icon( + Icons.search_rounded, + size: 19, + color: widget.client.safeSearch != null && widget.client.safeSearch!.enabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red + ) + ], + ) + ], + ), + ), + ); + } + } +} \ No newline at end of file diff --git a/lib/screens/clients/client/blocked_services_section.dart b/lib/screens/clients/client/blocked_services_section.dart new file mode 100644 index 0000000..4a7fc11 --- /dev/null +++ b/lib/screens/clients/client/blocked_services_section.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/screens/clients/client/client_screen_functions.dart'; + +class BlockedServicesSection extends StatelessWidget { + final bool useGlobalSettingsServices; + final List blockedServices; + final void Function(List) onUpdatedBlockedServices; + final void Function(bool) onUpdateServicesGlobalSettings; + + const BlockedServicesSection({ + super.key, + required this.useGlobalSettingsServices, + required this.blockedServices, + required this.onUpdatedBlockedServices, + required this.onUpdateServicesGlobalSettings + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Material( + color: Theme.of(context).colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(28), + child: InkWell( + onTap: () => onUpdateServicesGlobalSettings(!useGlobalSettingsServices), + borderRadius: BorderRadius.circular(28), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 6 + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + AppLocalizations.of(context)!.useGlobalSettings, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface + ), + ), + ), + Switch( + value: useGlobalSettingsServices, + onChanged: (value) => onUpdateServicesGlobalSettings(value), + ) + ], + ), + ), + ), + ), + ), + const SizedBox(height: 12), + Material( + color: Colors.transparent, + child: InkWell( + onTap: useGlobalSettingsServices == false + ? () => openServicesModal( + context: context, + blockedServices: blockedServices, + onUpdateBlockedServices: onUpdatedBlockedServices + ) + : null, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, horizontal: 32 + ), + child: Row( + children: [ + Icon( + Icons.public, + color: useGlobalSettingsServices == false + ? Theme.of(context).listTileTheme.iconColor + : Theme.of(context).colorScheme.onSurface.withOpacity(0.38), + ), + const SizedBox(width: 16), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppLocalizations.of(context)!.selectBlockedServices, + style: TextStyle( + fontSize: 16, + color: useGlobalSettingsServices == false + ? Theme.of(context).colorScheme.onSurface + : Theme.of(context).colorScheme.onSurface.withOpacity(0.38), + ), + ), + if (useGlobalSettingsServices == false) ...[ + const SizedBox(height: 5), + Text( + blockedServices.isNotEmpty + ? "${blockedServices.length} ${AppLocalizations.of(context)!.servicesBlocked}" + : AppLocalizations.of(context)!.noBlockedServicesSelected, + style: TextStyle( + color: Theme.of(context).listTileTheme.iconColor + ), + ) + ] + ], + ), + ) + ], + ), + ), + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/clients/client/blocking_schedule.dart b/lib/screens/clients/client/blocking_schedule.dart new file mode 100644 index 0000000..f17bd4d --- /dev/null +++ b/lib/screens/clients/client/blocking_schedule.dart @@ -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 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 + ) + ); + } +} \ No newline at end of file diff --git a/lib/screens/clients/client/blocking_schedule_modal.dart b/lib/screens/clients/client/blocking_schedule_modal.dart new file mode 100644 index 0000000..1521ead --- /dev/null +++ b/lib/screens/clients/client/blocking_schedule_modal.dart @@ -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 createState() => _BlockingScheduleModalState(); +} + +class _BlockingScheduleModalState extends State { + final _weekdaysScrollController = ScrollController(); + + String? _timezone; + List _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) + ), + ], + ) + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/clients/client/client_form.dart b/lib/screens/clients/client/client_form.dart new file mode 100644 index 0000000..2d22dd7 --- /dev/null +++ b/lib/screens/clients/client/client_form.dart @@ -0,0 +1,298 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/screens/clients/client/blocked_services_section.dart'; +import 'package:adguard_home_manager/screens/clients/client/client_screen.dart'; +import 'package:adguard_home_manager/screens/clients/client/client_screen_functions.dart'; +import 'package:adguard_home_manager/screens/clients/client/identifiers_section.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/upstream_servers_section.dart'; +import 'package:adguard_home_manager/screens/clients/client/blocking_schedule.dart'; + +import 'package:adguard_home_manager/widgets/custom_switch_list_tile.dart'; +import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; +import 'package:adguard_home_manager/widgets/section_label.dart'; + +import 'package:adguard_home_manager/models/clients.dart'; +import 'package:adguard_home_manager/models/safe_search.dart'; + +class ClientForm extends StatelessWidget { + final bool isFullScreen; + final Client? client; + final TextEditingController nameController; + final List identifiersControllers; + final List selectedTags; + final bool useGlobalSettingsFiltering; + final bool? enableFiltering; + final bool? enableSafeBrowsing; + final bool? enableParentalControl; + final bool? enableSafeSearch; + final SafeSearch? safeSearch; + final SafeSearch defaultSafeSearch; + final bool useGlobalSettingsServices; + final List blockedServices; + final void Function(List) updateBlockedServices; + final List upstreamServers; + final void Function(List) updateUpstreamServers; + final void Function(List) updateSelectedTags; + final void Function(List) updateIdentifiersControllers; + final void Function() enableDisableGlobalSettingsFiltering; + final void Function(bool) updateEnableFiltering; + final void Function(bool) updateEnableSafeBrowsing; + final void Function(bool) updateEnableParentalControl; + final void Function(bool) updateEnableSafeSearch; + final void Function(SafeSearch) updateSafeSearch; + final void Function(bool) updateUseGlobalSettingsServices; + final bool ignoreClientQueryLog; + final void Function(bool) updateIgnoreClientQueryLog; + final bool ignoreClientStatistics; + final void Function(bool) updateIgnoreClientStatistics; + final bool enableDnsCache; + final void Function(bool) updateEnableDnsCache; + final TextEditingController dnsCacheField; + final String? dnsCacheError; + final void Function(String?) updateDnsCacheError; + final BlockedServicesSchedule blockedServicesSchedule; + final void Function(BlockedServicesSchedule) setBlockedServicesSchedule; + + const ClientForm({ + super.key, + required this.isFullScreen, + required this.client, + required this.nameController, + required this.identifiersControllers, + required this.selectedTags, + required this.useGlobalSettingsFiltering, + required this.enableFiltering, + required this.enableParentalControl, + required this.enableSafeBrowsing, + required this.enableSafeSearch, + required this.safeSearch, + required this.blockedServices, + required this.updateBlockedServices, + required this.upstreamServers, + required this.updateUpstreamServers, + required this.defaultSafeSearch, + required this.useGlobalSettingsServices, + required this.updateSelectedTags, + required this.updateIdentifiersControllers, + required this.enableDisableGlobalSettingsFiltering, + required this.updateEnableFiltering, + required this.updateEnableParentalControl, + required this.updateEnableSafeBrowsing, + required this.updateEnableSafeSearch, + required this.updateSafeSearch, + required this.updateUseGlobalSettingsServices, + required this.ignoreClientQueryLog, + required this.ignoreClientStatistics, + required this.updateIgnoreClientQueryLog, + required this.updateIgnoreClientStatistics, + required this.enableDnsCache, + required this.updateEnableDnsCache, + required this.dnsCacheField, + required this.dnsCacheError, + required this.updateDnsCacheError, + required this.blockedServicesSchedule, + required this.setBlockedServicesSchedule, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: TextFormField( + enabled: client != null ? false : true, + controller: nameController, + onChanged: (_) => {}, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.badge_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + labelText: AppLocalizations.of(context)!.name, + ), + ), + ), + SectionLabel( + label: AppLocalizations.of(context)!.tags, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), + ), + TagsSection( + selectedTags: selectedTags, + onTagsSelected: updateSelectedTags + ), + IdentifiersSection( + identifiersControllers: identifiersControllers, + onUpdateIdentifiersControllers: (c) { + updateIdentifiersControllers(c); + }, + onCheckValidValues: () => {} + ), + SectionLabel( + label: AppLocalizations.of(context)!.settings, + padding: const EdgeInsets.only( + left: 16, right: 16, top: 12, bottom: 24 + ) + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Material( + color: Theme.of(context).colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(28), + child: InkWell( + onTap: enableDisableGlobalSettingsFiltering, + borderRadius: BorderRadius.circular(28), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 6 + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + AppLocalizations.of(context)!.useGlobalSettings, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface + ), + ), + ), + Switch( + value: useGlobalSettingsFiltering, + onChanged: (value) => enableDisableGlobalSettingsFiltering() + ) + ], + ), + ), + ), + ), + ), + const SizedBox(height: 8), + SettingsTile( + label: AppLocalizations.of(context)!.enableFiltering, + value: enableFiltering, + onChange: (value) => updateEnableFiltering(value), + useGlobalSettingsFiltering: useGlobalSettingsFiltering, + ), + SettingsTile( + label: AppLocalizations.of(context)!.enableSafeBrowsing, + value: enableSafeBrowsing, + onChange: (value) => updateEnableSafeBrowsing(value), + useGlobalSettingsFiltering: useGlobalSettingsFiltering, + ), + SettingsTile( + label: AppLocalizations.of(context)!.enableParentalControl, + value: enableParentalControl, + onChange: (value) => updateEnableParentalControl(value), + useGlobalSettingsFiltering: useGlobalSettingsFiltering, + ), + CustomListTile( + title: AppLocalizations.of(context)!.safeSearch, + padding: const EdgeInsets.symmetric( + horizontal: 34, + vertical: 16 + ), + trailing: Padding( + padding: const EdgeInsets.only(right: 16), + child: Icon( + Icons.chevron_right_rounded, + color: useGlobalSettingsFiltering == true + ? Colors.grey + : Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + onTap: useGlobalSettingsFiltering == false + ? () => openSafeSearchModal( + context: context, + blockedServices: blockedServices, + defaultSafeSearch: defaultSafeSearch, + safeSearch: safeSearch, + onUpdateSafeSearch: updateSafeSearch + ) + : null, + ), + SectionLabel( + label: AppLocalizations.of(context)!.queryLogsAndStatistics, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), + ), + CustomSwitchListTile( + title: AppLocalizations.of(context)!.ignoreClientQueryLog, + value: ignoreClientQueryLog, + onChanged: updateIgnoreClientQueryLog, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 6 + ), + ), + CustomSwitchListTile( + title: AppLocalizations.of(context)!.ignoreClientStatistics, + value: ignoreClientStatistics, + onChanged: updateIgnoreClientStatistics, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 6 + ), + ), + SectionLabel( + label: AppLocalizations.of(context)!.blockedServices, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), + ), + BlockedServicesSection( + useGlobalSettingsServices: useGlobalSettingsServices, + blockedServices: blockedServices, + onUpdatedBlockedServices: updateBlockedServices, + onUpdateServicesGlobalSettings: updateUseGlobalSettingsServices, + ), + UpstreamServersSection( + upstreamServers: upstreamServers, + onCheckValidValues: () => {}, + onUpdateUpstreamServers: updateUpstreamServers + ), + SectionLabel( + label: AppLocalizations.of(context)!.upstreamDnsCacheConfiguration, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), + ), + CustomSwitchListTile( + title: AppLocalizations.of(context)!.enableDnsCachingClient, + value: enableDnsCache, + onChanged: updateEnableDnsCache, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 6 + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), + child: TextFormField( + controller: dnsCacheField, + onChanged: (v) => updateDnsCacheError(!validateNumber(v) ? AppLocalizations.of(context)!.invalidValue : null), + decoration: InputDecoration( + prefixIcon: const Icon(Icons.storage_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + labelText: AppLocalizations.of(context)!.dnsCacheSize, + errorText: dnsCacheError + ), + keyboardType: TextInputType.number, + ), + ), + BlockingSchedule( + blockedServicesSchedule: blockedServicesSchedule, + setBlockedServicesSchedule: setBlockedServicesSchedule, + ), + const SizedBox(height: 16), + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/clients/client/client_placeholder.dart b/lib/screens/clients/client/client_placeholder.dart new file mode 100644 index 0000000..0e8f018 --- /dev/null +++ b/lib/screens/clients/client/client_placeholder.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +class ClientPlaceholder extends StatelessWidget { + const ClientPlaceholder({super.key}); + + @override + Widget build(BuildContext context) { + return const Center( + child: Text("Select a client"), + ); + } +} \ No newline at end of file diff --git a/lib/screens/clients/client/client_screen.dart b/lib/screens/clients/client/client_screen.dart new file mode 100644 index 0000000..573e6cf --- /dev/null +++ b/lib/screens/clients/client/client_screen.dart @@ -0,0 +1,469 @@ +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/clients/client/client_form.dart'; +import 'package:adguard_home_manager/screens/clients/client/client_screen_functions.dart'; + +import 'package:adguard_home_manager/models/safe_search.dart'; +import 'package:adguard_home_manager/providers/clients_provider.dart'; +import 'package:adguard_home_manager/models/clients.dart'; + +class ClientInitialData { + final String name; + final String ip; + + const ClientInitialData({ + required this.name, + required this.ip, + }); +} + +class ControllerListItem { + final String id; + final TextEditingController controller; + + const ControllerListItem({ + required this.id, + required this.controller + }); +} + +class ClientScreen extends StatefulWidget { + final Client? client; + final void Function(Client) onConfirm; + final void Function(Client)? onDelete; + final bool fullScreen; + final ClientInitialData? initialData; + + const ClientScreen({ + super.key, + this.client, + required this.onConfirm, + this.onDelete, + required this.fullScreen, + this.initialData, + }); + + @override + State createState() => _ClientScreenState(); +} + +class _ClientScreenState extends State { + final _scrollController = ScrollController(); + + final Uuid uuid = const Uuid(); + + bool validValues = false; + + TextEditingController nameController = TextEditingController(); + + List selectedTags = []; + + List identifiersControllers = [ + ControllerListItem(id: "0", controller: TextEditingController()) + ]; + + bool useGlobalSettingsFiltering = true; + bool? enableFiltering; + bool? enableSafeBrowsing; + bool? enableParentalControl; + bool? enableSafeSearch; + SafeSearch? safeSearch; + + final SafeSearch defaultSafeSearch = SafeSearch( + enabled: false, + bing: false, + duckduckgo: false, + google: false, + pixabay: false, + yandex: false, + youtube: false + ); + + bool useGlobalSettingsServices = true; + List blockedServices = []; + + List upstreamServers = []; + + bool _ignoreClientQueryLog = false; + bool _ignoreClientStatistics = false; + + bool _enableDnsCache = false; + final _dnsCacheField = TextEditingController(); + String? _dnsCacheError; + + BlockedServicesSchedule _blockedServicesSchedule = BlockedServicesSchedule(); + + // VALIDATIONS + bool _nameValid = true; + bool _identifiersValid = true; + bool _dnsCacheValid = true; + + void enableDisableGlobalSettingsFiltering() { + if (useGlobalSettingsFiltering == true) { + setState(() { + useGlobalSettingsFiltering = false; + + enableFiltering = false; + enableSafeBrowsing = false; + enableParentalControl = false; + enableSafeSearch = false; + safeSearch = defaultSafeSearch; + }); + } + else if (useGlobalSettingsFiltering == false) { + setState(() { + useGlobalSettingsFiltering = true; + + enableFiltering = null; + enableSafeBrowsing = null; + enableParentalControl = null; + enableSafeSearch = null; + safeSearch = null; + }); + } + } + + @override + void initState() { + if (widget.client != null) { + validValues = true; + + nameController.text = widget.client!.name; + selectedTags = widget.client!.tags; + identifiersControllers = widget.client!.ids.map((e) => ControllerListItem( + id: uuid.v4(), + controller: TextEditingController(text: e) + )).toList(); + useGlobalSettingsFiltering = widget.client!.useGlobalSettings; + enableFiltering = widget.client!.filteringEnabled; + enableParentalControl = widget.client!.parentalEnabled; + enableSafeBrowsing = widget.client!.safebrowsingEnabled; + safeSearch = widget.client!.safeSearch; + useGlobalSettingsServices = widget.client!.useGlobalBlockedServices; + blockedServices = widget.client!.blockedServices; + upstreamServers = widget.client!.upstreams.map((e) => ControllerListItem( + id: uuid.v4(), + controller: TextEditingController(text: e) + )).toList(); + _ignoreClientQueryLog = widget.client!.ignoreQuerylog ?? false; + _ignoreClientStatistics = widget.client!.ignoreStatistics ?? false; + _enableDnsCache = widget.client!.upstreamsCacheEnabled ?? false; + _dnsCacheField.text = widget.client!.upstreamsCacheSize != null + ? widget.client!.upstreamsCacheSize.toString() + : ""; + if (widget.client!.blockedServicesSchedule != null) { + _blockedServicesSchedule = widget.client!.blockedServicesSchedule!; + } + } + if (widget.initialData != null) { + nameController.text = widget.initialData!.name; + identifiersControllers[0] = ControllerListItem( + id: uuid.v4(), + controller: TextEditingController(text: widget.initialData!.ip) + ); + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + final clientsProvider = Provider.of(context); + + void createClient() { + final Client client = Client( + name: nameController.text, + ids: List.from(identifiersControllers.map((e) => e.controller.text)), + useGlobalSettings: useGlobalSettingsFiltering, + filteringEnabled: enableFiltering ?? false, + parentalEnabled: enableParentalControl ?? false, + safebrowsingEnabled: enableSafeBrowsing ?? false, + safeSearch: safeSearch, + useGlobalBlockedServices: useGlobalSettingsServices, + blockedServices: blockedServices, + upstreams: List.from(upstreamServers.map((e) => e.controller.text)), + tags: selectedTags, + ignoreQuerylog: _ignoreClientQueryLog, + ignoreStatistics: _ignoreClientStatistics, + upstreamsCacheEnabled: _enableDnsCache, + upstreamsCacheSize: _dnsCacheField.text != "" + ? int.parse(_dnsCacheField.text) + : null, + blockedServicesSchedule: _blockedServicesSchedule + ); + widget.onConfirm(client); + } + + void validateValues() { + _nameValid = nameController.text != ''; + _identifiersValid = identifiersControllers.isNotEmpty && identifiersControllers[0].controller.text != ''; + _dnsCacheValid = (_dnsCacheField.text == "" || _dnsCacheField.text != "" && RegExp(r'^\d+$').hasMatch(_dnsCacheField.text)); + if (_nameValid && _identifiersValid && _dnsCacheValid) { + createClient(); + Navigator.pop(context); + } + else { + _scrollController.animateTo( + 0, + curve: Curves.easeOut, + duration: const Duration(milliseconds: 500) + ); + setState(() => {}); + } + } + + List actions() { + return [ + IconButton( + onPressed: validateValues, + icon: const Icon(Icons.save_rounded), + tooltip: AppLocalizations.of(context)!.save, + ), + if (widget.client != null) IconButton( + onPressed: () => openDeleteClientScreen( + context: context, + onDelete: () => clientsProvider.deleteClient(widget.client!), + ), + icon: const Icon(Icons.delete_rounded), + tooltip: AppLocalizations.of(context)!.delete, + ), + const SizedBox(width: 10), + ]; + } + + + if (widget.fullScreen == true) { + return Material( + child: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverAppBar.large( + pinned: true, + floating: true, + centerTitle: false, + forceElevated: innerBoxIsScrolled, + leading: IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.close) + ), + title: Text( + widget.client != null + ? AppLocalizations.of(context)!.client + : AppLocalizations.of(context)!.addClient + ), + actions: actions(), + ) + ) + ], + body: SafeArea( + top: false, + bottom: false, + child: Builder( + builder: (context) => CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + ), + SliverList.list( + children: [ + if (!_nameValid || !_identifiersValid || !_dnsCacheValid) _Errors( + nameValid: _nameValid, + identifiersValid: _identifiersValid, + dnsCacheValid: _dnsCacheValid + ), + ClientForm( + isFullScreen: true, + client: widget.client, + nameController: nameController, + identifiersControllers: identifiersControllers, + selectedTags: selectedTags, + useGlobalSettingsFiltering: useGlobalSettingsFiltering, + enableFiltering: enableFiltering, + enableParentalControl: enableParentalControl, + enableSafeBrowsing: enableSafeBrowsing, + enableSafeSearch: enableSafeSearch, + safeSearch: safeSearch, + blockedServices: blockedServices, + updateBlockedServices: (v) => setState(() => blockedServices = v), + upstreamServers: upstreamServers, + updateUpstreamServers: (v) => setState(() => upstreamServers = v), + defaultSafeSearch: defaultSafeSearch, + useGlobalSettingsServices: useGlobalSettingsServices, + updateSelectedTags: (v) => setState(() => selectedTags = v), + updateIdentifiersControllers: (v) => setState(() => identifiersControllers = v), + enableDisableGlobalSettingsFiltering: enableDisableGlobalSettingsFiltering, + updateEnableFiltering: (v) => setState(() => enableFiltering = v), + updateEnableParentalControl: (v) => setState(() => enableParentalControl = v), + updateEnableSafeBrowsing: (v) => setState(() => enableSafeBrowsing = v), + updateEnableSafeSearch: (v) => setState(() => enableSafeSearch = v), + updateSafeSearch: (v) => setState(() => safeSearch = v), + updateUseGlobalSettingsServices: (v) => setState(() => useGlobalSettingsServices = v), + ignoreClientQueryLog: _ignoreClientQueryLog, + ignoreClientStatistics: _ignoreClientStatistics, + updateIgnoreClientQueryLog: (v) => setState(() => _ignoreClientQueryLog = v), + updateIgnoreClientStatistics: (v) => setState(() => _ignoreClientStatistics = v), + enableDnsCache: _enableDnsCache, + updateEnableDnsCache: (v) => setState(() => _enableDnsCache = v), + dnsCacheField: _dnsCacheField, + dnsCacheError: _dnsCacheError, + updateDnsCacheError: (v) => setState(() => _dnsCacheError = v), + blockedServicesSchedule: _blockedServicesSchedule, + setBlockedServicesSchedule: (v) => setState(() => _blockedServicesSchedule = v), + ), + ], + ) + ], + ), + ) + ) + ), + ); + } + else { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500 + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + CloseButton(onPressed: () => Navigator.pop(context)), + const SizedBox(width: 8), + Text( + widget.client != null + ? AppLocalizations.of(context)!.client + : AppLocalizations.of(context)!.addClient, + style: const TextStyle( + fontSize: 22 + ), + ), + ], + ), + Row( + children: actions() + ) + ], + ), + ), + Flexible( + child: ListView( + controller: _scrollController, + children: [ + if (!_nameValid || !_identifiersValid || !_dnsCacheValid) _Errors( + nameValid: _nameValid, + identifiersValid: _identifiersValid, + dnsCacheValid: _dnsCacheValid + ), + ClientForm( + isFullScreen: false, + client: widget.client, + nameController: nameController, + identifiersControllers: identifiersControllers, + selectedTags: selectedTags, + useGlobalSettingsFiltering: useGlobalSettingsFiltering, + enableFiltering: enableFiltering, + enableParentalControl: enableParentalControl, + enableSafeBrowsing: enableSafeBrowsing, + enableSafeSearch: enableSafeSearch, + safeSearch: safeSearch, + blockedServices: blockedServices, + updateBlockedServices: (v) => setState(() => blockedServices = v), + upstreamServers: upstreamServers, + updateUpstreamServers: (v) => setState(() => upstreamServers = v), + defaultSafeSearch: defaultSafeSearch, + useGlobalSettingsServices: useGlobalSettingsServices, + updateSelectedTags: (v) => setState(() => selectedTags = v), + updateIdentifiersControllers: (v) => setState(() => identifiersControllers = v), + enableDisableGlobalSettingsFiltering: enableDisableGlobalSettingsFiltering, + updateEnableFiltering: (v) => setState(() => enableFiltering = v), + updateEnableParentalControl: (v) => setState(() => enableParentalControl = v), + updateEnableSafeBrowsing: (v) => setState(() => enableSafeBrowsing = v), + updateEnableSafeSearch: (v) => setState(() => enableSafeSearch = v), + updateSafeSearch: (v) => setState(() => safeSearch = v), + updateUseGlobalSettingsServices: (v) => setState(() => useGlobalSettingsServices = v), + ignoreClientQueryLog: _ignoreClientQueryLog, + ignoreClientStatistics: _ignoreClientStatistics, + updateIgnoreClientQueryLog: (v) => setState(() => _ignoreClientQueryLog = v), + updateIgnoreClientStatistics: (v) => setState(() => _ignoreClientStatistics = v), + enableDnsCache: _enableDnsCache, + updateEnableDnsCache: (v) => setState(() => _enableDnsCache = v), + dnsCacheField: _dnsCacheField, + dnsCacheError: _dnsCacheError, + updateDnsCacheError: (v) => setState(() => _dnsCacheError = v), + blockedServicesSchedule: _blockedServicesSchedule, + setBlockedServicesSchedule: (v) => setState(() => _blockedServicesSchedule = v), + ), + ], + ), + ) + ], + ), + ), + ); + } + } +} + +class _Errors extends StatelessWidget { + final bool nameValid; + final bool identifiersValid; + final bool dnsCacheValid; + + const _Errors({ + required this.nameValid, + required this.identifiersValid, + required this.dnsCacheValid, + }); + + @override + Widget build(BuildContext context) { + return Card( + elevation: 0, + color: Colors.red.withOpacity(0.2), + margin: const EdgeInsets.all(16), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppLocalizations.of(context)!.errors, + style: const TextStyle( + fontSize: 18 + ), + ), + const SizedBox(height: 8), + if (!nameValid) Text( + "● ${AppLocalizations.of(context)!.nameInvalid}", + style: const TextStyle( + fontSize: 14 + ), + ), + if (!identifiersValid) Text( + "● ${AppLocalizations.of(context)!.oneIdentifierRequired}", + style: const TextStyle( + fontSize: 14 + ), + ), + if (!dnsCacheValid) Text( + "● ${AppLocalizations.of(context)!.dnsCacheNumber}", + style: const TextStyle( + fontSize: 14 + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/clients/client/client_screen_functions.dart b/lib/screens/clients/client/client_screen_functions.dart new file mode 100644 index 0000000..2c176c4 --- /dev/null +++ b/lib/screens/clients/client/client_screen_functions.dart @@ -0,0 +1,118 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'package:adguard_home_manager/screens/clients/client/client_screen.dart'; +import 'package:adguard_home_manager/screens/clients/client/remove_client_modal.dart'; +import 'package:adguard_home_manager/screens/clients/client/safe_search_modal.dart'; +import 'package:adguard_home_manager/screens/clients/client/services_modal.dart'; +import 'package:adguard_home_manager/screens/clients/client/tags_modal.dart'; + +import 'package:adguard_home_manager/models/clients.dart'; +import 'package:adguard_home_manager/providers/clients_provider.dart'; +import 'package:adguard_home_manager/models/safe_search.dart'; + +void openTagsModal({ + required BuildContext context, + required List selectedTags, + required void Function(List) onSelectedTags +}) { + showDialog( + context: context, + builder: (context) => TagsModal( + selectedTags: selectedTags, + tags: Provider.of(context, listen: false).clients!.supportedTags, + onConfirm: onSelectedTags, + ) + ); +} + +void openServicesModal({ + required BuildContext context, + required List blockedServices, + required void Function(List) onUpdateBlockedServices +}) { + showDialog( + context: context, + builder: (context) => ServicesModal( + blockedServices: blockedServices, + onConfirm: onUpdateBlockedServices, + ) + ); +} + +void openDeleteClientScreen({ + required BuildContext context, + required void Function() onDelete +}) { + showDialog( + context: context, + builder: (ctx) => RemoveClientModal( + onConfirm: () { + Navigator.pop(context); + onDelete(); + } + ) + ); +} + +void openSafeSearchModal({ + required BuildContext context, + required List blockedServices, + required void Function(SafeSearch) onUpdateSafeSearch, + required SafeSearch? safeSearch, + required SafeSearch defaultSafeSearch +}) { + showDialog( + context: context, + builder: (context) => SafeSearchModal( + safeSearch: safeSearch ?? defaultSafeSearch, + disabled: false, + onConfirm: onUpdateSafeSearch + ) + ); +} + +void openClientFormModal({ + required BuildContext context, + required double width, + Client? client, + required void Function(Client) onConfirm, + void Function(Client)? onDelete, + ClientInitialData? initialData, +}) { + showGeneralDialog( + context: context, + barrierColor: !(width > 900 || !(Platform.isAndroid | Platform.isIOS)) + ?Colors.transparent + : Colors.black54, + transitionBuilder: (context, anim1, anim2, child) { + return SlideTransition( + position: Tween( + begin: const Offset(0, 1), + end: const Offset(0, 0) + ).animate( + CurvedAnimation( + parent: anim1, + curve: Curves.easeInOutCubicEmphasized + ) + ), + child: child, + ); + }, + pageBuilder: (context, animation, secondaryAnimation) => ClientScreen( + fullScreen: !(width > 900 || !(Platform.isAndroid | Platform.isIOS)), + client: client, + onConfirm: onConfirm, + onDelete: onDelete, + initialData: initialData, + ), + ); +} + +bool validateNumber(String value) { + if (value == "") return true; + final regexp = RegExp(r'^\d+$'); + return regexp.hasMatch(value); +} \ No newline at end of file diff --git a/lib/screens/clients/client/identifiers_section.dart b/lib/screens/clients/client/identifiers_section.dart new file mode 100644 index 0000000..c5f9d4c --- /dev/null +++ b/lib/screens/clients/client/identifiers_section.dart @@ -0,0 +1,105 @@ +import 'package:adguard_home_manager/screens/clients/client/client_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:uuid/uuid.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/widgets/section_label.dart'; + +class IdentifiersSection extends StatefulWidget { + final List identifiersControllers; + final void Function(List) onUpdateIdentifiersControllers; + final void Function() onCheckValidValues; + + const IdentifiersSection({ + super.key, + required this.identifiersControllers, + required this.onUpdateIdentifiersControllers, + required this.onCheckValidValues + }); + + @override + State createState() => _IdentifiersSectionState(); +} + +class _IdentifiersSectionState extends State { + final Uuid uuid = const Uuid(); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SectionLabel( + label: AppLocalizations.of(context)!.identifiers, + padding: const EdgeInsets.only( + left: 16, right: 16, top: 24, bottom: 12 + ) + ), + Padding( + padding: const EdgeInsets.only(right: 10), + child: IconButton( + onPressed: () => widget.onUpdateIdentifiersControllers([ + ...widget.identifiersControllers, + ControllerListItem( + id: uuid.v4(), + controller: TextEditingController() + ), + ]), + icon: const Icon(Icons.add) + ), + ) + ], + ), + if (widget.identifiersControllers.isNotEmpty) ...widget.identifiersControllers.map((controller) => Padding( + padding: const EdgeInsets.only( + top: 12, bottom: 12, left: 16, right: 10 + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: TextFormField( + controller: controller.controller, + onChanged: (_) => widget.onCheckValidValues(), + decoration: InputDecoration( + prefixIcon: const Icon(Icons.tag), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + helperText: AppLocalizations.of(context)!.identifierHelper, + labelText: AppLocalizations.of(context)!.identifier, + ), + ), + ), + const SizedBox(width: 12), + Padding( + padding: const EdgeInsets.only(bottom: 24), + child: IconButton( + onPressed: () => widget.onUpdateIdentifiersControllers( + widget.identifiersControllers.where((e) => e.id != controller.id).toList() + ), + icon: const Icon(Icons.remove_circle_outline_outlined) + ), + ) + ], + ), + )), + if (widget.identifiersControllers.isEmpty) Container( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Text( + AppLocalizations.of(context)!.noIdentifiers, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 18, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/clients/client/logs_list_client.dart b/lib/screens/clients/client/logs_list_client.dart new file mode 100644 index 0000000..f12f8e4 --- /dev/null +++ b/lib/screens/clients/client/logs_list_client.dart @@ -0,0 +1,307 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:async/async.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/screens/logs/log_tile.dart'; +import 'package:adguard_home_manager/screens/logs/details/log_details_screen.dart'; + +import 'package:adguard_home_manager/services/api_client.dart'; +import 'package:adguard_home_manager/functions/desktop_mode.dart'; +import 'package:adguard_home_manager/models/logs.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; + +class LogsListClient extends StatefulWidget { + final String ip; + final String? name; + final ServersProvider serversProvider; + final AppConfigProvider appConfigProvider; + final bool splitView; + + const LogsListClient({ + super.key, + required this.ip, + this.name, + required this.serversProvider, + required this.appConfigProvider, + required this.splitView, + }); + + @override + State createState() => _LogsListClientState(); +} + +class _LogsListClientState extends State { + late ScrollController scrollController; + + bool isLoadingMore = false; + + int logsQuantity = 100; + int offset = 0; + + int loadStatus = 0; + LogsData? logsData; + + String previousIp = ""; + + bool showDivider = true; + + CancelableOperation? cancelableRequest; + + Future fetchLogs({ + int? inOffset, + bool? loadingMore, + String? responseStatus, + String? searchText, + }) async { + final serversProvider = Provider.of(context, listen: false); + + int offst = inOffset ?? offset; + + if (loadingMore != null && loadingMore == true) { + setState(() => isLoadingMore = true); + } + + if (cancelableRequest != null) cancelableRequest!.cancel(); + + cancelableRequest = CancelableOperation.fromFuture( + serversProvider.apiClient2!.getLogs( + count: logsQuantity, + offset: offst, + search: '"${widget.ip}"' + ) + ); + + final result = await cancelableRequest?.value as ApiResponse; + if (!mounted) return; + + if (loadingMore != null && loadingMore == true && mounted) { + setState(() => isLoadingMore = false); + } + + if (result.successful == true) { + setState(() => offset = inOffset != null ? inOffset+logsQuantity : offset+logsQuantity); + if (loadingMore != null && loadingMore == true && logsData != null) { + LogsData newLogsData = result.content; + newLogsData.data = [...logsData!.data, ...result.content.data]; + setState(() => logsData = newLogsData); + } + else { + LogsData newLogsData = result.content; + setState(() => logsData = newLogsData); + } + setState(() => loadStatus = 1); + } + else { + setState(() => loadStatus = 2); + } + } + + + void scrollListener() { + if (scrollController.position.extentAfter < 500 && isLoadingMore == false) { + fetchLogs(loadingMore: true); + } + if (scrollController.position.pixels > 0) { + setState(() => showDivider = false); + } + else { + setState(() => showDivider = true); + } + } + + @override + void initState() { + scrollController = ScrollController()..addListener(scrollListener); + fetchLogs(inOffset: 0); + setState(() => previousIp = widget.ip); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width; + + if (widget.ip != previousIp) { + setState(() => loadStatus = 0); + fetchLogs(inOffset: 0); + setState(() => previousIp = widget.ip); + } + + return Material( + child: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverAppBar.large( + pinned: true, + floating: true, + centerTitle: false, + forceElevated: innerBoxIsScrolled, + title: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppLocalizations.of(context)!.client, + style: const TextStyle( + fontSize: 24 + ), + ), + const SizedBox(height: 4), + Text( + widget.name != null && widget.name != '' ? widget.name! : widget.ip, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.secondary + ), + ) + ], + ), + ), + surfaceTintColor: isDesktop(MediaQuery.of(context).size.width) + ? Colors.transparent + : null, + actions: [ + if (!(Platform.isAndroid || Platform.isIOS)) ...[ + IconButton( + onPressed: fetchLogs, + icon: const Icon(Icons.refresh_rounded), + tooltip: AppLocalizations.of(context)!.refresh, + ), + const SizedBox(width: 8) + ] + ], + ) + ) + ], + body: SafeArea( + top: false, + bottom: false, + child: Builder( + builder: (context) => RefreshIndicator( + onRefresh: fetchLogs, + displacement: 95, + child: CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + ), + if (loadStatus == 0) SliverFillRemaining( + child: SizedBox( + width: double.maxFinite, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.loadingLogs, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ) + ), + if (loadStatus == 1 && logsData!.data.isNotEmpty) SliverList.builder( + itemCount: isLoadingMore == true + ? logsData!.data.length+1 + : logsData!.data.length, + itemBuilder: (context, index) { + if (isLoadingMore == true && index == logsData!.data.length) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 20), + child: Center( + child: CircularProgressIndicator(), + ), + ); + } + else { + return LogTile( + log: logsData!.data[index], + index: index, + length: logsData!.data.length, + useAlwaysNormalTile: true, + onLogTap: (log) => { + if (width > 700) { + showDialog( + context: context, + builder: (context) => LogDetailsScreen( + log: log, + dialog: true, + twoColumns: widget.splitView, + ) + ) + } + else { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => LogDetailsScreen( + log: log, + dialog: false, + twoColumns: widget.splitView, + ) + ) + ) + } + }, + twoColumns: widget.splitView, + ); + } + } + ), + if (loadStatus == 1 && logsData!.data.isEmpty) SliverFillRemaining( + child: Center( + child: Text( + AppLocalizations.of(context)!.noLogsDisplay, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + ), + if (loadStatus == 2) SliverFillRemaining( + child: SizedBox( + width: double.maxFinite, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon( + Icons.error, + color: Colors.red, + size: 50, + ), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.logsNotLoaded, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ), + ) + ], + ), + ), + ) + ) + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/clients/remove_client_modal.dart b/lib/screens/clients/client/remove_client_modal.dart similarity index 97% rename from lib/screens/clients/remove_client_modal.dart rename to lib/screens/clients/client/remove_client_modal.dart index 4427d22..b531540 100644 --- a/lib/screens/clients/remove_client_modal.dart +++ b/lib/screens/clients/client/remove_client_modal.dart @@ -5,9 +5,9 @@ class RemoveClientModal extends StatelessWidget { final void Function() onConfirm; const RemoveClientModal({ - Key? key, + super.key, required this.onConfirm - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/screens/clients/client/safe_search_modal.dart b/lib/screens/clients/client/safe_search_modal.dart new file mode 100644 index 0000000..1d539be --- /dev/null +++ b/lib/screens/clients/client/safe_search_modal.dart @@ -0,0 +1,201 @@ +import 'package:flutter/material.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/models/safe_search.dart'; + +class SafeSearchModal extends StatefulWidget { + final SafeSearch safeSearch; + final bool disabled; + final void Function(SafeSearch) onConfirm; + + const SafeSearchModal({ + super.key, + required this.safeSearch, + required this.disabled, + required this.onConfirm + }); + + @override + State createState() => _SafeSearchModalState(); +} + +class _SafeSearchModalState extends State { + bool generalEnabled = false; + bool bingEnabled = false; + bool duckduckgoEnabled = false; + bool googleEnabled = false; + bool pixabayEnabled = false; + bool yandexEnabled = false; + bool youtubeEnabled = false; + + @override + void initState() { + generalEnabled = widget.safeSearch.enabled; + bingEnabled = widget.safeSearch.bing; + duckduckgoEnabled = widget.safeSearch.duckduckgo; + googleEnabled = widget.safeSearch.google; + pixabayEnabled = widget.safeSearch.pixabay; + yandexEnabled = widget.safeSearch.yandex; + youtubeEnabled = widget.safeSearch.youtube; + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 16), + title: Column( + children: [ + Icon( + Icons.search_rounded, + size: 24, + color: Theme.of(context).listTileTheme.iconColor + ), + const SizedBox(height: 16), + Text( + AppLocalizations.of(context)!.safeSearch, + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface + ), + ) + ], + ), + content: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 400 + ), + child: Wrap( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Material( + color: Theme.of(context).colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(28), + child: InkWell( + onTap: widget.disabled == true + ? null + : () => setState(() => generalEnabled = !generalEnabled), + borderRadius: BorderRadius.circular(28), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 5 + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + AppLocalizations.of(context)!.enable, + style: TextStyle( + fontSize: 16, + color: widget.disabled == true + ? Colors.grey + : Theme.of(context).colorScheme.onSurface + ), + ), + Switch( + value: generalEnabled, + onChanged: widget.disabled == true + ? null + : (value) => setState(() => generalEnabled = value), + ) + ], + ), + ), + ), + ), + ), + const SizedBox(height: 4, width: double.maxFinite), + CustomCheckboxListTile( + value: bingEnabled, + onChanged: (value) => setState(() => bingEnabled = value), + title: "Bing", + disabled: widget.disabled || !generalEnabled, + padding: const EdgeInsets.symmetric( + horizontal: 36, + vertical: 4 + ), + ), + CustomCheckboxListTile( + value: duckduckgoEnabled, + onChanged: (value) => setState(() => duckduckgoEnabled = value), + title: "DuckDuckGo", + disabled: widget.disabled || !generalEnabled, + padding: const EdgeInsets.symmetric( + horizontal: 36, + vertical: 4 + ), + ), + CustomCheckboxListTile( + value: googleEnabled, + onChanged: (value) => setState(() => googleEnabled = value), + title: "Google", + disabled: widget.disabled || !generalEnabled, + padding: const EdgeInsets.symmetric( + horizontal: 36, + vertical: 4 + ), + ), + CustomCheckboxListTile( + value: pixabayEnabled, + onChanged: (value) => setState(() => pixabayEnabled = value), + title: "Pixabay", + disabled: widget.disabled || !generalEnabled, + padding: const EdgeInsets.symmetric( + horizontal: 36, + vertical: 4 + ), + ), + CustomCheckboxListTile( + value: yandexEnabled, + onChanged: (value) => setState(() => yandexEnabled = value), + title: "Yandex", + disabled: widget.disabled || !generalEnabled, + padding: const EdgeInsets.symmetric( + horizontal: 36, + vertical: 4 + ), + ), + CustomCheckboxListTile( + value: youtubeEnabled, + onChanged: (value) => setState(() => youtubeEnabled = value), + title: "YouTube", + disabled: widget.disabled || !generalEnabled, + padding: const EdgeInsets.symmetric( + horizontal: 36, + vertical: 4 + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.cancel) + ), + TextButton( + onPressed: widget.disabled == false ? () { + widget.onConfirm( + SafeSearch( + enabled: generalEnabled, + bing: bingEnabled, + duckduckgo: duckduckgoEnabled, + google: googleEnabled, + pixabay: pixabayEnabled, + yandex: yandexEnabled, + youtube: youtubeEnabled + ) + ); + Navigator.pop(context); + } : null, + child: Text(AppLocalizations.of(context)!.confirm) + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/clients/services_modal.dart b/lib/screens/clients/client/services_modal.dart similarity index 67% rename from lib/screens/clients/services_modal.dart rename to lib/screens/clients/client/services_modal.dart index fa5c2aa..b8f69dc 100644 --- a/lib/screens/clients/services_modal.dart +++ b/lib/screens/clients/client/services_modal.dart @@ -2,71 +2,34 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/providers/filtering_provider.dart'; -class ServicesModal extends StatelessWidget { +class ServicesModal extends StatefulWidget { final List blockedServices; final void Function(List) onConfirm; const ServicesModal({ - Key? key, + super.key, required this.blockedServices, required this.onConfirm, - }) : super(key: key); + }); @override - Widget build(BuildContext context) { - final serversProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - - return ServicesModalWidget( - blockedServices: blockedServices, - onConfirm: onConfirm, - serversProvider: serversProvider, - appConfigProvider: appConfigProvider, - ); - } + State createState() => _ServicesModalStateWidget(); } -class ServicesModalWidget extends StatefulWidget { - final ServersProvider serversProvider; - final AppConfigProvider appConfigProvider; - final List blockedServices; - final void Function(List) onConfirm; - - const ServicesModalWidget({ - Key? key, - required this.blockedServices, - required this.onConfirm, - required this.serversProvider, - required this.appConfigProvider - }) : super(key: key); - - @override - State createState() => _ServicesModalStateWidget(); -} - -class _ServicesModalStateWidget extends State { +class _ServicesModalStateWidget extends State { List blockedServices = []; - Future loadBlockedServices() async { - final result = await getBlockedServices(server: widget.serversProvider.selectedServer!); - if (result['result'] == 'success') { - widget.serversProvider.setBlockedServicesListLoadStatus(1, true); - widget.serversProvider.setBlockedServiceListData(result['data']); - } - else { - widget.serversProvider.setBlockedServicesListLoadStatus(2, true); - widget.appConfigProvider.addLog(result['log']); - } - } + @override void initState() { - if (widget.serversProvider.blockedServicesList.loadStatus != 1) { - loadBlockedServices(); + final filteringProvider = Provider.of(context, listen: false); + + if (filteringProvider.blockedServicesLoadStatus != LoadStatus.loaded) { + filteringProvider.getBlockedServices(); } blockedServices = widget.blockedServices; @@ -88,11 +51,11 @@ class _ServicesModalStateWidget extends State { @override Widget build(BuildContext context) { - final serversProvider = Provider.of(context); + final filteringProvider = Provider.of(context); Widget content() { - switch (serversProvider.blockedServicesList.loadStatus) { - case 0: + switch (filteringProvider.blockedServicesLoadStatus) { + case LoadStatus.loading: return Padding( padding: const EdgeInsets.all(24), child: SizedBox( @@ -116,34 +79,34 @@ class _ServicesModalStateWidget extends State { ), ); - case 1: + case LoadStatus.loaded: return SizedBox( width: double.minPositive, height: MediaQuery.of(context).size.height*0.5, child: ListView.builder( shrinkWrap: true, - itemCount: serversProvider.blockedServicesList.services!.length, + itemCount: filteringProvider.blockedServices!.services.length, itemBuilder: (context, index) => CheckboxListTile( title: Padding( padding: const EdgeInsets.only(left: 10), child: Text( - serversProvider.blockedServicesList.services![index].name, + filteringProvider.blockedServices!.services[index].name, style: TextStyle( fontWeight: FontWeight.normal, color: Theme.of(context).colorScheme.onSurface ), ), ), - value: blockedServices.contains(serversProvider.blockedServicesList.services![index].id), + value: blockedServices.contains(filteringProvider.blockedServices!.services[index].id), checkboxShape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5) ), - onChanged: (value) => checkUncheckService(value!, serversProvider.blockedServicesList.services![index].id) + onChanged: (value) => checkUncheckService(value!, filteringProvider.blockedServices!.services[index].id) ) ), ); - case 2: + case LoadStatus.error: return Padding( padding: const EdgeInsets.all(24), child: SizedBox( diff --git a/lib/screens/clients/client/settings_tile.dart b/lib/screens/clients/client/settings_tile.dart new file mode 100644 index 0000000..9b26b2c --- /dev/null +++ b/lib/screens/clients/client/settings_tile.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; + +class SettingsTile extends StatelessWidget { + final String label; + final bool? value; + final void Function(bool)? onChange; + final bool useGlobalSettingsFiltering; + + const SettingsTile({ + super.key, + required this.label, + required this.value, + this.onChange, + required this.useGlobalSettingsFiltering + }); + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onChange != null + ? value != null ? () => onChange!(!value!) : null + : null, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 6), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + label, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + useGlobalSettingsFiltering == false + ? Switch( + value: value!, + onChanged: onChange, + ) + : Padding( + padding: const EdgeInsets.symmetric( + vertical: 14, + horizontal: 12 + ), + child: Text( + "Global", + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ) + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/clients/tags_modal.dart b/lib/screens/clients/client/tags_modal.dart similarity index 55% rename from lib/screens/clients/tags_modal.dart rename to lib/screens/clients/client/tags_modal.dart index b8d0afe..0756c83 100644 --- a/lib/screens/clients/tags_modal.dart +++ b/lib/screens/clients/client/tags_modal.dart @@ -7,11 +7,11 @@ class TagsModal extends StatefulWidget { final void Function(List) onConfirm; const TagsModal({ - Key? key, + super.key, required this.selectedTags, required this.tags, required this.onConfirm, - }) : super(key: key); + }); @override State createState() => _TagsModalState(); @@ -68,20 +68,56 @@ class _TagsModalState extends State { child: ListView.builder( shrinkWrap: true, itemCount: widget.tags.length, - itemBuilder: (context, index) => CheckboxListTile( - title: Text( - widget.tags[index], - style: TextStyle( - fontWeight: FontWeight.normal, - color: Theme.of(context).colorScheme.onSurface + itemBuilder: (context, index) => Material( + color: Colors.transparent, + child: InkWell( + onTap: () => checkUncheckTag(!selectedTags.contains(widget.tags[index]), widget.tags[index]), + child: Container( + padding: const EdgeInsets.only( + left: 24, + top: 8, + bottom: 8, + right: 12 + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + widget.tags[index], + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + Checkbox( + value: selectedTags.contains(widget.tags[index]), + onChanged: (value) => checkUncheckTag(value!, widget.tags[index]), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5) + ), + ) + ], + ), ), ), - value: selectedTags.contains(widget.tags[index]), - checkboxShape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5) - ), - onChanged: (value) => checkUncheckTag(value!, widget.tags[index]) - ) + ), + // itemBuilder: (context, index) => CheckboxListTile( + // title: Text( + // widget.tags[index], + // style: TextStyle( + // fontWeight: FontWeight.normal, + // color: Theme.of(context).colorScheme.onSurface + // ), + // ), + // value: selectedTags.contains(widget.tags[index]), + // checkboxShape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(5) + // ), + // onChanged: (value) => checkUncheckTag(value!, widget.tags[index]) + // ) ), ), actions: [ diff --git a/lib/screens/clients/client/tags_section.dart b/lib/screens/clients/client/tags_section.dart new file mode 100644 index 0000000..8d63a0e --- /dev/null +++ b/lib/screens/clients/client/tags_section.dart @@ -0,0 +1,63 @@ +import 'package:adguard_home_manager/screens/clients/client/client_screen_functions.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class TagsSection extends StatelessWidget { + final List selectedTags; + final void Function(List) onTagsSelected; + + const TagsSection({ + super.key, + required this.selectedTags, + required this.onTagsSelected + }); + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () => openTagsModal( + context: context, + selectedTags: selectedTags, + onSelectedTags: onTagsSelected + ) , + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 0, horizontal: 16 + ), + child: Row( + children: [ + Icon( + Icons.label_rounded, + color: Theme.of(context).listTileTheme.iconColor + ), + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppLocalizations.of(context)!.selectTags, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface + ), + ), + const SizedBox(height: 3), + Text( + selectedTags.isNotEmpty + ? "${selectedTags.length} ${AppLocalizations.of(context)!.tagsSelected}" + : AppLocalizations.of(context)!.noTagsSelected, + style: TextStyle( + color: Theme.of(context).listTileTheme.iconColor + ), + ) + ], + ) + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/clients/client/upstream_servers_section.dart b/lib/screens/clients/client/upstream_servers_section.dart new file mode 100644 index 0000000..56fb7aa --- /dev/null +++ b/lib/screens/clients/client/upstream_servers_section.dart @@ -0,0 +1,112 @@ +import 'package:adguard_home_manager/screens/clients/client/client_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:uuid/uuid.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/widgets/section_label.dart'; + +class UpstreamServersSection extends StatefulWidget { + final List upstreamServers; + final void Function() onCheckValidValues; + final void Function(List) onUpdateUpstreamServers; + + const UpstreamServersSection({ + super.key, + required this.upstreamServers, + required this.onCheckValidValues, + required this.onUpdateUpstreamServers + }); + + @override + State createState() => _UpstreamServersSectionState(); +} + +class _UpstreamServersSectionState extends State { + final Uuid uuid = const Uuid(); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SectionLabel( + label: AppLocalizations.of(context)!.upstreamServers, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), + ), + Padding( + padding: const EdgeInsets.only(right: 12), + child: IconButton( + onPressed: () => setState(() => widget.upstreamServers.add( + ControllerListItem( + id: uuid.v4(), + controller: TextEditingController() + ) + )), + icon: const Icon(Icons.add) + ), + ) + ], + ), + if (widget.upstreamServers.isNotEmpty) ...widget.upstreamServers.map((controller) => Padding( + padding: const EdgeInsets.only(left: 16, right: 12), + child: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: TextFormField( + controller: controller.controller, + onChanged: (_) => widget.onCheckValidValues, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.dns_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + labelText: AppLocalizations.of(context)!.serverAddress, + ), + ), + ), + const SizedBox(width: 12), + IconButton( + onPressed: () => widget.onUpdateUpstreamServers( + widget.upstreamServers.where((e) => e.id != controller.id).toList() + ), + icon: const Icon(Icons.remove_circle_outline_outlined) + ) + ], + ), + ), + )), + if (widget.upstreamServers.isEmpty) Container( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Column( + children: [ + Text( + AppLocalizations.of(context)!.noUpstreamServers, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 18, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 10), + Text( + AppLocalizations.of(context)!.willBeUsedGeneralServers, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/clients/client_screen.dart b/lib/screens/clients/client_screen.dart deleted file mode 100644 index 51dcb6e..0000000 --- a/lib/screens/clients/client_screen.dart +++ /dev/null @@ -1,678 +0,0 @@ -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/clients/remove_client_modal.dart'; -import 'package:adguard_home_manager/screens/clients/services_modal.dart'; -import 'package:adguard_home_manager/screens/clients/tags_modal.dart'; - -import 'package:adguard_home_manager/providers/servers_provider.dart'; -import 'package:adguard_home_manager/models/clients.dart'; - -class ClientScreen extends StatefulWidget { - final Client? client; - final void Function(Client) onConfirm; - final void Function(Client)? onDelete; - - const ClientScreen({ - Key? key, - this.client, - required this.onConfirm, - this.onDelete, - }) : super(key: key); - - @override - State createState() => _ClientScreenState(); -} - -class _ClientScreenState extends State { - final Uuid uuid = const Uuid(); - bool editMode = true; - - TextEditingController nameController = TextEditingController(); - - List selectedTags = []; - - List> identifiersControllers = [ - { - 'id': 0, - 'controller': TextEditingController() - } - ]; - - bool useGlobalSettingsFiltering = true; - bool? enableFiltering; - bool? enableSafeBrowsing; - bool? enableParentalControl; - bool? enableSafeSearch; - - bool useGlobalSettingsServices = true; - List blockedServices = []; - - List> upstreamServers = []; - - - bool checkValidValues() { - if ( - nameController.text != '' && - identifiersControllers.isNotEmpty && - identifiersControllers[0]['controller'].text != '' - ) { - return true; - } - else { - return false; - } - } - - @override - void initState() { - if (widget.client != null) { - editMode = false; - - nameController.text = widget.client!.name; - selectedTags = widget.client!.tags; - identifiersControllers = widget.client!.ids.map((e) => { - 'id': uuid.v4(), - 'controller': TextEditingController(text: e) - }).toList(); - useGlobalSettingsFiltering = widget.client!.useGlobalSettings; - enableFiltering = widget.client!.filteringEnabled; - enableParentalControl = widget.client!.parentalEnabled; - enableSafeBrowsing = widget.client!.safebrowsingEnabled; - enableSafeSearch = widget.client!.safesearchEnabled; - useGlobalSettingsServices = widget.client!.useGlobalBlockedServices; - blockedServices = widget.client!.blockedServices; - upstreamServers = widget.client!.upstreams.map((e) => { - 'id': uuid.v4(), - 'controller': TextEditingController(text: e) - }).toList(); - } - super.initState(); - } - - @override - Widget build(BuildContext context) { - final serversProvider = Provider.of(context); - - void createClient() { - final Client client = Client( - name: nameController.text, - ids: List.from(identifiersControllers.map((e) => e['controller'].text)), - useGlobalSettings: useGlobalSettingsFiltering, - filteringEnabled: enableFiltering ?? false, - parentalEnabled: enableParentalControl ?? false, - safebrowsingEnabled: enableSafeBrowsing ?? false, - safesearchEnabled: enableSafeSearch ?? false, - useGlobalBlockedServices: useGlobalSettingsServices, - blockedServices: blockedServices, - upstreams: List.from(upstreamServers.map((e) => e['controller'].text)), - tags: selectedTags - ); - widget.onConfirm(client); - } - - Widget sectionLabel({ - required String label, - EdgeInsets? padding - }) { - return Padding( - padding: padding ?? const EdgeInsets.symmetric( - vertical: 24, - horizontal: 24 - ), - child: Text( - label, - style: TextStyle( - fontWeight: FontWeight.w500, - fontSize: 16, - color: Theme.of(context).colorScheme.primary - ), - ), - ); - } - - void enableDisableGlobalSettingsFiltering() { - if (useGlobalSettingsFiltering == true) { - setState(() { - useGlobalSettingsFiltering = false; - - enableFiltering = false; - enableSafeBrowsing = false; - enableParentalControl = false; - enableSafeSearch = false; - }); - } - else if (useGlobalSettingsFiltering == false) { - setState(() { - useGlobalSettingsFiltering = true; - - enableFiltering = null; - enableSafeBrowsing = null; - enableParentalControl = null; - enableSafeSearch = null; - }); - } - } - - void openTagsModal() { - showDialog( - context: context, - builder: (context) => TagsModal( - selectedTags: selectedTags, - tags: serversProvider.clients.data!.supportedTags, - onConfirm: (selected) => setState(() => selectedTags = selected), - ) - ); - } - - void openServicesModal() { - showDialog( - context: context, - builder: (context) => ServicesModal( - blockedServices: blockedServices, - onConfirm: (values) => setState(() => blockedServices = values), - ) - ); - } - - void updateServicesGlobalSettings(bool value) { - if (value == true) { - setState(() { - blockedServices = []; - useGlobalSettingsServices = true; - }); - } - else if (value == false) { - setState(() { - useGlobalSettingsServices = false; - }); - } - } - - void openDeleteClientScreen() { - showDialog( - context: context, - builder: (ctx) => RemoveClientModal( - onConfirm: () { - Navigator.pop(context); - widget.onDelete!(widget.client!); - } - ) - ); - } - - Widget settignsTile({ - required String label, - required bool? value, - void Function(bool)? onChange - }) { - return Material( - color: Colors.transparent, - child: InkWell( - onTap: onChange != null - ? value != null ? () => onChange(!value) : null - : null, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 42, - vertical: 5 - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - label, - style: TextStyle( - fontSize: 16, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - useGlobalSettingsFiltering == false - ? Switch( - value: value!, - onChanged: onChange, - activeColor: Theme.of(context).colorScheme.primary, - ) - : Padding( - padding: const EdgeInsets.symmetric(vertical: 14), - child: Text( - "Global", - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ) - ], - ), - ), - ), - ); - } - - return Scaffold( - appBar: AppBar( - leading: IconButton( - onPressed: () => Navigator.pop(context), - icon: const Icon(Icons.close) - ), - title: Text( - widget.client != null - ? AppLocalizations.of(context)!.client - : AppLocalizations.of(context)!.addClient - ), - actions: [ - if (widget.client == null || (widget.client != null && editMode == true)) IconButton( - onPressed: checkValidValues() == true - ? () { - createClient(); - Navigator.pop(context); - } - : null, - icon: Icon( - widget.client != null && editMode == true - ? Icons.save_rounded - : Icons.check_rounded - ), - tooltip: widget.client != null && editMode == true - ? AppLocalizations.of(context)!.save - : AppLocalizations.of(context)!.confirm, - ), - if (widget.client != null && editMode == false) IconButton( - onPressed: () => setState(() => editMode = true), - icon: const Icon(Icons.edit_rounded), - tooltip: AppLocalizations.of(context)!.edit, - ), - if (widget.client != null) IconButton( - onPressed: openDeleteClientScreen, - icon: const Icon(Icons.delete_rounded), - tooltip: AppLocalizations.of(context)!.delete, - ), - const SizedBox(width: 10), - ], - ), - body: ListView( - children: [ - const SizedBox(height: 24), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: TextFormField( - enabled: widget.client != null ? false : true, - controller: nameController, - onChanged: (_) => checkValidValues(), - decoration: InputDecoration( - prefixIcon: const Icon(Icons.badge_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - labelText: AppLocalizations.of(context)!.name, - ), - ), - ), - sectionLabel(label: AppLocalizations.of(context)!.tags), - Material( - color: Colors.transparent, - child: InkWell( - onTap: editMode == true ? () => openTagsModal() : null, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 0, horizontal: 24 - ), - child: Row( - children: [ - Icon( - Icons.label_rounded, - color: Theme.of(context).listTileTheme.iconColor - ), - const SizedBox(width: 16), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - AppLocalizations.of(context)!.selectTags, - style: TextStyle( - fontSize: 16, - color: Theme.of(context).colorScheme.onSurface - ), - ), - const SizedBox(height: 3), - Text( - selectedTags.isNotEmpty - ? "${selectedTags.length} ${AppLocalizations.of(context)!.tagsSelected}" - : AppLocalizations.of(context)!.noTagsSelected, - style: TextStyle( - color: Theme.of(context).listTileTheme.iconColor - ), - ) - ], - ) - ], - ), - ), - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - sectionLabel( - label: AppLocalizations.of(context)!.identifiers, - padding: const EdgeInsets.only( - left: 24, right: 24, top: 24, bottom: 12 - ) - ), - if (editMode == true) Padding( - padding: const EdgeInsets.only(right: 20), - child: IconButton( - onPressed: () => setState(() => identifiersControllers.add( - Map.from({ - 'id': uuid.v4(), - 'controller': TextEditingController() - }) - )), - icon: const Icon(Icons.add) - ), - ) - ], - ), - if (identifiersControllers.isNotEmpty) ...identifiersControllers.map((controller) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: TextFormField( - enabled: editMode, - controller: controller['controller'], - onChanged: (_) => checkValidValues(), - decoration: InputDecoration( - prefixIcon: const Icon(Icons.tag), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - helperText: AppLocalizations.of(context)!.identifierHelper, - labelText: AppLocalizations.of(context)!.identifier, - ), - ), - ), - if (editMode == true) ...[ - const SizedBox(width: 20), - Padding( - padding: const EdgeInsets.only(bottom: 25), - child: IconButton( - onPressed: () => setState( - () => identifiersControllers = identifiersControllers.where((e) => e['id'] != controller['id']).toList() - ), - icon: const Icon(Icons.remove_circle_outline_outlined) - ), - ) - ] - ], - ), - )).toList(), - if (identifiersControllers.isEmpty) Container( - padding: const EdgeInsets.only(top: 10), - child: Text( - AppLocalizations.of(context)!.noIdentifiers, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 18, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ), - sectionLabel( - label: AppLocalizations.of(context)!.settings, - padding: const EdgeInsets.only( - left: 24, right: 24, top: 12, bottom: 24 - ) - ), - 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: editMode - ? () => enableDisableGlobalSettingsFiltering() - : null, - borderRadius: BorderRadius.circular(28), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 5 - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - AppLocalizations.of(context)!.useGlobalSettings, - style: TextStyle( - fontSize: 16, - color: Theme.of(context).colorScheme.onSurface - ), - ), - Switch( - value: useGlobalSettingsFiltering, - onChanged: editMode == true - ? (value) => enableDisableGlobalSettingsFiltering() - : null, - activeColor: Theme.of(context).colorScheme.primary, - ) - ], - ), - ), - ), - ), - ), - const SizedBox(height: 10), - settignsTile( - label: AppLocalizations.of(context)!.enableFiltering, - value: enableFiltering, - onChange: editMode == true - ? (value) => setState(() => enableFiltering = value) - : null - ), - settignsTile( - label: AppLocalizations.of(context)!.enableSafeBrowsing, - value: enableSafeBrowsing, - onChange: editMode == true - ? (value) => setState(() => enableSafeBrowsing = value) - : null - ), - settignsTile( - label: AppLocalizations.of(context)!.enableParentalControl, - value: enableParentalControl, - onChange: editMode == true - ? (value) => setState(() => enableParentalControl = value) - : null - ), - settignsTile( - label: AppLocalizations.of(context)!.enableSafeSearch, - value: enableSafeSearch, - onChange: editMode == true - ? (value) => setState(() => enableSafeSearch = value) - : null - ), - sectionLabel(label: AppLocalizations.of(context)!.blockedServices), - 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: editMode == true - ? () => updateServicesGlobalSettings(!useGlobalSettingsServices) - : null, - borderRadius: BorderRadius.circular(28), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 5 - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - AppLocalizations.of(context)!.useGlobalSettings, - style: TextStyle( - fontSize: 16, - color: Theme.of(context).colorScheme.onSurface - ), - ), - Switch( - value: useGlobalSettingsServices, - onChanged: editMode == true - ? (value) => updateServicesGlobalSettings(value) - : null, - activeColor: Theme.of(context).colorScheme.primary, - ) - ], - ), - ), - ), - ), - ), - const SizedBox(height: 10), - Material( - color: Colors.transparent, - child: InkWell( - onTap: editMode == true - ? useGlobalSettingsServices == false - ? openServicesModal - : null - : null, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, horizontal: 24 - ), - child: Row( - children: [ - Icon( - Icons.public, - color: useGlobalSettingsServices == false - ? Theme.of(context).listTileTheme.iconColor - : Theme.of(context).colorScheme.onSurface.withOpacity(0.38), - ), - const SizedBox(width: 16), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - AppLocalizations.of(context)!.selectBlockedServices, - style: TextStyle( - fontSize: 16, - color: useGlobalSettingsServices == false - ? Theme.of(context).colorScheme.onSurface - : Theme.of(context).colorScheme.onSurface.withOpacity(0.38), - ), - ), - if (useGlobalSettingsServices == false) ...[ - const SizedBox(height: 5), - Text( - blockedServices.isNotEmpty - ? "${blockedServices.length} ${AppLocalizations.of(context)!.servicesBlocked}" - : AppLocalizations.of(context)!.noBlockedServicesSelected, - style: TextStyle( - color: Theme.of(context).listTileTheme.iconColor - ), - ) - ] - ], - ) - ], - ), - ), - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - sectionLabel(label: AppLocalizations.of(context)!.upstreamServers), - if (editMode == true) Padding( - padding: const EdgeInsets.only(right: 20), - child: IconButton( - onPressed: () => setState(() => upstreamServers.add( - Map.from({ - 'id': uuid.v4(), - 'controller': TextEditingController() - }) - )), - icon: const Icon(Icons.add) - ), - ) - ], - ), - if (upstreamServers.isNotEmpty) ...upstreamServers.map((controller) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Padding( - padding: const EdgeInsets.only(bottom: 20), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox( - width: editMode == true - ? MediaQuery.of(context).size.width - 108 - : MediaQuery.of(context).size.width - 40, - child: TextFormField( - enabled: editMode, - controller: controller['controller'], - onChanged: (_) => checkValidValues(), - decoration: InputDecoration( - prefixIcon: const Icon(Icons.dns_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - labelText: AppLocalizations.of(context)!.serverAddress, - ), - ), - ), - if (editMode == true) ...[ - const SizedBox(width: 20), - IconButton( - onPressed: () => setState( - () => upstreamServers = upstreamServers.where((e) => e['id'] != controller['id']).toList() - ), - icon: const Icon(Icons.remove_circle_outline_outlined) - ) - ] - ], - ), - ), - )).toList(), - if (upstreamServers.isEmpty) Container( - padding: const EdgeInsets.only(top: 12), - child: Column( - children: [ - Text( - AppLocalizations.of(context)!.noUpstreamServers, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 18, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 10), - Text( - AppLocalizations.of(context)!.willBeUsedGeneralServers, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 15, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - const SizedBox(height: 20) - ], - ), - ); - } -} \ No newline at end of file diff --git a/lib/screens/clients/clients.dart b/lib/screens/clients/clients.dart index f411da9..2ca57f1 100644 --- a/lib/screens/clients/clients.dart +++ b/lib/screens/clients/clients.dart @@ -1,179 +1,54 @@ 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/clients/clients_list.dart'; -import 'package:adguard_home_manager/screens/clients/search_clients.dart'; -import 'package:adguard_home_manager/screens/clients/added_list.dart'; +import 'package:adguard_home_manager/screens/clients/clients_lists.dart'; -import 'package:adguard_home_manager/models/app_log.dart'; -import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/models/server.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; import 'package:adguard_home_manager/models/clients.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; -class Clients extends StatelessWidget { - const Clients({Key? key}) : super(key: key); +final clientsNavigatorKey = GlobalKey(); + +class Clients extends StatefulWidget { + const Clients({super.key}); @override - Widget build(BuildContext context) { - final serversProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - - return ClientsWidget( - server: serversProvider.selectedServer!, - setLoadingStatus: serversProvider.setClientsLoadStatus, - setClientsData: serversProvider.setClientsData, - setSelectedClientsTab: appConfigProvider.setSelectedClientsTab, - addLog: appConfigProvider.addLog, - ); - } + State createState() => _ClientsState(); } -class ClientsWidget extends StatefulWidget { - final Server server; - final void Function(int, bool) setLoadingStatus; - final void Function(ClientsData) setClientsData; - final void Function(int) setSelectedClientsTab; - final void Function(AppLog) addLog; - - const ClientsWidget({ - Key? key, - required this.server, - required this.setLoadingStatus, - required this.setClientsData, - required this.setSelectedClientsTab, - required this.addLog, - }) : super(key: key); - - @override - State createState() => _ClientsWidgetState(); -} - -class _ClientsWidgetState extends State with TickerProviderStateMixin { - late TabController tabController; - final ScrollController scrollController = ScrollController(); - - Future fetchClients() async { - widget.setLoadingStatus(0, false); - final result = await getClients(widget.server); - if (mounted) { - if (result['result'] == 'success') { - widget.setClientsData(result['data']); - widget.setLoadingStatus(1, true); - } - else { - widget.addLog(result['log']); - widget.setLoadingStatus(2, true); - } - } - } - - @override - void initState() { - fetchClients(); - super.initState(); - tabController = TabController( - initialIndex: 0, - length: 2, - vsync: this, - ); - tabController.addListener(() => widget.setSelectedClientsTab(tabController.index)); - } - +class _ClientsState extends State with TickerProviderStateMixin { List generateClientsList(List clients, List ips) { return clients.where((client) => ips.contains(client.ip)).toList(); } @override Widget build(BuildContext context) { - final serversProvider = Provider.of(context); - - return DefaultTabController( - length: 2, - child: NestedScrollView( - controller: scrollController, - headerSliverBuilder: ((context, innerBoxIsScrolled) { - return [ - SliverOverlapAbsorber( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - sliver: SliverSafeArea( - top: false, - sliver: SliverAppBar( - title: Text(AppLocalizations.of(context)!.clients), - pinned: true, - floating: true, - forceElevated: innerBoxIsScrolled, - actions: [ - if (serversProvider.clients.loadStatus == 1) ...[ - IconButton( - onPressed: () => { - Navigator.push(context, MaterialPageRoute( - builder: (context) => const SearchClients() - )) - }, - icon: const Icon(Icons.search), - tooltip: AppLocalizations.of(context)!.searchClients, - ), - const SizedBox(width: 10), - ] - ], - bottom: TabBar( - controller: tabController, - unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant, - tabs: [ - Tab( - icon: const Icon(Icons.devices), - text: AppLocalizations.of(context)!.activeClients, - ), - Tab( - icon: const Icon(Icons.add_rounded), - text: AppLocalizations.of(context)!.added, - ), - ] - ) - ), - ), - ) - ]; - }), - body: Container( - decoration: BoxDecoration( - color: Theme.of(context).scaffoldBackgroundColor, - border: Border( - top: BorderSide( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.1) - ) - ) - ), - child: TabBarView( - controller: tabController, + return Scaffold( + body: LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth > 900) { + return Row( children: [ - RefreshIndicator( - onRefresh: fetchClients, - child: ClientsList( - scrollController: scrollController, - loadStatus: serversProvider.clients.loadStatus, - data: serversProvider.clients.loadStatus == 1 - ? serversProvider.clients.data!.autoClientsData : [], - fetchClients: fetchClients, - ), - ), - RefreshIndicator( - onRefresh: fetchClients, - child: AddedList( - scrollController: scrollController, - loadStatus: serversProvider.clients.loadStatus, - data: serversProvider.clients.loadStatus == 1 - ? serversProvider.clients.data!.clients : [], - fetchClients: fetchClients, + const Expanded( + flex: 1, + child: ClientsLists( + splitView: true, ) ), - ] - ) - ), - ) + Expanded( + flex: 2, + child: Navigator( + key: clientsNavigatorKey, + onGenerateRoute: (settings) => MaterialPageRoute(builder: (ctx) => const SizedBox()), + ), + ) + ], + ); + } + else { + return const ClientsLists( + splitView: false, + ); + } + }, + ), ); } } \ No newline at end of file diff --git a/lib/screens/clients/clients_list.dart b/lib/screens/clients/clients_list.dart index ee371f3..bdff4ee 100644 --- a/lib/screens/clients/clients_list.dart +++ b/lib/screens/clients/clients_list.dart @@ -1,123 +1,111 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; +import 'package:adguard_home_manager/screens/clients/client/active_client_tile.dart'; +import 'package:adguard_home_manager/widgets/tab_content_list.dart'; + +import 'package:adguard_home_manager/providers/clients_provider.dart'; import 'package:adguard_home_manager/models/clients.dart'; class ClientsList extends StatelessWidget { - final ScrollController scrollController; - final int loadStatus; final List data; - final Future Function() fetchClients; + final void Function(AutoClient) onClientSelected; + final AutoClient? selectedClient; + final bool splitView; const ClientsList({ - Key? key, - required this.scrollController, - required this.loadStatus, + super.key, required this.data, - required this.fetchClients - }) : super(key: key); + required this.onClientSelected, + this.selectedClient, + required this.splitView, + }); @override Widget build(BuildContext context) { - switch (loadStatus) { - case 0: - return SizedBox( - width: double.maxFinite, - height: MediaQuery.of(context).size.height-171, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const CircularProgressIndicator(), - const SizedBox(height: 30), - Text( - AppLocalizations.of(context)!.loadingStatus, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ) - ], - ), - ); + final clientsProvider = Provider.of(context); - case 1: - if (data.isNotEmpty) { - return ListView.builder( - padding: const EdgeInsets.only(top: 0), - itemCount: data.length, - itemBuilder: (context, index) => CustomListTile( - title: data[index].name != '' - ? data[index].name! - : data[index].ip, - subtitle: data[index].name != '' - ? data[index].ip - : null, - trailing: Text( - data[index].source, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface - ), + return CustomTabContentList( + listPadding: splitView == true + ? const EdgeInsets.only(top: 8) + : null, + loadingGenerator: () => SizedBox( + width: double.maxFinite, + height: MediaQuery.of(context).size.height-171, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.loadingStatus, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, ), ) - ); - } - else { - return SizedBox( - width: double.maxFinite, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - AppLocalizations.of(context)!.noClientsList, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 30), - TextButton.icon( - onPressed: fetchClients, - icon: const Icon(Icons.refresh_rounded), - label: Text(AppLocalizations.of(context)!.refresh) - ) - ], - ), - ); - } - - case 2: - return SizedBox( - width: double.maxFinite, - height: MediaQuery.of(context).size.height-171, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon( - Icons.error, - color: Colors.red, - size: 50, + ], + ), + ), + itemsCount: data.length, + contentWidget: (index) => ActiveClientTile( + client: data[index], + onTap: onClientSelected, + splitView: splitView, + selectedClient: selectedClient, + ), + noData: SizedBox( + width: double.maxFinite, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + AppLocalizations.of(context)!.noClientsList, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + color: Theme.of(context).colorScheme.onSurfaceVariant, ), - const SizedBox(height: 30), - Text( - AppLocalizations.of(context)!.errorLoadServerStatus, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ) - ], - ), - ); - - default: - return const SizedBox(); - } + ), + const SizedBox(height: 30), + TextButton.icon( + onPressed: () => clientsProvider.fetchClients(updateLoading: false), + icon: const Icon(Icons.refresh_rounded), + label: Text(AppLocalizations.of(context)!.refresh) + ) + ], + ), + ), + errorGenerator: () => SizedBox( + width: double.maxFinite, + height: MediaQuery.of(context).size.height-171, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon( + Icons.error, + color: Colors.red, + size: 50, + ), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.errorLoadServerStatus, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ), + loadStatus: clientsProvider.loadStatus, + onRefresh: () => clientsProvider.fetchClients(updateLoading: false), + ); } } \ No newline at end of file diff --git a/lib/screens/clients/clients_lists.dart b/lib/screens/clients/clients_lists.dart new file mode 100644 index 0000000..a2cfa19 --- /dev/null +++ b/lib/screens/clients/clients_lists.dart @@ -0,0 +1,238 @@ +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/clients/clients.dart'; +import 'package:adguard_home_manager/screens/clients/added_list.dart'; +import 'package:adguard_home_manager/screens/clients/client/logs_list_client.dart'; +import 'package:adguard_home_manager/screens/clients/clients_list.dart'; + +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/functions/desktop_mode.dart'; +import 'package:adguard_home_manager/models/clients.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; +import 'package:adguard_home_manager/providers/clients_provider.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; + +class ClientsLists extends StatefulWidget { + final bool splitView; + + const ClientsLists({ + super.key, + required this.splitView, + }); + + @override + State createState() => _ClientsListsState(); +} + +class _ClientsListsState extends State with TickerProviderStateMixin { + late TabController tabController; + final ScrollController scrollController = ScrollController(); + + bool searchMode = false; + final TextEditingController searchController = TextEditingController(); + + AutoClient? _selectedAutoClient; + Client? _selectedClient; + + @override + void initState() { + final clientsProvider = Provider.of(context, listen: false); + clientsProvider.fetchClients(updateLoading: true); + + super.initState(); + tabController = TabController( + initialIndex: 0, + length: 2, + vsync: this, + ); + tabController.addListener( + () => Provider.of(context, listen: false).setSelectedClientsTab(tabController.index) + ); + } + + + @override + Widget build(BuildContext context) { + final serversProvider = Provider.of(context); + final clientsProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + + void onAutoClientSelected(AutoClient client) { + setState(() => _selectedAutoClient = client); + final w = LogsListClient( + ip: client.ip, + serversProvider: serversProvider, + appConfigProvider: appConfigProvider, + splitView: widget.splitView, + ); + if (widget.splitView) { + Navigator.of(clientsNavigatorKey.currentContext!).pushReplacement( + PageRouteBuilder( + pageBuilder: (context, animation1, animation2) => w, + transitionDuration: Duration.zero, + reverseTransitionDuration: Duration.zero, + ) + ); + } + else { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => w, + ) + ); + } + } + + void onClientSelected(Client client) { + setState(() => _selectedClient = client); + final w = LogsListClient( + ip: client.ids[0], + serversProvider: serversProvider, + appConfigProvider: appConfigProvider, + splitView: widget.splitView, + ); + if (widget.splitView) { + Navigator.of(clientsNavigatorKey.currentContext!).pushReplacement( + PageRouteBuilder( + pageBuilder: (context, animation1, animation2) => w, + transitionDuration: Duration.zero, + reverseTransitionDuration: Duration.zero, + ) + ); + } + else { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => w, + ) + ); + } + } + + return DefaultTabController( + length: 2, + child: NestedScrollView( + headerSliverBuilder: ((context, innerBoxIsScrolled) { + return [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverAppBar( + title: searchMode == true + ? Row( + children: [ + IconButton( + onPressed: () { + setState(() { + searchMode = false; + searchController.text = ""; + clientsProvider.setSearchTermClients(null); + }); + }, + icon: const Icon(Icons.arrow_back_rounded), + tooltip: AppLocalizations.of(context)!.exitSearch, + ), + const SizedBox(width: 16), + Expanded( + child: TextField( + controller: searchController, + onChanged: (value) => clientsProvider.setSearchTermClients(value), + decoration: InputDecoration( + suffixIcon: IconButton( + onPressed: () { + setState(() { + searchController.text = ""; + clientsProvider.setSearchTermClients(null); + }); + }, + icon: const Icon(Icons.clear_rounded) + ), + hintText: AppLocalizations.of(context)!.search, + hintStyle: const TextStyle( + fontWeight: FontWeight.normal, + fontSize: 18 + ), + border: InputBorder.none, + ), + style: const TextStyle( + fontWeight: FontWeight.normal, + fontSize: 18 + ), + autofocus: true, + ), + ) + ], + ) + : Text(AppLocalizations.of(context)!.clients), + pinned: true, + floating: true, + centerTitle: false, + forceElevated: innerBoxIsScrolled, + surfaceTintColor: isDesktop(MediaQuery.of(context).size.width) + ? Colors.transparent + : null, + actions: [ + if (clientsProvider.loadStatus == LoadStatus.loaded && searchMode == false) ...[ + IconButton( + onPressed: () => setState(() => searchMode = true), + icon: const Icon(Icons.search), + tooltip: AppLocalizations.of(context)!.searchClients, + ), + const SizedBox(width: 10), + ] + ], + bottom: TabBar( + controller: tabController, + unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant, + tabs: [ + Tab( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.devices), + const SizedBox(width: 8), + Text(AppLocalizations.of(context)!.activeClients) + ], + ), + ), + Tab( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.add_rounded), + const SizedBox(width: 8), + Text(AppLocalizations.of(context)!.added) + ], + ), + ), + ] + ) + ), + ) + ]; + }), + body: TabBarView( + controller: tabController, + children: [ + ClientsList( + data: clientsProvider.loadStatus == LoadStatus.loaded + ? clientsProvider.filteredActiveClients : [], + onClientSelected: onAutoClientSelected, + selectedClient: _selectedAutoClient, + splitView: widget.splitView, + ), + AddedList( + scrollController: scrollController, + data: clientsProvider.loadStatus == LoadStatus.loaded + ? clientsProvider.filteredAddedClients : [], + onClientSelected: onClientSelected, + selectedClient: _selectedClient, + splitView: widget.splitView, + ), + ] + ) + ) + ); + } +} \ No newline at end of file diff --git a/lib/screens/clients/fab.dart b/lib/screens/clients/fab.dart index 74477af..9571ae3 100644 --- a/lib/screens/clients/fab.dart +++ b/lib/screens/clients/fab.dart @@ -4,53 +4,47 @@ 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/clients/client_screen.dart'; +import 'package:adguard_home_manager/screens/clients/client/client_screen_functions.dart'; import 'package:adguard_home_manager/functions/snackbar.dart'; +import 'package:adguard_home_manager/providers/status_provider.dart'; +import 'package:adguard_home_manager/providers/clients_provider.dart'; import 'package:adguard_home_manager/models/clients.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; import 'package:adguard_home_manager/classes/process_modal.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; class ClientsFab extends StatelessWidget { - final int tab; - - const ClientsFab({ - Key? key, - required this.tab, - }) : super(key: key); + const ClientsFab({super.key}); @override Widget build(BuildContext context) { - final serversProvider = Provider.of(context); + final statusProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final clientsProvider = Provider.of(context); + + final width = MediaQuery.of(context).size.width; void confirmAddClient(Client client) async { - ProcessModal processModal = ProcessModal(context: context); + if (!context.mounted) return; + + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.addingClient); - final result = await postAddClient(server: serversProvider.selectedServer!, data: client.toJson()); + final result = await clientsProvider.addClient(client); + + if (!context.mounted) return; processModal.close(); - if (result['result'] == 'success') { - ClientsData clientsData = serversProvider.clients.data!; - clientsData.clients.add(client); - serversProvider.setClientsData(clientsData); - - showSnacbkar( - context: context, + if (result == true) { + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.clientAddedSuccessfully, color: Colors.green ); } else { - appConfigProvider.addLog(result['log']); - - showSnacbkar( - context: context, + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.clientNotAdded, color: Colors.red @@ -59,17 +53,21 @@ class ClientsFab extends StatelessWidget { } void openAddClient() { - Navigator.push(context, MaterialPageRoute( - fullscreenDialog: true, - builder: (BuildContext context) => ClientScreen( - onConfirm: confirmAddClient, - ) - )); + openClientFormModal( + context: context, + width: width, + onConfirm: confirmAddClient + ); } - return FloatingActionButton( - onPressed: () => openAddClient(), - child: const Icon(Icons.add), - ); + if (statusProvider.serverStatus != null) { + return FloatingActionButton( + onPressed: openAddClient, + child: const Icon(Icons.add), + ); + } + else { + return const SizedBox(); + } } } \ No newline at end of file diff --git a/lib/screens/clients/options_modal.dart b/lib/screens/clients/options_modal.dart deleted file mode 100644 index 61dea10..0000000 --- a/lib/screens/clients/options_modal.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -import 'package:adguard_home_manager/widgets/custom_list_tile_dialog.dart'; - -class OptionsModal extends StatelessWidget { - final void Function() onEdit; - final void Function() onDelete; - - const OptionsModal({ - Key? key, - required this.onDelete, - required this.onEdit, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return AlertDialog( - contentPadding: const EdgeInsets.symmetric( - horizontal: 0, - vertical: 16 - ), - title: Column( - children: [ - Icon( - Icons.more_horiz, - color: Theme.of(context).listTileTheme.iconColor - ), - const SizedBox(height: 16), - Text( - AppLocalizations.of(context)!.options, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface - ), - ) - ], - ), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: 24), - CustomListTileDialog( - onTap: () { - Navigator.pop(context); - onEdit(); - }, - title: AppLocalizations.of(context)!.edit, - icon: Icons.edit, - ), - CustomListTileDialog( - onTap: () { - Navigator.pop(context); - onDelete(); - }, - title: AppLocalizations.of(context)!.delete, - icon: Icons.delete, - ), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(AppLocalizations.of(context)!.close) - ) - ], - ); - } -} \ No newline at end of file diff --git a/lib/screens/clients/search_clients.dart b/lib/screens/clients/search_clients.dart index 9a570af..2654c24 100644 --- a/lib/screens/clients/search_clients.dart +++ b/lib/screens/clients/search_clients.dart @@ -5,46 +5,29 @@ import 'package:animations/animations.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:adguard_home_manager/screens/clients/remove_client_modal.dart'; -import 'package:adguard_home_manager/screens/clients/client_screen.dart'; -import 'package:adguard_home_manager/screens/clients/options_modal.dart'; +import 'package:adguard_home_manager/screens/clients/client/remove_client_modal.dart'; +import 'package:adguard_home_manager/screens/clients/client/client_screen_functions.dart'; +import 'package:adguard_home_manager/widgets/options_menu.dart'; +import 'package:adguard_home_manager/widgets/section_label.dart'; import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; +import 'package:adguard_home_manager/models/menu_option.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/clients_provider.dart'; import 'package:adguard_home_manager/models/clients.dart'; -import 'package:adguard_home_manager/widgets/section_label.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; +import 'package:adguard_home_manager/providers/status_provider.dart'; -class SearchClients extends StatelessWidget { - const SearchClients({Key? key}) : super(key: key); +class SearchClients extends StatefulWidget { + const SearchClients({super.key}); @override - Widget build(BuildContext context) { - final serversProvider = Provider.of(context); - - return SearchClientsWidget( - serversProvider: serversProvider, - ); - } + State createState() => _SearchClientsState(); } -class SearchClientsWidget extends StatefulWidget { - final ServersProvider serversProvider; - - const SearchClientsWidget({ - Key? key, - required this.serversProvider, - }) : super(key: key); - - @override - State createState() => _SearchClientsWidgetState(); -} - -class _SearchClientsWidgetState extends State { +class _SearchClientsState extends State { late ScrollController scrollController; final TextEditingController searchController = TextEditingController(); @@ -83,11 +66,13 @@ class _SearchClientsWidgetState extends State { @override void initState() { + final clientsProvider = Provider.of(context, listen: false); + scrollController = ScrollController()..addListener(scrollListener); setState(() { - clients = widget.serversProvider.clients.data!.clients; - autoClients = widget.serversProvider.clients.data!.autoClientsData; + clients = clientsProvider.clients!.clients; + autoClients = clientsProvider.clients!.autoClients; }); super.initState(); @@ -95,85 +80,53 @@ class _SearchClientsWidgetState extends State { @override Widget build(BuildContext context) { - final serversProvider = Provider.of(context); + final statusProvider = Provider.of(context); + final clientsProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + void deleteClient(Client client) async { - ProcessModal processModal = ProcessModal(context: context); + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.removingClient); - final result = await postDeleteClient(server: serversProvider.selectedServer!, name: client.name); + final result = await clientsProvider.deleteClient(client); processModal.close(); - if (result['result'] == 'success') { - ClientsData clientsData = serversProvider.clients.data!; - clientsData.clients = clientsData.clients.where((c) => c.name != client.name).toList(); - serversProvider.setClientsData(clientsData); - setState(() { - clients = clientsData.clients; - }); - search(searchController.text); - - showSnacbkar( - context: context, + if (result == true) { + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.clientDeletedSuccessfully, color: Colors.green ); } else { - appConfigProvider.addLog(result['log']); - - showSnacbkar( - context: context, + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.clientNotDeleted, color: Colors.red ); } - } + } void confirmEditClient(Client client) async { - ProcessModal processModal = ProcessModal(context: context); + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.addingClient); - final result = await postUpdateClient(server: serversProvider.selectedServer!, data: { - 'name': client.name, - 'data': client.toJson() - }); + final result = await clientsProvider.editClient(client); processModal.close(); - if (result['result'] == 'success') { - ClientsData clientsData = serversProvider.clients.data!; - clientsData.clients = clientsData.clients.map((e) { - if (e.name == client.name) { - return client; - } - else { - return e; - } - }).toList(); - serversProvider.setClientsData(clientsData); - - setState(() { - clients = clientsData.clients; - }); - search(searchController.text); - - showSnacbkar( - context: context, + if (result == true) { + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.clientUpdatedSuccessfully, color: Colors.green ); } else { - appConfigProvider.addLog(result['log']); - - showSnacbkar( - context: context, + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.clientNotUpdated, color: Colors.red @@ -182,14 +135,12 @@ class _SearchClientsWidgetState extends State { } void openClientModal(Client client) { - Navigator.push(context, MaterialPageRoute( - fullscreenDialog: true, - builder: (BuildContext context) => ClientScreen( - onConfirm: confirmEditClient, - onDelete: deleteClient, - client: client, - ) - )); + openClientFormModal( + context: context, + width: width, + onConfirm: confirmEditClient, + onDelete: deleteClient + ); } void openDeleteModal(Client client) { @@ -201,15 +152,15 @@ class _SearchClientsWidgetState extends State { ); } - void openOptionsModal(Client client) { - showModal( - context: context, - builder: (ctx) => OptionsModal( - onDelete: () => openDeleteModal(client), - onEdit: () => openClientModal(client), - ) - ); - } + // void openOptionsModal(Client client) { + // showModal( + // context: context, + // builder: (ctx) => OptionsModal( + // onDelete: () => openDeleteModal(client), + // onEdit: () => openClientModal(client), + // ) + // ); + // } return Scaffold( appBar: AppBar( @@ -254,7 +205,7 @@ class _SearchClientsWidgetState extends State { height: 1, decoration: BoxDecoration( color: showDivider == true - ? Theme.of(context).colorScheme.surfaceVariant + ? Theme.of(context).colorScheme.surfaceContainerHighest : Colors.transparent ), ), @@ -274,80 +225,96 @@ class _SearchClientsWidgetState extends State { primary: false, itemCount: clientsScreen.length, padding: const EdgeInsets.only(bottom: 0), - itemBuilder: (context, index) => ListTile( - contentPadding: index == 0 - ? const EdgeInsets.only(left: 20, right: 20, bottom: 15) - : const EdgeInsets.symmetric(horizontal: 20, vertical: 15), - isThreeLine: true, - onLongPress: () => openOptionsModal(clientsScreen[index]), - onTap: () => openClientModal(clientsScreen[index]), - title: Padding( - padding: const EdgeInsets.only(bottom: 5), - child: Text( - clientsScreen[index].name, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.normal + itemBuilder: (context, index) => OptionsMenu( + options: (v) => [ + MenuOption( + icon: Icons.edit_rounded, + title: AppLocalizations.of(context)!.edit, + action: () => openClientModal(v) + ), + MenuOption( + icon: Icons.delete_rounded, + title: AppLocalizations.of(context)!.delete, + action: () => openDeleteModal(v) + ), + ], + value: clientsScreen[index], + child: ListTile( + contentPadding: index == 0 + ? const EdgeInsets.only(left: 20, right: 20, bottom: 15) + : const EdgeInsets.symmetric(horizontal: 20, vertical: 15), + isThreeLine: true, + onTap: statusProvider.serverStatus != null + ? () => openClientModal(clientsScreen[index]) + : null, + title: Padding( + padding: const EdgeInsets.only(bottom: 5), + child: Text( + clientsScreen[index].name, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.normal + ), ), ), - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(clientsScreen[index].ids.toString().replaceAll(RegExp(r'^\[|\]$'), '')), - const SizedBox(height: 7), - Row( - children: [ - Icon( - Icons.filter_list_rounded, - size: 19, - color: clientsScreen[index].filteringEnabled == true - ? appConfigProvider.useThemeColorForStatus == true - ? Theme.of(context).colorScheme.primary - : Colors.green - : appConfigProvider.useThemeColorForStatus == true - ? Colors.grey - : Colors.red, - ), - const SizedBox(width: 10), - Icon( - Icons.vpn_lock_rounded, - size: 18, - color: clientsScreen[index].safebrowsingEnabled == true - ? appConfigProvider.useThemeColorForStatus == true - ? Theme.of(context).colorScheme.primary - : Colors.green - : appConfigProvider.useThemeColorForStatus == true - ? Colors.grey - : Colors.red, - ), - const SizedBox(width: 10), - Icon( - Icons.block, - size: 18, - color: clientsScreen[index].parentalEnabled == true - ? appConfigProvider.useThemeColorForStatus == true - ? Theme.of(context).colorScheme.primary - : Colors.green - : appConfigProvider.useThemeColorForStatus == true - ? Colors.grey - : Colors.red, - ), - const SizedBox(width: 10), - Icon( - Icons.search_rounded, - size: 19, - color: clientsScreen[index].safesearchEnabled == true - ? appConfigProvider.useThemeColorForStatus == true - ? Theme.of(context).colorScheme.primary - : Colors.green - : appConfigProvider.useThemeColorForStatus == true - ? Colors.grey - : Colors.red, - ) - ], - ) - ], + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(clientsScreen[index].ids.toString().replaceAll(RegExp(r'^\[|\]$'), '')), + const SizedBox(height: 7), + Row( + children: [ + Icon( + Icons.filter_list_rounded, + size: 19, + color: clientsScreen[index].filteringEnabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red, + ), + const SizedBox(width: 10), + Icon( + Icons.vpn_lock_rounded, + size: 18, + color: clientsScreen[index].safebrowsingEnabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red, + ), + const SizedBox(width: 10), + Icon( + Icons.block, + size: 18, + color: clientsScreen[index].parentalEnabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red, + ), + const SizedBox(width: 10), + Icon( + Icons.search_rounded, + size: 19, + color: clientsScreen[index].safeSearch != null && clientsScreen[index].safeSearch!.enabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red + ) + ], + ) + ], + ), ), ) ) diff --git a/lib/screens/connect/appbar.dart b/lib/screens/connect/appbar.dart deleted file mode 100644 index a75540f..0000000 --- a/lib/screens/connect/appbar.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -class ConnectAppBar extends StatelessWidget with PreferredSizeWidget { - const ConnectAppBar({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return AppBar( - title: Text(AppLocalizations.of(context)!.connect), - ); - } - - @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); -} \ No newline at end of file diff --git a/lib/screens/connect/connect.dart b/lib/screens/connect/connect.dart index ff55daf..01ac7cc 100644 --- a/lib/screens/connect/connect.dart +++ b/lib/screens/connect/connect.dart @@ -2,16 +2,16 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:provider/provider.dart'; import 'package:expandable/expandable.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/screens/connect/fab.dart'; -import 'package:adguard_home_manager/screens/connect/appbar.dart'; import 'package:adguard_home_manager/widgets/servers_list/servers_list.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; class Connect extends StatefulWidget { - const Connect({Key? key}) : super(key: key); + const Connect({super.key}); @override State createState() => _ConnectState(); @@ -58,26 +58,31 @@ class _ConnectState extends State { } return Scaffold( - appBar: const ConnectAppBar(), - body: Stack( - children: [ - ServersList( - context: context, - controllers: expandableControllerList, - onChange: expandOrContract, - scrollController: scrollController, - ), - AnimatedPositioned( - duration: const Duration(milliseconds: 100), - curve: Curves.easeInOut, - bottom: isVisible ? - appConfigProvider.showingSnackbar - ? 70 : 20 - : -70, - right: 20, - child: const FabConnect() - ) - ], + appBar: AppBar( + title: Text(AppLocalizations.of(context)!.connect), + ), + body: SafeArea( + child: Stack( + children: [ + ServersList( + context: context, + controllers: expandableControllerList, + onChange: expandOrContract, + scrollController: scrollController, + breakingWidth: 700, + ), + AnimatedPositioned( + duration: const Duration(milliseconds: 100), + curve: Curves.easeInOut, + bottom: isVisible ? + appConfigProvider.showingSnackbar + ? 90 : 20 + : -90, + right: 20, + child: const FabConnect() + ) + ], + ), ), ); } diff --git a/lib/screens/connect/fab.dart b/lib/screens/connect/fab.dart index 0c3204d..f826485 100644 --- a/lib/screens/connect/fab.dart +++ b/lib/screens/connect/fab.dart @@ -1,17 +1,17 @@ -import 'package:adguard_home_manager/widgets/add_server_modal.dart'; import 'package:flutter/material.dart'; +import 'package:adguard_home_manager/widgets/add_server/add_server_functions.dart'; + class FabConnect extends StatelessWidget { - const FabConnect({Key? key}) : super(key: key); + const FabConnect({super.key}); @override Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width; + void openAddServerModal() async { await Future.delayed(const Duration(seconds: 0), (() => { - Navigator.push(context, MaterialPageRoute( - fullscreenDialog: true, - builder: (BuildContext context) => const AddServerModal() - )) + openServerFormModal(context: context, width: width) })); } diff --git a/lib/screens/filters/add_button.dart b/lib/screens/filters/add_button.dart new file mode 100644 index 0000000..e2de2ff --- /dev/null +++ b/lib/screens/filters/add_button.dart @@ -0,0 +1,231 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/screens/filters/modals/custom_rules/edit_custom_rules.dart'; +import 'package:adguard_home_manager/screens/filters/modals/custom_rules/add_custom_rule.dart'; +import 'package:adguard_home_manager/screens/filters/details/add_list_modal.dart'; + +import 'package:adguard_home_manager/providers/filtering_provider.dart'; +import 'package:adguard_home_manager/functions/snackbar.dart'; +import 'package:adguard_home_manager/classes/process_modal.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; + +class AddFiltersButton extends StatelessWidget { + final String type; + final Widget Function(void Function()) widget; + + const AddFiltersButton({ + super.key, + required this.type, + required this.widget + }); + + @override + Widget build(BuildContext context) { + final filteringProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + + final width = MediaQuery.of(context).size.width; + + void confirmAddRule(String rule) async { + if (!context.mounted) return; + + ProcessModal processModal = ProcessModal(); + processModal.open(AppLocalizations.of(context)!.addingRule); + + final result = await filteringProvider.addCustomRule(rule); + + processModal.close(); + + if (!context.mounted) return; + if (result == true) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.ruleAddedSuccessfully, + color: Colors.green + ); + } + else { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.ruleNotAdded, + color: Colors.red + ); + } + } + + void confirmEditCustomRules(List rules) async { + if (!context.mounted) return; + + ProcessModal processModal = ProcessModal(); + processModal.open(AppLocalizations.of(context)!.savingCustomRules); + + final result = await filteringProvider.setCustomRules(rules); + + processModal.close(); + + if (!context.mounted) return; + if (result == true) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.customRulesUpdatedSuccessfully, + color: Colors.green + ); + } + else { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.customRulesNotUpdated, + color: Colors.red + ); + } + } + + void openAddCustomRule() { + showGeneralDialog( + context: context, + barrierColor: !(width > 700 || !(Platform.isAndroid || Platform.isIOS)) + ?Colors.transparent + : Colors.black54, + transitionBuilder: (context, anim1, anim2, child) { + return SlideTransition( + position: Tween( + begin: const Offset(0, 1), + end: const Offset(0, 0) + ).animate( + CurvedAnimation( + parent: anim1, + curve: Curves.easeInOutCubicEmphasized + ) + ), + child: child, + ); + }, + pageBuilder: (context, animation, secondaryAnimation) => AddCustomRule( + fullScreen: !(width > 700 || !(Platform.isAndroid || Platform.isIOS)), + onConfirm: confirmAddRule, + ), + ); + } + + void openEditCustomRule() { + showGeneralDialog( + context: context, + barrierColor: !(width > 700 || !(Platform.isAndroid || Platform.isIOS)) + ?Colors.transparent + : Colors.black54, + transitionBuilder: (context, anim1, anim2, child) { + return SlideTransition( + position: Tween( + begin: const Offset(0, 1), + end: const Offset(0, 0) + ).animate( + CurvedAnimation( + parent: anim1, + curve: Curves.easeInOutCubicEmphasized + ) + ), + child: child, + ); + }, + pageBuilder: (context, animation, secondaryAnimation) => EditCustomRules( + fullScreen: !(width > 700 || !(Platform.isAndroid || Platform.isIOS)), + onConfirm: confirmEditCustomRules, + ), + ); + } + + void confirmAddList({required String name, required String url, required String type}) async { + if (!context.mounted) return; + + ProcessModal processModal = ProcessModal(); + processModal.open(AppLocalizations.of(context)!.addingList); + + final result = await filteringProvider.addList(name: name, url: url, type: type); + + processModal.close(); + + if (!context.mounted) return; + if (result['success'] == true) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: "${AppLocalizations.of(context)!.listAdded} ${result['data']}.", + color: Colors.green + ); + } + else if (result['success'] == false && result['error'] == 'invalid_url') { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.listUrlInvalid, + color: Colors.red + ); + } + else if (result['success'] == false && result['error'] == 'url_exists') { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.listAlreadyAdded, + color: Colors.red + ); + } + else { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.listNotAdded, + color: Colors.red + ); + } + } + + void openAddWhitelistBlacklist() { + if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + context: context, + builder: (ctx) => AddListModal( + type: type, + onConfirm: confirmAddList, + dialog: true, + ), + ); + } + else { + showModalBottomSheet( + context: context, + useRootNavigator: true, + builder: (ctx) => AddListModal( + type: type, + onConfirm: confirmAddList, + dialog: false, + ), + isScrollControlled: true, + backgroundColor: Colors.transparent + ); + } + } + + switch (type) { + case 'blacklist': + case 'whitelist': + return widget( + () => openAddWhitelistBlacklist(), + ); + + case 'add_custom_rule': + return widget( + () => openAddCustomRule(), + ); + + case 'edit_custom_rule': + return widget( + () => openEditCustomRule(), + ); + + default: + return const SizedBox(); + } + } +} \ No newline at end of file diff --git a/lib/screens/filters/add_custom_rule.dart b/lib/screens/filters/add_custom_rule.dart deleted file mode 100644 index 82b6bb9..0000000 --- a/lib/screens/filters/add_custom_rule.dart +++ /dev/null @@ -1,440 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:provider/provider.dart'; -import 'package:flutter_web_browser/flutter_web_browser.dart'; - -import 'package:adguard_home_manager/constants/urls.dart'; -import 'package:adguard_home_manager/providers/app_config_provider.dart'; - -class AddCustomRule extends StatefulWidget { - final void Function(String) onConfirm; - - const AddCustomRule({ - Key? key, - required this.onConfirm - }) : super(key: key); - - @override - State createState() => _AddCustomRuleState(); -} - -enum BlockingPresets { block, unblock, custom } - -class _AddCustomRuleState extends State { - final TextEditingController domainController = TextEditingController(); - String? domainError; - - BlockingPresets preset = BlockingPresets.block; - - bool addImportant = false; - - bool checkValidValues() { - if ( - domainController.text != '' && - domainError == null - ) { - return true; - } - else { - return false; - } - } - - void validateDomain(String value) { - final domainRegex = RegExp(r'^(([a-zA-Z]{1})|([a-zA-Z]{1}[a-zA-Z]{1})|([a-zA-Z]{1}[0-9]{1})|([0-9]{1}[a-zA-Z]{1})|([a-zA-Z0-9][a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]))\.([a-zA-Z]{2,6}|[a-zA-Z0-9-]{2,30}\.[a-zA-Z]{2,3})$'); - if (domainRegex.hasMatch(value)) { - setState(() => domainError = null); - } - else { - setState(() => domainError = AppLocalizations.of(context)!.domainNotValid); - } - checkValidValues(); - } - - String buildRule({String?value}) { - String rule = ""; - - String fieldValue = value ?? domainController.text; - - if (preset == BlockingPresets.block) { - rule = "||${fieldValue.trim()}^"; - } - else if (preset == BlockingPresets.unblock) { - rule = "@@||${fieldValue.trim()}^"; - } - else { - rule = fieldValue.trim(); - } - - if (addImportant == true) { - rule = "$rule\$important"; - } - - return rule; - } - - void openDocsPage() { - FlutterWebBrowser.openWebPage( - url: Urls.customRuleDocs, - customTabsOptions: const CustomTabsOptions( - instantAppsEnabled: true, - showTitle: true, - urlBarHidingEnabled: false, - ), - safariVCOptions: const SafariViewControllerOptions( - barCollapsingEnabled: true, - dismissButtonStyle: SafariViewControllerDismissButtonStyle.close, - modalPresentationCapturesStatusBarAppearance: true, - ) - ); - } - - @override - Widget build(BuildContext context) { - final appConfigProvider = Provider.of(context); - - Map presets = { - 0: Text( - AppLocalizations.of(context)!.block, - style: TextStyle( - color: appConfigProvider.useDynamicColor == true - ? Theme.of(context).floatingActionButtonTheme.foregroundColor! - : preset == 0 - ? Colors.white - : Theme.of(context).colorScheme.primary, - fontSize: 14, - fontWeight: FontWeight.w500 - ), - ), - 1: Text( - AppLocalizations.of(context)!.unblock, - style: TextStyle( - color: appConfigProvider.useDynamicColor == true - ? Theme.of(context).floatingActionButtonTheme.foregroundColor! - : preset == 1 - ? Colors.white - : Theme.of(context).colorScheme.primary, - fontSize: 14, - fontWeight: FontWeight.w500 - ), - ), - 2: Text( - AppLocalizations.of(context)!.custom, - style: TextStyle( - color: appConfigProvider.useDynamicColor == true - ? Theme.of(context).floatingActionButtonTheme.foregroundColor! - : preset == 2 - ? Colors.white - : Theme.of(context).colorScheme.primary, - fontSize: 14, - fontWeight: FontWeight.w500 - ), - ), - }; - - return Scaffold( - appBar: AppBar( - title: Text(AppLocalizations.of(context)!.addCustomRule), - actions: [ - IconButton( - onPressed: checkValidValues() == true - ? () { - Navigator.pop(context); - widget.onConfirm(buildRule()); - } - : null, - icon: const Icon(Icons.check) - ), - const SizedBox(width: 10) - ], - ), - body: ListView( - children: [ - const SizedBox(height: 24), - Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 5 - ), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary.withOpacity(0.1), - borderRadius: BorderRadius.circular(30), - border: Border.all( - color: Theme.of(context).colorScheme.primary - ) - ), - child: Text( - buildRule(), - textAlign: TextAlign.center, - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.w500 - ), - ) - ), - ], - ), - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: TextFormField( - controller: domainController, - onChanged: (value) => setState(() => {}), - decoration: InputDecoration( - prefixIcon: const Icon(Icons.link_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - errorText: domainError, - labelText: AppLocalizations.of(context)!.domain, - ), - ), - ), - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: SegmentedButton( - segments: [ - ButtonSegment( - value: BlockingPresets.block, - label: Text(AppLocalizations.of(context)!.block) - ), - ButtonSegment( - value: BlockingPresets.unblock, - label: Text(AppLocalizations.of(context)!.unblock) - ), - ButtonSegment( - value: BlockingPresets.custom, - label: Text(AppLocalizations.of(context)!.custom) - ), - ], - selected: {preset}, - onSelectionChanged: (value) => setState(() => preset = value.first), - ), - ), - const SizedBox(height: 20), - Material( - color: Colors.transparent, - child: InkWell( - onTap: () => setState(() => addImportant = !addImportant), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 28), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only(left: 10), - child: Text( - AppLocalizations.of(context)!.addImportant, - style: TextStyle( - fontSize: 16, - color: Theme.of(context).colorScheme.onSurface - ), - ), - ), - Switch( - value: addImportant, - onChanged: (value) => setState(() => addImportant = value), - activeColor: Theme.of(context).colorScheme.primary, - ) - ], - ), - ), - ), - ), - const SizedBox(height: 20), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Card( - child: Padding( - padding: const EdgeInsets.all(20), - child: Column( - children: [ - Row( - children: [ - Icon( - Icons.info, - color: Theme.of(context).colorScheme.onSurface - ), - const SizedBox(width: 20), - Text( - AppLocalizations.of(context)!.examples, - style: TextStyle( - fontSize: 18, - color: Theme.of(context).colorScheme.onSurface - ), - ) - ], - ), - const SizedBox(height: 20), - SizedBox( - width: double.maxFinite, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "||example.org^", - textAlign: TextAlign.left, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.primary - ), - ), - const SizedBox(height: 5), - Text( - AppLocalizations.of(context)!.example1, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.primary - ), - ), - const SizedBox(height: 20), - Text( - "@@||example.org^", - textAlign: TextAlign.left, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.primary - ), - ), - const SizedBox(height: 5), - Text( - AppLocalizations.of(context)!.example2, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.primary - ), - ), - const SizedBox(height: 20), - Text( - "! Here goes a comment", - textAlign: TextAlign.left, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.primary - ), - ), - Text( - "# Also a comment", - textAlign: TextAlign.left, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.primary - ), - ), - const SizedBox(height: 5), - Text( - AppLocalizations.of(context)!.example3, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.primary - ), - ), - const SizedBox(height: 20), - Text( - "/REGEX/", - textAlign: TextAlign.left, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.primary - ), - ), - const SizedBox(height: 5), - Text( - AppLocalizations.of(context)!.example4, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.primary - ), - ), - ], - ), - ) - ], - ), - ), - ), - ), - const SizedBox(height: 20), - Material( - color: Colors.transparent, - child: InkWell( - onTap: openDocsPage, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only(left: 10), - child: Text( - AppLocalizations.of(context)!.moreInformation, - style: TextStyle( - fontSize: 16, - color: Theme.of(context).colorScheme.onSurface - ), - ), - ), - Padding( - padding: const EdgeInsets.only(right: 15), - child: Icon( - Icons.open_in_new, - color: Theme.of(context).colorScheme.onSurface - ), - ) - ], - ), - ), - ), - ), - const SizedBox(height: 20) - ], - ), - ); - } -} - // Padding( - // padding: const EdgeInsets.only( - // left: 28, - // right: 28, - // top: 20, - // bottom: 28 - // ), - // child: Row( - // mainAxisAlignment: MainAxisAlignment.end, - // children: [ - // TextButton( - // onPressed: () => Navigator.pop(context), - // child: Text(AppLocalizations.of(context)!.cancel) - // ), - // const SizedBox(width: 20), - // TextButton( - // onPressed: checkValidValues() == true - // ? () { - // Navigator.pop(context); - // widget.onConfirm(buildRule()); - // } - // : null, - // child: Text( - // AppLocalizations.of(context)!.confirm, - // style: TextStyle( - // color: checkValidValues() == true - // ? Theme.of(context).colorScheme.primary - // : Colors.grey - // ), - // ) - // ), - // ], - // ), - // ) \ No newline at end of file diff --git a/lib/screens/filters/add_list_modal.dart b/lib/screens/filters/add_list_modal.dart deleted file mode 100644 index d76d55b..0000000 --- a/lib/screens/filters/add_list_modal.dart +++ /dev/null @@ -1,201 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -import 'package:adguard_home_manager/models/filtering.dart'; - -class AddListModal extends StatefulWidget { - final String type; - final Filter? list; - final void Function({required String name, required String url, required String type})? onConfirm; - final void Function({required Filter list, required String type})? onEdit; - - const AddListModal({ - Key? key, - required this.type, - this.list, - this.onConfirm, - this.onEdit, - }) : super(key: key); - - @override - State createState() => _AddListModalState(); -} - -class _AddListModalState extends State { - final TextEditingController nameController = TextEditingController(); - final TextEditingController urlController = TextEditingController(); - String? urlError; - - bool validData = false; - - void checkValidValues() { - if (nameController.text != '' && urlController.text != '') { - setState(() => validData = true); - } - else { - setState(() => validData = false); - } - } - - void validateUrl(String value) { - final urlRegex = RegExp(r'^(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})$'); - if (urlRegex.hasMatch(value)) { - setState(() => urlError = null); - } - else { - final pathRegex = RegExp(r'^(((\\|\/)[a-z0-9^&@{}\[\],$=!\-#\(\)%\.\+~_]+)*(\\|\/))([^\\\/:\*\<>\|]+\.[a-z0-9]+)$'); - if (pathRegex.hasMatch(value)) { - setState(() => urlError = null); - } - else { - setState(() => urlError = AppLocalizations.of(context)!.urlNotValid); - } - } - } - - @override - void initState() { - if (widget.list != null) { - nameController.text = widget.list!.name; - urlController.text = widget.list!.url; - - validData = true; - } - super.initState(); - } - - @override - Widget build(BuildContext context) { - return Padding( - padding: MediaQuery.of(context).viewInsets, - child: Container( - height: 370, - decoration: BoxDecoration( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28) - ), - color: Theme.of(context).dialogBackgroundColor - ), - child: Column( - children: [ - Expanded( - child: ListView( - physics: 410 < MediaQuery.of(context).size.height - ? const NeverScrollableScrollPhysics() - : null, - children: [ - Padding( - padding: const EdgeInsets.only(top: 24), - child: Icon( - widget.type == 'whitelist' - ? Icons.verified_user_rounded - : Icons.gpp_bad_rounded, - size: 24, - color: Theme.of(context).listTileTheme.iconColor - ), - ), - const SizedBox(height: 16), - Text( - widget.list != null - ? widget.type == 'whitelist' - ? AppLocalizations.of(context)!.editWhitelist - : AppLocalizations.of(context)!.editBlacklist - : widget.type == 'whitelist' - ? AppLocalizations.of(context)!.addWhitelist - : AppLocalizations.of(context)!.addBlacklist, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSurface - ), - ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: TextFormField( - controller: nameController, - onChanged: (_) => checkValidValues(), - decoration: InputDecoration( - prefixIcon: const Icon(Icons.badge_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - labelText: AppLocalizations.of(context)!.name, - ), - ), - ), - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: TextFormField( - controller: urlController, - onChanged: validateUrl, - enabled: widget.list != null ? false : true, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.link_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - errorText: urlError, - labelText: AppLocalizations.of(context)!.urlAbsolutePath, - ), - ), - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.all(24), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(AppLocalizations.of(context)!.cancel) - ), - const SizedBox(width: 20), - TextButton( - onPressed: () { - Navigator.pop(context); - if (widget.list != null) { - final Filter newList = Filter( - url: urlController.text, - name: nameController.text, - lastUpdated: widget.list!.lastUpdated, - id: widget.list!.id, - rulesCount: widget.list!.rulesCount, - enabled: widget.list!.enabled - ); - widget.onEdit!( - list: newList, - type: widget.type - ); - } - else { - widget.onConfirm!( - name: nameController.text, - url: urlController.text, - type: widget.type - ); - } - }, - child: Text( - widget.list != null - ? AppLocalizations.of(context)!.save - : AppLocalizations.of(context)!.confirm - ) - ), - ], - ), - ) - ], - ), - ), - ); - } -} \ No newline at end of file diff --git a/lib/screens/filters/blocked_services_scren.dart b/lib/screens/filters/blocked_services_scren.dart deleted file mode 100644 index 710a714..0000000 --- a/lib/screens/filters/blocked_services_scren.dart +++ /dev/null @@ -1,227 +0,0 @@ -// ignore_for_file: use_build_context_synchronously - -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -import 'package:adguard_home_manager/models/blocked_services.dart'; -import 'package:adguard_home_manager/functions/snackbar.dart'; -import 'package:adguard_home_manager/classes/process_modal.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; -import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; - -class BlockedServicesScreen extends StatelessWidget { - const BlockedServicesScreen({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final serversProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - - return BlockedServicesScreenWidget( - serversProvider: serversProvider, - appConfigProvider: appConfigProvider - ); - } -} - -class BlockedServicesScreenWidget extends StatefulWidget { - final ServersProvider serversProvider; - final AppConfigProvider appConfigProvider; - - const BlockedServicesScreenWidget({ - Key? key, - required this.serversProvider, - required this.appConfigProvider, - }) : super(key: key); - - @override - State createState() => _BlockedServicesScreenStateWidget(); -} - -class _BlockedServicesScreenStateWidget extends State { - List values = []; - - Future loadBlockedServices() async { - final result = await getBlockedServices(server: widget.serversProvider.selectedServer!); - if (result['result'] == 'success') { - widget.serversProvider.setBlockedServicesListLoadStatus(1, true); - widget.serversProvider.setBlockedServiceListData(result['data']); - } - else { - widget.serversProvider.setBlockedServicesListLoadStatus(2, true); - widget.appConfigProvider.addLog(result['log']); - } - } - - @override - void initState() { - if (widget.serversProvider.blockedServicesList.loadStatus != 1) { - loadBlockedServices(); - } - - values = widget.serversProvider.filtering.data!.blockedServices; - - super.initState(); - } - - @override - Widget build(BuildContext context) { - final serversProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - - void updateValues(bool value, BlockedService item) { - if (value == true) { - setState(() { - values = values.where((v) => v != item.id).toList(); - }); - } - else { - setState(() { - values.add(item.id); - }); - } - } - - void updateBlockedServices() async { - ProcessModal processModal = ProcessModal(context: context); - processModal.open(AppLocalizations.of(context)!.updating); - - final result = await setBlockedServices(server: serversProvider.selectedServer!, data: values); - - processModal.close(); - - if (result['result'] == 'success') { - serversProvider.setBlockedServices(values); - - showSnacbkar( - context: context, - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.blockedServicesUpdated, - color: Colors.green - ); - } - else { - showSnacbkar( - context: context, - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.blockedServicesNotUpdated, - color: Colors.red - ); - } - } - - Widget body() { - switch (serversProvider.blockedServicesList.loadStatus) { - case 0: - return SizedBox( - width: double.maxFinite, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const CircularProgressIndicator(), - const SizedBox(height: 30), - Text( - AppLocalizations.of(context)!.loadingBlockedServicesList, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ) - ], - ), - ); - - case 1: - return ListView.builder( - itemCount: serversProvider.blockedServicesList.services!.length, - itemBuilder: (context, index) => Material( - color: Colors.transparent, - child: InkWell( - onTap: () => updateValues( - values.contains(serversProvider.blockedServicesList.services![index].id), - serversProvider.blockedServicesList.services![index] - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 5), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - serversProvider.blockedServicesList.services![index].name, - style: TextStyle( - fontSize: 16, - color: Theme.of(context).colorScheme.onSurface - ), - ), - Checkbox( - value: values.contains(serversProvider.blockedServicesList.services![index].id), - onChanged: (value) => updateValues( - value!, - serversProvider.blockedServicesList.services![index] - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5) - ), - ) - ], - ), - ), - ), - ) - ); - - case 2: - return SizedBox( - width: double.maxFinite, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon( - Icons.error, - color: Colors.red, - size: 50, - ), - const SizedBox(height: 30), - Text( - AppLocalizations.of(context)!.blockedServicesListNotLoaded, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ) - ], - ), - ); - - default: - return const SizedBox(); - } - } - - return Scaffold( - appBar: AppBar( - title: Text(AppLocalizations.of(context)!.blockedServices), - actions: [ - IconButton( - onPressed: updateBlockedServices, - icon: const Icon( - Icons.save_rounded - ), - tooltip: AppLocalizations.of(context)!.save, - ), - const SizedBox(width: 10) - ], - ), - body: RefreshIndicator( - onRefresh: loadBlockedServices, - child: body() - ), - ); - } -} \ No newline at end of file diff --git a/lib/screens/filters/check_host_modal.dart b/lib/screens/filters/check_host_modal.dart deleted file mode 100644 index 18f1f0f..0000000 --- a/lib/screens/filters/check_host_modal.dart +++ /dev/null @@ -1,242 +0,0 @@ -// ignore_for_file: use_build_context_synchronously - -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/functions/get_filtered_status.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; - -class CheckHostModal extends StatefulWidget { - const CheckHostModal({Key? key}) : super(key: key); - - @override - State createState() => _CheckHostModalState(); -} - -class _CheckHostModalState extends State { - final TextEditingController domainController = TextEditingController(); - String? domainError; - - Widget? resultWidget; - - void validateDomain(String value) { - final domainRegex = RegExp(r'^([a-z0-9|-]+\.)*[a-z0-9|-]+\.[a-z]+$'); - if (domainRegex.hasMatch(value)) { - setState(() => domainError = null); - } - else { - setState(() => domainError = AppLocalizations.of(context)!.domainNotValid); - } - } - - Widget checking() { - return SizedBox( - height: 30, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const CircularProgressIndicator(), - const SizedBox(width: 20), - Text(AppLocalizations.of(context)!.checkingHost) - ], - ), - ); - } - - @override - Widget build(BuildContext context) { - final serversProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - - void checkHost() async { - if (mounted) { - setState(() => resultWidget = checking()); - - final result = await checkHostFiltered(server: serversProvider.selectedServer!, host: domainController.text); - - if (result['result'] == 'success') { - final status = getFilteredStatus(context, appConfigProvider, result['data']['reason'], true); - if (mounted) { - setState(() => resultWidget = Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - status['icon'], - size: 18, - color: status['filtered'] == true - ? appConfigProvider.useThemeColorForStatus == true - ? Colors.grey - : Colors.red - : appConfigProvider.useThemeColorForStatus - ? Theme.of(context).colorScheme.primary - : Colors.green, - ), - const SizedBox(width: 10), - Text( - status['label'], - style: TextStyle( - color: status['filtered'] == true - ? appConfigProvider.useThemeColorForStatus == true - ? Colors.grey - : Colors.red - : appConfigProvider.useThemeColorForStatus - ? Theme.of(context).colorScheme.primary - : Colors.green, - fontWeight: FontWeight.w500 - ), - ) - ], - )); - } - } - else { - if (mounted) { - setState(() => resultWidget = Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.cancel, - size: 18, - color: Colors.red, - ), - const SizedBox(width: 10), - Text( - AppLocalizations.of(context)!.check, - style: const TextStyle( - color: Colors.red, - fontWeight: FontWeight.w500 - ), - ) - ], - )); - } - } - } - } - - return Padding( - padding: MediaQuery.of(context).viewInsets, - child: Container( - height: 330, - width: double.maxFinite, - decoration: BoxDecoration( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28), - ), - color: Theme.of(context).dialogBackgroundColor - ), - child: Center( - child: Column( - children: [ - Expanded( - child: ListView( - physics: 350 < MediaQuery.of(context).size.height - ? const NeverScrollableScrollPhysics() - : null, - children: [ - Padding( - padding: const EdgeInsets.only(top: 24), - child: Icon( - Icons.shield_rounded, - size: 24, - color: Theme.of(context).listTileTheme.iconColor - ), - ), - const SizedBox(height: 16), - Text( - AppLocalizations.of(context)!.checkHostFiltered, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSurface - ), - ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: TextFormField( - controller: domainController, - onChanged: validateDomain, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.link_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - errorText: domainError, - labelText: AppLocalizations.of(context)!.domain, - ), - ), - ), - if (resultWidget != null) Padding( - padding: const EdgeInsets.only( - top: 20, - left: 20, - right: 20 - ), - child: resultWidget, - ), - if (resultWidget == null) Padding( - padding: const EdgeInsets.only( - top: 20, - left: 20, - right: 20 - ), - child: Center( - child: Text( - AppLocalizations.of(context)!.insertDomain, - style: const TextStyle( - fontSize: 16, - ), - ), - ), - ), - ], - ), - ), - Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Padding( - padding: const EdgeInsets.only( - bottom: 24, - right: 24 - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(AppLocalizations.of(context)!.close), - ), - const SizedBox(width: 20), - TextButton( - onPressed: domainController.text != '' && domainError == null - ? () => checkHost() - : null, - child: Text( - AppLocalizations.of(context)!.check, - style: TextStyle( - color: domainController.text != '' && domainError == null - ? Theme.of(context).colorScheme.primary - : Colors.grey - ), - ), - ), - ], - ), - ) - ], - ) - ], - ), - ), - ), - ); - } -} \ No newline at end of file diff --git a/lib/screens/filters/custom_rules_list.dart b/lib/screens/filters/custom_rules_list.dart index 3b90728..284e15f 100644 --- a/lib/screens/filters/custom_rules_list.dart +++ b/lib/screens/filters/custom_rules_list.dart @@ -5,29 +5,28 @@ import 'package:flutter/rendering.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:adguard_home_manager/screens/filters/fab.dart'; -import 'package:adguard_home_manager/screens/filters/remove_custom_rule_modal.dart'; +import 'package:adguard_home_manager/screens/filters/add_button.dart'; +import 'package:adguard_home_manager/screens/filters/modals/custom_rules/sort_rules.dart'; +import 'package:adguard_home_manager/widgets/tab_content_list.dart'; import 'package:adguard_home_manager/functions/snackbar.dart'; -import 'package:adguard_home_manager/models/filtering.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; -import 'package:adguard_home_manager/classes/process_modal.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/providers/filtering_provider.dart'; class CustomRulesList extends StatefulWidget { - final int loadStatus; + final LoadStatus loadStatus; final ScrollController scrollController; final List data; - final void Function() fetchData; + final void Function(String) onRemoveCustomRule; const CustomRulesList({ - Key? key, + super.key, required this.loadStatus, required this.scrollController, required this.data, - required this.fetchData - }) : super(key: key); + required this.onRemoveCustomRule + }); @override State createState() => _CustomRulesListState(); @@ -36,6 +35,8 @@ class CustomRulesList extends StatefulWidget { class _CustomRulesListState extends State { late bool isVisible; + CustomRulesSorting _sortingMethod = CustomRulesSorting.topBottom; + @override initState(){ super.initState(); @@ -59,51 +60,10 @@ class _CustomRulesListState extends State { @override Widget build(BuildContext context) { - final serversProvider = Provider.of(context); + final filteringProvider = Provider.of(context); final appConfigProvider = Provider.of(context); - void removeCustomRule(String rule) async { - ProcessModal processModal = ProcessModal(context: context); - processModal.open(AppLocalizations.of(context)!.deletingRule); - - final List newRules = serversProvider.filtering.data!.userRules.where((r) => r != rule).toList(); - - final result = await setCustomRules(server: serversProvider.selectedServer!, rules: newRules); - - processModal.close(); - - if (result['result'] == 'success') { - FilteringData filteringData = serversProvider.filtering.data!; - filteringData.userRules = newRules; - serversProvider.setFilteringData(filteringData); - - showSnacbkar( - context: context, - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.ruleRemovedSuccessfully, - color: Colors.green - ); - } - else { - appConfigProvider.addLog(result['log']); - - showSnacbkar( - context: context, - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.ruleNotRemoved, - color: Colors.red - ); - } - } - - void openRemoveCustomRuleModal(String rule) { - showDialog( - context: context, - builder: (context) => RemoveCustomRule( - onConfirm: () => removeCustomRule(rule), - ) - ); - } + final renderData = _sortingMethod == CustomRulesSorting.bottomTop ? widget.data.reversed.toList() : widget.data.toList(); bool checkIfComment(String value) { final regex = RegExp(r'^(!|#).*$'); @@ -149,120 +109,148 @@ class _CustomRulesListState extends State { } } - switch (widget.loadStatus) { - case 0: - return SizedBox( - width: double.maxFinite, - height: MediaQuery.of(context).size.height-171, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const CircularProgressIndicator(), - const SizedBox(height: 30), - Text( - AppLocalizations.of(context)!.loadingFilters, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ) - ], - ), - ); - - case 1: - return Stack( - children: [ - if (widget.data.isNotEmpty) ListView.builder( - padding: const EdgeInsets.only(top: 0), - itemCount: widget.data.length, - itemBuilder: (context, index) => ListTile( - title: Text( - widget.data[index], - style: TextStyle( - color: checkIfComment(widget.data[index]) == true - ? Theme.of(context).colorScheme.onSurface.withOpacity(0.6) - : Theme.of(context).colorScheme.onSurface, - fontWeight: FontWeight.normal, - ), - ), - subtitle: generateSubtitle(widget.data[index]), - trailing: IconButton( - onPressed: () => openRemoveCustomRuleModal(widget.data[index]), - icon: const Icon(Icons.delete) - ), - ) - ), - if (widget.data.isEmpty) SizedBox( - width: double.maxFinite, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox( - width: MediaQuery.of(context).size.width-100, - child: Text( - AppLocalizations.of(context)!.noBlackLists, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ), - const SizedBox(height: 30), - TextButton.icon( - onPressed: widget.fetchData, - icon: const Icon(Icons.refresh_rounded), - label: Text(AppLocalizations.of(context)!.refresh), - ) - ], - ), - ), - AnimatedPositioned( - duration: const Duration(milliseconds: 100), - curve: Curves.easeInOut, - bottom: isVisible ? - appConfigProvider.showingSnackbar - ? 70 : 20 - : -70, - right: 20, - child: const FiltersFab( - type: 'custom_rule', - ) - ) - ], - ); - - case 2: - return SizedBox( - width: double.maxFinite, - height: MediaQuery.of(context).size.height-171, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon( - Icons.error, - color: Colors.red, - size: 50, - ), - const SizedBox(height: 30), - Text( - AppLocalizations.of(context)!.filtersNotLoaded, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ) - ], - ), - ); - - default: - return const SizedBox(); + void showSortingMethodModal() { + showDialog( + context: context, + builder: (ctx) => SortCustomRulesModal( + sortingMethod: _sortingMethod, + onSelect: (value) => setState(() => _sortingMethod = value), + ), + ); } + return CustomTabContentList( + loadingGenerator: () => SizedBox( + width: double.maxFinite, + height: MediaQuery.of(context).size.height-171, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.loadingFilters, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ), + itemsCount: renderData.length, + contentWidget: (index) => ListTile( + title: Text( + renderData[index], + style: TextStyle( + color: checkIfComment(widget.data[index]) == true + ? Theme.of(context).colorScheme.onSurface.withOpacity(0.6) + : Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.normal, + ), + ), + subtitle: generateSubtitle(renderData[index]), + trailing: IconButton( + onPressed: () => widget.onRemoveCustomRule(renderData[index]), + icon: const Icon(Icons.delete) + ), + ), + noData: Container( + width: double.maxFinite, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: Text( + AppLocalizations.of(context)!.noCustomFilters, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 24, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + const SizedBox(height: 30), + TextButton.icon( + onPressed: () async { + final result = await filteringProvider.fetchFilters(); + if (result == false && context.mounted) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.errorLoadFilters, + color: Colors.red + ); + } + }, + icon: const Icon(Icons.refresh_rounded), + label: Text(AppLocalizations.of(context)!.refresh), + ) + ], + ), + ), + errorGenerator: () => SizedBox( + width: double.maxFinite, + height: MediaQuery.of(context).size.height-171, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon( + Icons.error, + color: Colors.red, + size: 50, + ), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.filtersNotLoaded, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ), + loadStatus: widget.loadStatus, + onRefresh: () async { + final result = await filteringProvider.fetchFilters(); + if (result == false) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.errorLoadFilters, + color: Colors.red + ); + } + }, + fab: Column( + children: [ + FloatingActionButton.small( + onPressed: showSortingMethodModal, + child: Icon(Icons.sort_rounded), + ), + const SizedBox(height: 16), + AddFiltersButton( + type: 'edit_custom_rule', + widget: (fn) => FloatingActionButton.small( + onPressed: fn, + child: const Icon(Icons.edit_rounded), + ), + ), + const SizedBox(height: 16), + AddFiltersButton( + type: 'add_custom_rule', + widget: (fn) => FloatingActionButton( + onPressed: fn, + child: const Icon(Icons.add), + ), + ), + ], + ), + heightFabHidden: -180, + fabVisible: isVisible, + ); } } \ No newline at end of file diff --git a/lib/screens/filters/details/add_list_modal.dart b/lib/screens/filters/details/add_list_modal.dart new file mode 100644 index 0000000..f153224 --- /dev/null +++ b/lib/screens/filters/details/add_list_modal.dart @@ -0,0 +1,257 @@ +import 'dart:io'; + +import 'package:adguard_home_manager/constants/regexps.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/models/filtering.dart'; + +class AddListModal extends StatelessWidget { + final String type; + final Filter? list; + final void Function({required String name, required String url, required String type})? onConfirm; + final void Function({required Filter list, required String type})? onEdit; + final bool dialog; + + const AddListModal({ + super.key, + required this.type, + this.list, + this.onConfirm, + this.onEdit, + required this.dialog + }); + + @override + Widget build(BuildContext context) { + if (dialog == true) { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 400 + ), + child: _Content( + list: list, + onConfirm: onConfirm, + onEdit: onEdit, + type: type, + ) + ), + ); + } + else { + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28) + ), + color: Theme.of(context).dialogBackgroundColor + ), + child: SafeArea( + child: _Content( + list: list, + onConfirm: onConfirm, + onEdit: onEdit, + type: type, + ), + ) + ), + ); + } + } +} + +class _Content extends StatefulWidget { + final String type; + final Filter? list; + final void Function({required String name, required String url, required String type})? onConfirm; + final void Function({required Filter list, required String type})? onEdit; + + const _Content({ + required this.type, + required this.list, + required this.onConfirm, + required this.onEdit, + }); + + @override + State<_Content> createState() => _ContentState(); +} + +class _ContentState extends State<_Content> { + final TextEditingController nameController = TextEditingController(); + final TextEditingController urlController = TextEditingController(); + String? urlError; + + bool validData = false; + + void checkValidValues() { + if (nameController.text != '' && urlController.text != '') { + setState(() => validData = true); + } + else { + setState(() => validData = false); + } + } + + void validateUrl(String value) { + if (Regexps.url.hasMatch(value)) { + setState(() => urlError = null); + } + else { + final pathRegex = RegExp(r'^(((\\|\/)[a-z0-9^&@{}\[\],$=!\-#\(\)%\.\+~_]+)*(\\|\/))([^\\\/:\*\<>\|]+\.[a-z0-9]+)$'); + if (pathRegex.hasMatch(value)) { + setState(() => urlError = null); + } + else { + setState(() => urlError = AppLocalizations.of(context)!.urlNotValid); + } + } + } + + @override + void initState() { + if (widget.list != null) { + nameController.text = widget.list!.name; + urlController.text = widget.list!.url; + + validData = true; + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + 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( + widget.type == 'whitelist' + ? Icons.verified_user_rounded + : Icons.gpp_bad_rounded, + size: 24, + color: Theme.of(context).listTileTheme.iconColor + ), + ), + const SizedBox(height: 16), + Text( + widget.list != null + ? widget.type == 'whitelist' + ? AppLocalizations.of(context)!.editWhitelist + : AppLocalizations.of(context)!.editBlacklist + : widget.type == 'whitelist' + ? AppLocalizations.of(context)!.addWhitelist + : AppLocalizations.of(context)!.addBlacklist, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + color: Theme.of(context).colorScheme.onSurface + ), + ), + const SizedBox(height: 16), + ], + ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: TextFormField( + controller: nameController, + onChanged: (_) => checkValidValues(), + decoration: InputDecoration( + prefixIcon: const Icon(Icons.badge_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + labelText: AppLocalizations.of(context)!.name, + ), + ), + ), + Container(height: 30), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: TextFormField( + controller: urlController, + onChanged: validateUrl, + enabled: widget.list != null ? false : true, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.link_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + errorText: urlError, + labelText: AppLocalizations.of(context)!.urlAbsolutePath, + ), + ), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(24), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.cancel) + ), + const SizedBox(width: 20), + TextButton( + onPressed: () { + Navigator.pop(context); + if (widget.list != null) { + final Filter newList = Filter( + url: urlController.text, + name: nameController.text, + lastUpdated: widget.list!.lastUpdated, + id: widget.list!.id, + rulesCount: widget.list!.rulesCount, + enabled: widget.list!.enabled + ); + widget.onEdit!( + list: newList, + type: widget.type + ); + } + else { + widget.onConfirm!( + name: nameController.text, + url: urlController.text, + type: widget.type + ); + } + }, + child: Text( + widget.list != null + ? AppLocalizations.of(context)!.save + : AppLocalizations.of(context)!.confirm + ) + ), + ], + ), + ), + if (Platform.isIOS) const SizedBox(height: 16) + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/filters/details/check_host_modal.dart b/lib/screens/filters/details/check_host_modal.dart new file mode 100644 index 0000000..be0efce --- /dev/null +++ b/lib/screens/filters/details/check_host_modal.dart @@ -0,0 +1,265 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:adguard_home_manager/constants/regexps.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/providers/app_config_provider.dart'; +import 'package:adguard_home_manager/functions/get_filtered_status.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; + +class CheckHostModal extends StatelessWidget { + final bool dialog; + + const CheckHostModal({ + super.key, + required this.dialog + }); + + @override + Widget build(BuildContext context) { + if (dialog == true) { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 400 + ), + child: const _Content() + ), + ); + } + else { + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: Container( + width: double.maxFinite, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28), + ), + color: Theme.of(context).dialogBackgroundColor + ), + child: const SafeArea( + child: _Content() + ) + ), + ); + } + } +} + +class _Content extends StatefulWidget { + const _Content(); + + @override + State<_Content> createState() => _ContentState(); +} + +class _ContentState extends State<_Content> { + final TextEditingController domainController = TextEditingController(); + String? domainError; + + Widget? resultWidget; + + void validateDomain(String value) { + if (Regexps.domain.hasMatch(value)) { + setState(() => domainError = null); + } + else { + setState(() => domainError = AppLocalizations.of(context)!.domainNotValid); + } + } + + Widget checking() { + return SizedBox( + height: 30, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(width: 20), + Text(AppLocalizations.of(context)!.checkingHost) + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + final serversProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + + void checkHost() async { + setState(() => resultWidget = checking()); + + final result = await serversProvider.apiClient2!.checkHostFiltered(host: domainController.text); + if (!mounted) return; + + if (result.successful == true) { + final status = getFilteredStatus(context, appConfigProvider, result.content['reason'], true); + if (mounted) { + setState(() => resultWidget = Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + status['icon'], + size: 18, + color: status['filtered'] == true + ? appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red + : appConfigProvider.useThemeColorForStatus + ? Theme.of(context).colorScheme.primary + : Colors.green, + ), + const SizedBox(width: 10), + Text( + status['label'], + style: TextStyle( + color: status['filtered'] == true + ? appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red + : appConfigProvider.useThemeColorForStatus + ? Theme.of(context).colorScheme.primary + : Colors.green, + fontWeight: FontWeight.w500 + ), + ) + ], + )); + } + } + else { + setState(() => resultWidget = Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.cancel, + size: 18, + color: Colors.red, + ), + const SizedBox(width: 10), + Text( + AppLocalizations.of(context)!.check, + style: const TextStyle( + color: Colors.red, + fontWeight: FontWeight.w500 + ), + ) + ], + )); + } + } + + 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.shield_rounded, + size: 24, + color: Theme.of(context).listTileTheme.iconColor + ), + ), + const SizedBox(height: 16), + Text( + AppLocalizations.of(context)!.checkHostFiltered, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + color: Theme.of(context).colorScheme.onSurface + ), + ), + const SizedBox(height: 16), + ], + ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: TextFormField( + controller: domainController, + onChanged: validateDomain, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.link_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + errorText: domainError, + labelText: AppLocalizations.of(context)!.domain, + ), + ), + ), + if (resultWidget != null) Padding( + padding: const EdgeInsets.all(24), + child: resultWidget, + ), + if (resultWidget == null) Padding( + padding: const EdgeInsets.all(24), + child: Center( + child: Text( + AppLocalizations.of(context)!.insertDomain, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 16, + ), + ), + ), + ), + ], + ), + ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.only( + bottom: 24, + right: 24 + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.close), + ), + const SizedBox(width: 20), + TextButton( + onPressed: domainController.text != '' && domainError == null + ? () => checkHost() + : null, + child: Text( + AppLocalizations.of(context)!.check, + style: TextStyle( + color: domainController.text != '' && domainError == null + ? Theme.of(context).colorScheme.primary + : Colors.grey + ), + ), + ), + ], + ), + ) + ], + ) + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/filters/details/list_details_screen.dart b/lib/screens/filters/details/list_details_screen.dart new file mode 100644 index 0000000..188854c --- /dev/null +++ b/lib/screens/filters/details/list_details_screen.dart @@ -0,0 +1,463 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/screens/filters/details/add_list_modal.dart'; +import 'package:adguard_home_manager/screens/filters/modals/delete_list_modal.dart'; +import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; + +import 'package:adguard_home_manager/functions/open_url.dart'; +import 'package:adguard_home_manager/functions/format_time.dart'; +import 'package:adguard_home_manager/providers/filtering_provider.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/models/filtering.dart'; + +class ListDetailsScreen extends StatefulWidget { + final int listId; + final String type; + final bool dialog; + + const ListDetailsScreen({ + super.key, + required this.listId, + required this.type, + required this.dialog + }); + + @override + State createState() => _ListDetailsScreenState(); +} + +class _ListDetailsScreenState extends State { + final ScrollController scrollController = ScrollController(); + bool fabVisible = true; + + @override + void initState() { + scrollController.addListener(() { + if (scrollController.position.userScrollDirection == ScrollDirection.reverse) { + if (mounted && fabVisible == true) { + setState(() => fabVisible = false); + } + } + else { + if (scrollController.position.userScrollDirection == ScrollDirection.forward) { + if (mounted && fabVisible == false) { + setState(() => fabVisible = true); + } + } + } + }); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + final filteringProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + + Filter? list; + try { + list = filteringProvider.filtering != null + ? widget.type == 'whitelist' + ? filteringProvider.filtering!.whitelistFilters.firstWhere((l) => l.id == widget.listId) + : filteringProvider.filtering!.filters.firstWhere((l) => l.id == widget.listId) + : null; + } catch (e) { + // ------- // + } + + void updateList({ + required FilteringListActions action, + required Filter filterList, + }) async { + ProcessModal processModal = ProcessModal(); + processModal.open( + action == FilteringListActions.edit + ? AppLocalizations.of(context)!.savingList + : action == FilteringListActions.disable + ? AppLocalizations.of(context)!.disablingList + : AppLocalizations.of(context)!.enablingList, + ); + final result = await filteringProvider.updateList( + list: filterList, + type: widget.type, + action: action + ); + processModal.close(); + if (result == true) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.listDataUpdated, + color: Colors.green + ); + } + else { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.listDataNotUpdated, + color: Colors.red + ); + } + } + + if (widget.dialog == true) { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500 + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.clear_rounded), + tooltip: AppLocalizations.of(context)!.close, + ), + const SizedBox(width: 8), + Text( + AppLocalizations.of(context)!.listDetails, + style: const TextStyle( + fontSize: 22 + ), + ) + ], + ), + if (list != null) Row( + children: [ + IconButton( + onPressed: () => updateList( + action: list!.enabled == true + ? FilteringListActions.disable + : FilteringListActions.enable, + filterList: list + ), + icon: Icon( + list.enabled == true + ? Icons.gpp_bad_rounded + : Icons.verified_user_rounded, + ), + tooltip: list.enabled == true + ? AppLocalizations.of(context)!.disableList + : AppLocalizations.of(context)!.enableList, + ), + _Actions( + list: list, + type: widget.type, + updateList: (action, filterList) => updateList(action: action, filterList: filterList), + ) + ], + ) + ], + ), + ), + Flexible( + child: list != null + ? SingleChildScrollView( + child: Wrap( + children: [ + _Content( + isDialog: widget.dialog, + list: list, + type: widget.type, + ) + ], + ), + ) + : Center( + child: Text( + AppLocalizations.of(context)!.listNotAvailable, + style: const TextStyle( + fontSize: 24, + ), + ), + ), + ) + ], + ), + ) + ); + } + else { + return Dialog.fullscreen( + child: Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context)!.listDetails), + actions: list != null + ? [ + _Actions( + list: list, + type: widget.type, + updateList: (action, filterList) => updateList(action: action, filterList: filterList), + ) + ] + : null, + ), + body: SafeArea( + child: Stack( + children: [ + if (list != null) ListView( + children: [ + _Content( + isDialog: widget.dialog, + list: list, + type: widget.type, + ) + ], + ), + if (list == null) Center( + child: Text( + AppLocalizations.of(context)!.listNotAvailable, + style: const TextStyle( + fontSize: 24, + ), + ), + ), + if (list != null) AnimatedPositioned( + duration: const Duration(milliseconds: 100), + curve: Curves.easeInOut, + bottom: fabVisible ? + appConfigProvider.showingSnackbar + ? 70 : (Platform.isIOS ? 40 : 20) + : -70, + right: 20, + child: FloatingActionButton( + onPressed: () => updateList( + action: list!.enabled == true + ? FilteringListActions.disable + : FilteringListActions.enable, + filterList: list + ), + child: Icon( + list.enabled == true + ? Icons.gpp_bad_rounded + : Icons.verified_user_rounded, + ), + ), + ) + ], + ), + ), + ), + ); + } + } +} + +class _Content extends StatelessWidget { + final Filter list; + final bool isDialog; + final String type; + + const _Content({ + required this.list, + required this.isDialog, + required this.type, + }); + + @override + Widget build(BuildContext context) { + final appConfigProvider = Provider.of(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: type == '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(context); + final appConfigProvider = Provider.of(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) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.listDeleted, + color: Colors.green + ); + Navigator.pop(context); + } + else { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.listNotDeleted, + color: Colors.red + ); + } + } + ) + ); + }, + icon: const Icon(Icons.delete), + tooltip: AppLocalizations.of(context)!.delete, + ), + const SizedBox(width: 10), + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/filters/fab.dart b/lib/screens/filters/fab.dart deleted file mode 100644 index 911f54b..0000000 --- a/lib/screens/filters/fab.dart +++ /dev/null @@ -1,175 +0,0 @@ -// ignore_for_file: use_build_context_synchronously - -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:bottom_sheet/bottom_sheet.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -import 'package:adguard_home_manager/screens/filters/add_custom_rule.dart'; -import 'package:adguard_home_manager/screens/filters/add_list_modal.dart'; - -import 'package:adguard_home_manager/functions/snackbar.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; -import 'package:adguard_home_manager/classes/process_modal.dart'; -import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/models/filtering.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; - -class FiltersFab extends StatelessWidget { - final String type; - - const FiltersFab({ - Key? key, - required this.type, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final serversProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - - void confirmAddRule(String rule) async { - ProcessModal processModal = ProcessModal(context: context); - processModal.open(AppLocalizations.of(context)!.addingRule); - - final List newRules = serversProvider.filtering.data!.userRules; - newRules.add(rule); - - final result = await setCustomRules(server: serversProvider.selectedServer!, rules: newRules); - - processModal.close(); - - if (result['result'] == 'success') { - FilteringData filteringData = serversProvider.filtering.data!; - filteringData.userRules = newRules; - serversProvider.setFilteringData(filteringData); - - showSnacbkar( - context: context, - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.ruleAddedSuccessfully, - color: Colors.green - ); - } - else { - appConfigProvider.addLog(result['log']); - - showSnacbkar( - context: context, - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.ruleNotAdded, - color: Colors.red - ); - } - } - - void openAddCustomRule() { - Navigator.of(context).push( - MaterialPageRoute( - fullscreenDialog: true, - builder: (context) => AddCustomRule( - onConfirm: confirmAddRule - ), - ) - ); - } - - void confirmAddList({required String name, required String url, required String type}) async { - ProcessModal processModal = ProcessModal(context: context); - processModal.open(AppLocalizations.of(context)!.addingList); - - final result1 = await addFilteringList(server: serversProvider.selectedServer!, data: { - 'name': name, - 'url': url, - 'whitelist': type == 'whitelist' ? true : false - }); - - if (result1['result'] == 'success') { - if (result1['data'].toString().contains("OK")) { - final result2 = await getFiltering(server: serversProvider.selectedServer!); - final items = result1['data'].toString().split(' ')[1]; - - if (result2['result'] == 'success') { - serversProvider.setFilteringData(result2['data']); - serversProvider.setFilteringLoadStatus(1, true); - } - else { - appConfigProvider.addLog(result2['log']); - serversProvider.setFilteringLoadStatus(2, true); - } - - processModal.close(); - - showSnacbkar( - context: context, - appConfigProvider: appConfigProvider, - label: "${AppLocalizations.of(context)!.listAdded} $items.", - color: Colors.green - ); - } - else { - processModal.close(); - - showSnacbkar( - context: context, - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.listNotAdded, - color: Colors.red - ); - } - } - else if (result1['result'] == 'error' && result1['log'].statusCode == '400' && result1['log'].resBody.toString().contains("Couldn't fetch filter from url")) { - processModal.close(); - appConfigProvider.addLog(result1['log']); - - showSnacbkar( - context: context, - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.listUrlInvalid, - color: Colors.red - ); - } - else if (result1['result'] == 'error' && result1['log'].statusCode == '400' && result1['log'].resBody.toString().contains('Filter URL already added')) { - processModal.close(); - appConfigProvider.addLog(result1['log']); - - showSnacbkar( - context: context, - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.listAlreadyAdded, - color: Colors.red - ); - } - else { - processModal.close(); - appConfigProvider.addLog(result1['log']); - - showSnacbkar( - context: context, - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.listNotAdded, - color: Colors.red - ); - } - } - - void openAddWhitelistBlacklist() { - showModalBottomSheet( - context: context, - builder: (ctx) => AddListModal( - type: type, - onConfirm: confirmAddList, - ), - isScrollControlled: true, - backgroundColor: Colors.transparent - ); - } - - return FloatingActionButton( - onPressed: type == 'blacklist' || type == 'whitelist' - ? () => openAddWhitelistBlacklist() - : () => openAddCustomRule(), - child: const Icon(Icons.add), - ); - } -} \ No newline at end of file diff --git a/lib/screens/filters/filter_list_tile.dart b/lib/screens/filters/filter_list_tile.dart deleted file mode 100644 index 1782d62..0000000 --- a/lib/screens/filters/filter_list_tile.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:flutter/material.dart'; - -class FilterListTile extends StatelessWidget { - final IconData icon; - final String title; - final String subtitle; - final Color? color; - final bool? bold; - - const FilterListTile({ - Key? key, - required this.icon, - required this.title, - required this.subtitle, - this.color, - this.bold, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Icon( - icon, - size: 24, - color: Theme.of(context).listTileTheme.iconColor, - ), - const SizedBox(width: 16), - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurface - ), - ), - const SizedBox(height: 3), - Text( - subtitle, - style: TextStyle( - fontSize: 14, - color: color ?? Theme.of(context).listTileTheme.textColor, - fontWeight: bold == true ? FontWeight.bold : FontWeight.w400 - ), - ), - ], - ), - ) - ], - ), - ); - } -} \ No newline at end of file diff --git a/lib/screens/filters/filters.dart b/lib/screens/filters/filters.dart index d10e70b..ab624e0 100644 --- a/lib/screens/filters/filters.dart +++ b/lib/screens/filters/filters.dart @@ -1,134 +1,70 @@ // ignore_for_file: use_build_context_synchronously +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:adguard_home_manager/screens/filters/filters_list.dart'; -import 'package:adguard_home_manager/screens/filters/blocked_services_scren.dart'; -import 'package:adguard_home_manager/screens/filters/check_host_modal.dart'; -import 'package:adguard_home_manager/screens/filters/custom_rules_list.dart'; -import 'package:adguard_home_manager/screens/filters/update_interval_lists_modal.dart'; +import 'package:adguard_home_manager/screens/filters/details/check_host_modal.dart'; +import 'package:adguard_home_manager/screens/filters/filters_tabs_view.dart'; +import 'package:adguard_home_manager/screens/filters/filters_triple_column.dart'; +import 'package:adguard_home_manager/screens/filters/details/list_details_screen.dart'; +import 'package:adguard_home_manager/screens/filters/modals/remove_custom_rule_modal.dart'; +import 'package:adguard_home_manager/screens/filters/modals/blocked_services_screen.dart'; +import 'package:adguard_home_manager/screens/filters/modals/update_interval_lists_modal.dart'; import 'package:adguard_home_manager/functions/snackbar.dart'; +import 'package:adguard_home_manager/providers/status_provider.dart'; import 'package:adguard_home_manager/classes/process_modal.dart'; +import 'package:adguard_home_manager/providers/filtering_provider.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; +import 'package:adguard_home_manager/models/filtering.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; import 'package:adguard_home_manager/models/clients.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; -class Filters extends StatelessWidget { - const Filters({Key? key}) : super(key: key); +class Filters extends StatefulWidget { + const Filters({super.key}); @override - Widget build(BuildContext context) { - final serversProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - - return FiltersWidget( - serversProvider: serversProvider, - appConfigProvider: appConfigProvider - ); - } + State createState() => _FiltersState(); } -class FiltersWidget extends StatefulWidget { - final ServersProvider serversProvider; - final AppConfigProvider appConfigProvider; - - const FiltersWidget({ - Key? key, - required this.serversProvider, - required this.appConfigProvider - }) : super(key: key); - - @override - State createState() => _FiltersWidgetState(); -} - -class _FiltersWidgetState extends State with TickerProviderStateMixin { - late TabController tabController; - final ScrollController scrollController = ScrollController(); - - Future fetchFilters() async { - widget.serversProvider.setFilteringLoadStatus(0, false); - - final result = await getFiltering(server: widget.serversProvider.selectedServer!); - - if (mounted) { - if (result['result'] == 'success') { - widget.serversProvider.setFilteringData(result['data']); - widget.serversProvider.setFilteringLoadStatus(1, false); - } - else { - widget.appConfigProvider.addLog(result['log']); - widget.serversProvider.setFilteringLoadStatus(2, false); - } - } - } - - @override - void initState() { - fetchFilters(); - super.initState(); - tabController = TabController( - initialIndex: 0, - length: 3, - vsync: this, - ); - tabController.addListener(() => widget.appConfigProvider.setSelectedFiltersTab(tabController.index)); - } - +class _FiltersState extends State { List generateClientsList(List clients, List ips) { return clients.where((client) => ips.contains(client.ip)).toList(); } + @override + void initState() { + final filteringProvider = Provider.of(context, listen: false); + filteringProvider.fetchFilters(showLoading: true); + super.initState(); + } + @override Widget build(BuildContext context) { - final serversProvider = Provider.of(context); + final filteringProvider = Provider.of(context); + final statusProvider = Provider.of(context); final appConfigProvider = Provider.of(context); - void fetchUpdateLists() async { - ProcessModal processModal = ProcessModal(context: context); + final width = MediaQuery.of(context).size.width; + + void updateLists() async { + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.updatingLists); - - final result = await updateLists(server: serversProvider.selectedServer!); - - if (result['result'] == 'success') { - final result2 = await getFiltering(server: widget.serversProvider.selectedServer!); - - processModal.close(); - - if (mounted) { - if (result2['result'] == 'success') { - widget.serversProvider.setFilteringData(result2['data']); - - showSnacbkar( - context: context, - appConfigProvider: appConfigProvider, - label: "${result['data']['updated']} ${AppLocalizations.of(context)!.listsUpdated}", - color: Colors.green - ); - } - else { - widget.appConfigProvider.addLog(result2['log']); - - showSnacbkar( - context: context, - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.listsNotLoaded, - color: Colors.red - ); - } - } - + final result = await filteringProvider.updateLists(); + if (!mounted) return; + processModal.close(); + if (result['success'] == true) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: "${result['data']['updated']} ${AppLocalizations.of(context)!.listsUpdated}", + color: Colors.green + ); } else { - processModal.close(); - appConfigProvider.addLog(result['log']); - - showSnacbkar( - context: context, + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.listsNotUpdated, color: Colors.red @@ -138,40 +74,49 @@ class _FiltersWidgetState extends State with TickerProviderStateM void showCheckHostModal() { Future.delayed(const Duration(seconds: 0), () { - showModalBottomSheet( - context: context, - builder: (context) => const CheckHostModal(), - backgroundColor: Colors.transparent, - isScrollControlled: true, - ); + if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + context: context, + builder: (context) => const CheckHostModal( + dialog: true, + ), + ); + } + else { + showModalBottomSheet( + context: context, + useRootNavigator: true, + builder: (context) => const CheckHostModal( + dialog: false, + ), + backgroundColor: Colors.transparent, + isScrollControlled: true, + ); + } }); } void enableDisableFiltering() async { - ProcessModal processModal = ProcessModal(context: context); + ProcessModal processModal = ProcessModal(); processModal.open( - serversProvider.serverStatus.data!.filteringEnabled == true + statusProvider.serverStatus!.filteringEnabled == true ? AppLocalizations.of(context)!.disableFiltering : AppLocalizations.of(context)!.enableFiltering ); - final result = await updateFiltering(serversProvider.selectedServer!, !serversProvider.serverStatus.data!.filteringEnabled); + final result = await filteringProvider.enableDisableFiltering(); processModal.close(); - if (result['result'] == 'success') { - serversProvider.setFilteringProtectionStatus(!serversProvider.serverStatus.data!.filteringEnabled); - - showSnacbkar( - context: context, + if (result == true) { + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.filteringStatusUpdated, color: Colors.green ); } else { - showSnacbkar( - context: context, + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.filteringStatusNotUpdated, color: Colors.red @@ -180,29 +125,22 @@ class _FiltersWidgetState extends State with TickerProviderStateM } void setUpdateFrequency(int value) async { - ProcessModal processModal = ProcessModal(context: context); + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.changingUpdateFrequency); - final result = await requestChangeUpdateFrequency(server: serversProvider.selectedServer!, data: { - "enabled": serversProvider.filtering.data!.enabled, - "interval": value - }); + final result = await filteringProvider.changeUpdateFrequency(value); processModal.close(); - if (result['result'] == 'success') { - serversProvider.setFiltersUpdateFrequency(value); - - showSnacbkar( - context: context, + if (result == true) { + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.updateFrequencyChanged, color: Colors.green ); } else { - showSnacbkar( - context: context, + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.updateFrequencyNotChanged, color: Colors.red @@ -210,182 +148,197 @@ class _FiltersWidgetState extends State with TickerProviderStateM } } - void openBlockedServicesModal() { + void openBlockedServices() { Future.delayed(const Duration(seconds: 0), () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const BlockedServicesScreen(), - ) - ); + openBlockedServicesModal(context: context, width: width); }); } - return DefaultTabController( - length: 3, - child: NestedScrollView( - controller: scrollController, - headerSliverBuilder: ((context, innerBoxIsScrolled) { - return [ - SliverOverlapAbsorber( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - sliver: SliverSafeArea( - top: false, - sliver: SliverAppBar( - title: Text(AppLocalizations.of(context)!.filters), - pinned: true, - floating: true, - forceElevated: innerBoxIsScrolled, - actions: serversProvider.filtering.loadStatus == 1 ? [ - IconButton( - onPressed: enableDisableFiltering, - tooltip: serversProvider.filtering.data!.enabled == true - ? AppLocalizations.of(context)!.disableFiltering - : AppLocalizations.of(context)!.enableFiltering, - icon: Stack( - children: [ - const Icon(Icons.power_settings_new_rounded), - Positioned( - bottom: 0, - right: 0, - child: Stack( - children: [ - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30), - color: Colors.white - ), - child: Icon( - serversProvider.filtering.data!.enabled == true - ? Icons.check_circle_rounded - : Icons.cancel, - size: 12, - color: serversProvider.filtering.data!.enabled == true - ? appConfigProvider.useThemeColorForStatus == true - ? Theme.of(context).colorScheme.primary - : Colors.green - : appConfigProvider.useThemeColorForStatus == true - ? Colors.grey - : Colors.red - ), - ), - ], - ), - ) - ], - ) - ), - IconButton( - onPressed: () { - showModalBottomSheet( - context: context, - builder: (context) => UpdateIntervalListsModal( - interval: serversProvider.filtering.data!.interval, - onChange: setUpdateFrequency - ), - backgroundColor: Colors.transparent, - isScrollControlled: true - ); - }, - icon: const Icon(Icons.update_rounded) - ), - PopupMenuButton( - itemBuilder: (context) => [ - PopupMenuItem( - onTap: fetchUpdateLists, - child: Row( - children: [ - const Icon(Icons.sync_rounded), - const SizedBox(width: 10), - Text(AppLocalizations.of(context)!.updateLists) - ], - ) - ), - PopupMenuItem( - onTap: openBlockedServicesModal, - child: Row( - children: [ - const Icon(Icons.block), - const SizedBox(width: 10), - Text(AppLocalizations.of(context)!.blockedServices) - ], - ) - ), - PopupMenuItem( - onTap: showCheckHostModal, - child: Row( - children: [ - const Icon(Icons.shield_rounded), - const SizedBox(width: 10), - Text(AppLocalizations.of(context)!.checkHostFiltered) - ], - ) - ), - ] - ), - const SizedBox(width: 5), - ] : [], - bottom: TabBar( - controller: tabController, - isScrollable: false, - unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant, - tabs: [ - Tab( - icon: const Icon(Icons.verified_user_rounded), - text: AppLocalizations.of(context)!.whitelists, - ), - Tab( - icon: const Icon(Icons.gpp_bad_rounded), - text: AppLocalizations.of(context)!.blacklist, - ), - Tab( - icon: const Icon(Icons.shield_rounded), - text: AppLocalizations.of(context)!.customRules, - ), - ] - ) - ), - ), - ) - ]; - }), - body: TabBarView( - controller: tabController, - children: [ - RefreshIndicator( - onRefresh: fetchFilters, - child: FiltersList( - loadStatus: serversProvider.filtering.loadStatus, - scrollController: scrollController, - type: 'whitelist', - data: serversProvider.filtering.loadStatus == 1 - ? serversProvider.filtering.data!.whitelistFilters : [], - fetchData: fetchFilters, - ) - ), - RefreshIndicator( - onRefresh: fetchFilters, - child: FiltersList( - loadStatus: serversProvider.filtering.loadStatus, - scrollController: scrollController, - type: 'blacklist', - data: serversProvider.filtering.loadStatus == 1 - ? serversProvider.filtering.data!.filters : [], - fetchData: fetchFilters, - ) - ), - RefreshIndicator( - onRefresh: fetchFilters, - child: CustomRulesList( - loadStatus: serversProvider.filtering.loadStatus, - scrollController: scrollController, - data: serversProvider.filtering.loadStatus == 1 - ? serversProvider.filtering.data!.userRules : [], - fetchData: fetchFilters, - ) - ), - ] + void removeCustomRule(String rule) async { + ProcessModal processModal = ProcessModal(); + processModal.open(AppLocalizations.of(context)!.deletingRule); + + final result = await filteringProvider.removeCustomRule(rule); + + processModal.close(); + + if (result == true) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.ruleRemovedSuccessfully, + color: Colors.green + ); + } + else { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.ruleNotRemoved, + color: Colors.red + ); + } + } + + void openRemoveCustomRuleModal(String rule) { + showDialog( + context: context, + builder: (context) => RemoveCustomRule( + onConfirm: () => removeCustomRule(rule), ) - ) + ); + } + + void openListDetails(Filter filter, String type) { + if (width > 900) { + showDialog( + context: context, + builder: (context) => ListDetailsScreen( + listId: filter.id, + type: type, + dialog: width > 900 || !(Platform.isAndroid | Platform.isIOS), + ), + ); + } + else { + Navigator.push(context, MaterialPageRoute( + builder: (context) => ListDetailsScreen( + listId: filter.id, + type: type, + dialog: width > 900 || !(Platform.isAndroid | Platform.isIOS), + ), + )); + } + } + + List actions() { + if (filteringProvider.loadStatus == LoadStatus.loaded) { + return [ + if (statusProvider.loadStatus == LoadStatus.loaded) IconButton( + onPressed: enableDisableFiltering, + tooltip: filteringProvider.filtering!.enabled == true + ? AppLocalizations.of(context)!.disableFiltering + : AppLocalizations.of(context)!.enableFiltering, + icon: Stack( + children: [ + const Icon(Icons.power_settings_new_rounded), + Positioned( + bottom: 0, + right: 0, + child: Stack( + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + color: Colors.white + ), + child: Icon( + filteringProvider.filtering!.enabled == true + ? Icons.check_circle_rounded + : Icons.cancel, + size: 12, + color: filteringProvider.filtering!.enabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red + ), + ), + ], + ), + ) + ], + ) + ), + IconButton( + onPressed: () { + if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + context: context, + builder: (context) => UpdateIntervalListsModal( + interval: filteringProvider.filtering!.interval, + onChange: setUpdateFrequency, + dialog: true, + ), + ); + } + else { + showModalBottomSheet( + context: context, + useRootNavigator: true, + builder: (context) => UpdateIntervalListsModal( + interval: filteringProvider.filtering!.interval, + onChange: setUpdateFrequency, + dialog: false, + ), + backgroundColor: Colors.transparent, + isScrollControlled: true + ); + } + }, + icon: const Icon(Icons.update_rounded), + tooltip: AppLocalizations.of(context)!.updateFrequency, + ), + PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + onTap: updateLists, + child: Row( + children: [ + const Icon(Icons.sync_rounded), + const SizedBox(width: 10), + Text(AppLocalizations.of(context)!.updateLists) + ], + ) + ), + PopupMenuItem( + onTap: openBlockedServices, + child: Row( + children: [ + const Icon(Icons.block), + const SizedBox(width: 10), + Text(AppLocalizations.of(context)!.blockedServices) + ], + ) + ), + PopupMenuItem( + onTap: showCheckHostModal, + child: Row( + children: [ + const Icon(Icons.shield_rounded), + const SizedBox(width: 10), + Text(AppLocalizations.of(context)!.checkHostFiltered) + ], + ) + ), + ] + ), + const SizedBox(width: 5), + ]; + } + else { + return []; + } + } + + return LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth > 900) { + return FiltersTripleColumn( + onRemoveCustomRule: openRemoveCustomRuleModal, + onOpenDetailsModal: openListDetails, + actions: actions(), + ); + } + else { + return FiltersTabsView( + appConfigProvider: appConfigProvider, + actions: actions(), + onRemoveCustomRule: openRemoveCustomRuleModal, + onOpenDetailsModal: openListDetails, + ); + } + }, ); } } \ No newline at end of file diff --git a/lib/screens/filters/filters_list.dart b/lib/screens/filters/filters_list.dart index 5cbb4ac..b9d4617 100644 --- a/lib/screens/filters/filters_list.dart +++ b/lib/screens/filters/filters_list.dart @@ -7,29 +7,33 @@ import 'package:provider/provider.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:adguard_home_manager/screens/filters/fab.dart'; -import 'package:adguard_home_manager/screens/filters/list_details_screen.dart'; +import 'package:adguard_home_manager/screens/filters/add_button.dart'; +import 'package:adguard_home_manager/screens/filters/list_options_menu.dart'; import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; +import 'package:adguard_home_manager/widgets/tab_content_list.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; +import 'package:adguard_home_manager/functions/snackbar.dart'; +import 'package:adguard_home_manager/providers/filtering_provider.dart'; import 'package:adguard_home_manager/functions/number_format.dart'; import 'package:adguard_home_manager/models/filtering.dart'; class FiltersList extends StatefulWidget { - final int loadStatus; + final LoadStatus loadStatus; final ScrollController scrollController; final List data; - final void Function() fetchData; final String type; + final void Function(Filter, String) onOpenDetailsScreen; const FiltersList({ - Key? key, + super.key, required this.loadStatus, required this.scrollController, required this.data, - required this.fetchData, required this.type, - }) : super(key: key); + required this.onOpenDetailsScreen + }); @override State createState() => _FiltersListState(); @@ -61,132 +65,129 @@ class _FiltersListState extends State { @override Widget build(BuildContext context) { + final filteringProvider = Provider.of(context); final appConfigProvider = Provider.of(context); - - void openDetailsModal(Filter filter) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => ListDetailsScreen( - list: filter, - type: widget.type, - ) - ) - ); - } - switch (widget.loadStatus) { - case 0: - return SizedBox( - width: double.maxFinite, - height: MediaQuery.of(context).size.height-171, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const CircularProgressIndicator(), - const SizedBox(height: 30), - Text( - AppLocalizations.of(context)!.loadingFilters, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ) - ], - ), - ); - - case 1: - return Stack( + return CustomTabContentList( + loadingGenerator: () => SizedBox( + width: double.maxFinite, + height: MediaQuery.of(context).size.height-171, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - if (widget.data.isNotEmpty) ListView.builder( - padding: const EdgeInsets.only(top: 0), - itemCount: widget.data.length, - itemBuilder: (context, index) => CustomListTile( - title: widget.data[index].name, - subtitle: "${intFormat(widget.data[index].rulesCount, Platform.localeName)} ${AppLocalizations.of(context)!.enabledRules}", - trailing: Icon( - widget.data[index].enabled == true - ? Icons.check_circle_rounded - : Icons.cancel, - color: widget.data[index].enabled == true - ? appConfigProvider.useThemeColorForStatus == true - ? Theme.of(context).colorScheme.primary - : Colors.green - : appConfigProvider.useThemeColorForStatus == true - ? Colors.grey - : Colors.red - ), - onTap: () => openDetailsModal(widget.data[index]), + const CircularProgressIndicator(), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.loadingFilters, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, ), - ), - if (widget.data.isEmpty) if (widget.data.isEmpty) SizedBox( - width: double.maxFinite, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - widget.type == 'blacklist' - ? AppLocalizations.of(context)!.noBlackLists - : AppLocalizations.of(context)!.noWhiteLists, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 30), - TextButton.icon( - onPressed: widget.fetchData, - icon: const Icon(Icons.refresh_rounded), - label: Text(AppLocalizations.of(context)!.refresh), - ) - ], - ), - ), - AnimatedPositioned( - duration: const Duration(milliseconds: 100), - curve: Curves.easeInOut, - bottom: isVisible ? - appConfigProvider.showingSnackbar - ? 70 : 20 - : -70, - right: 20, - child: FiltersFab( - type: widget.type - ) ) ], - ); - - case 2: - return SizedBox( - width: double.maxFinite, - height: MediaQuery.of(context).size.height-171, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon( - Icons.error, - color: Colors.red, - size: 50, - ), - const SizedBox(height: 30), - Text( - AppLocalizations.of(context)!.filtersNotLoaded, + ), + ), + itemsCount: widget.data.length, + contentWidget: (index) => ListOptionsMenu( + list: widget.data[index], + listType: widget.type, + child: CustomListTile( + title: widget.data[index].name, + subtitle: "${intFormat(widget.data[index].rulesCount, Platform.localeName)} ${AppLocalizations.of(context)!.enabledRules}", + trailing: Icon( + widget.data[index].enabled == true + ? Icons.check_circle_rounded + : Icons.cancel, + color: widget.data[index].enabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red + ), + onTap: () => widget.onOpenDetailsScreen(widget.data[index], widget.type), + ), + ), + noData: Container( + width: double.maxFinite, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: Text( + widget.type == 'blacklist' + ? AppLocalizations.of(context)!.noBlackLists + : AppLocalizations.of(context)!.noWhiteLists, + textAlign: TextAlign.center, style: TextStyle( - fontSize: 22, + fontSize: 24, color: Theme.of(context).colorScheme.onSurfaceVariant, ), - ) - ], - ), - ); - - default: - return const SizedBox(); - } - + ), + ), + const SizedBox(height: 30), + TextButton.icon( + onPressed: () async { + final result = await filteringProvider.fetchFilters(); + if (result == false && mounted) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.errorLoadFilters, + color: Colors.red + ); + } + }, + icon: const Icon(Icons.refresh_rounded), + label: Text(AppLocalizations.of(context)!.refresh), + ) + ], + ), + ), + errorGenerator: () => SizedBox( + width: double.maxFinite, + height: MediaQuery.of(context).size.height-171, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon( + Icons.error, + color: Colors.red, + size: 50, + ), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.filtersNotLoaded, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ), + loadStatus: widget.loadStatus, + onRefresh: () async { + final result = await filteringProvider.fetchFilters(); + if (result == false && mounted) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.errorLoadFilters, + color: Colors.red + ); + } + }, + fab: AddFiltersButton( + type: widget.type, + widget: (fn) => FloatingActionButton( + onPressed: fn, + child: const Icon(Icons.add), + ), + ), + fabVisible: isVisible, + ); } } \ No newline at end of file diff --git a/lib/screens/filters/filters_tabs_view.dart b/lib/screens/filters/filters_tabs_view.dart new file mode 100644 index 0000000..bc96bc8 --- /dev/null +++ b/lib/screens/filters/filters_tabs_view.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/screens/filters/custom_rules_list.dart'; +import 'package:adguard_home_manager/screens/filters/filters_list.dart'; + +import 'package:adguard_home_manager/functions/desktop_mode.dart'; +import 'package:adguard_home_manager/providers/filtering_provider.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/models/filtering.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; + +class FiltersTabsView extends StatefulWidget { + final AppConfigProvider appConfigProvider; + final List actions; + final void Function(String) onRemoveCustomRule; + final void Function(Filter, String) onOpenDetailsModal; + + const FiltersTabsView({ + super.key, + required this.appConfigProvider, + required this.actions, + required this.onOpenDetailsModal, + required this.onRemoveCustomRule + }); + + @override + State createState() => _FiltersTabsViewState(); +} + +class _FiltersTabsViewState extends State with TickerProviderStateMixin { + late TabController tabController; + final ScrollController scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + tabController = TabController( + initialIndex: 0, + length: 3, + vsync: this, + ); + tabController.addListener(() => widget.appConfigProvider.setSelectedFiltersTab(tabController.index)); + } + + @override + Widget build(BuildContext context) { + final filteringProvider = Provider.of(context); + + final width = MediaQuery.of(context).size.width; + + return DefaultTabController( + length: 3, + child: NestedScrollView( + controller: scrollController, + headerSliverBuilder: ((context, innerBoxIsScrolled) { + return [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverAppBar( + title: Text(AppLocalizations.of(context)!.filters), + pinned: true, + floating: true, + forceElevated: innerBoxIsScrolled, + centerTitle: false, + actions: widget.actions, + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, + bottom: TabBar( + controller: tabController, + isScrollable: true, + unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant, + tabAlignment: TabAlignment.start, + tabs: [ + Tab( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.verified_user_rounded), + const SizedBox(width: 8), + Text(AppLocalizations.of(context)!.whitelists) + ], + ), + ), + Tab( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.gpp_bad_rounded), + const SizedBox(width: 8), + Text(AppLocalizations.of(context)!.blacklists) + ], + ), + ), + Tab( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.shield_rounded), + const SizedBox(width: 8), + Text(AppLocalizations.of(context)!.customRules) + ], + ), + ), + ] + ) + ), + ) + ]; + }), + body: TabBarView( + controller: tabController, + children: [ + FiltersList( + loadStatus: filteringProvider.loadStatus, + scrollController: scrollController, + type: 'whitelist', + data: filteringProvider.loadStatus == LoadStatus.loaded + ? filteringProvider.filtering!.whitelistFilters : [], + onOpenDetailsScreen: widget.onOpenDetailsModal, + ), + FiltersList( + loadStatus: filteringProvider.loadStatus, + scrollController: scrollController, + type: 'blacklist', + data: filteringProvider.loadStatus == LoadStatus.loaded + ? filteringProvider.filtering!.filters : [], + onOpenDetailsScreen: widget.onOpenDetailsModal, + ), + CustomRulesList( + loadStatus: filteringProvider.loadStatus, + scrollController: scrollController, + data: filteringProvider.loadStatus == LoadStatus.loaded + ? filteringProvider.filtering!.userRules : [], + onRemoveCustomRule: widget.onRemoveCustomRule, + ), + ] + ) + ) + ); + } +} \ No newline at end of file diff --git a/lib/screens/filters/filters_triple_column.dart b/lib/screens/filters/filters_triple_column.dart new file mode 100644 index 0000000..a659fa8 --- /dev/null +++ b/lib/screens/filters/filters_triple_column.dart @@ -0,0 +1,364 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:contextmenu/contextmenu.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/widgets/options_menu.dart'; +import 'package:adguard_home_manager/screens/filters/add_button.dart'; +import 'package:adguard_home_manager/screens/filters/list_options_menu.dart'; +import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; + +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/functions/desktop_mode.dart'; +import 'package:adguard_home_manager/models/menu_option.dart'; +import 'package:adguard_home_manager/functions/snackbar.dart'; +import 'package:adguard_home_manager/functions/copy_clipboard.dart'; +import 'package:adguard_home_manager/providers/filtering_provider.dart'; +import 'package:adguard_home_manager/models/filtering.dart'; +import 'package:adguard_home_manager/functions/number_format.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; + +class FiltersTripleColumn extends StatelessWidget { + final void Function(String) onRemoveCustomRule; + final void Function(Filter, String) onOpenDetailsModal; + final List actions; + + const FiltersTripleColumn({ + super.key, + required this.onRemoveCustomRule, + required this.onOpenDetailsModal, + required this.actions, + }); + + @override + Widget build(BuildContext context) { + final filteringProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + + final width = MediaQuery.of(context).size.width; + + Widget? generateSubtitle(String rule) { + final allowRegex = RegExp(r'^@@.*$'); + final blockRegex = RegExp(r'^\|\|.*$'); + final commentRegex = RegExp(r'^(#|!).*$'); + + if (allowRegex.hasMatch(rule)) { + return Text( + AppLocalizations.of(context)!.allowed, + style: const TextStyle( + color: Colors.green + ), + ); + } + else if (blockRegex.hasMatch(rule)) { + return Text( + AppLocalizations.of(context)!.blocked, + style: const TextStyle( + color: Colors.red + ), + ); + } + else if (commentRegex.hasMatch(rule)) { + return Text( + AppLocalizations.of(context)!.comment, + style: const TextStyle( + color: Colors.grey + ), + ); + } + else { + return null; + } + } + + return Scaffold( + appBar: AppBar( + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, + title: Text(AppLocalizations.of(context)!.filters), + actions: [ + IconButton( + onPressed: () async { + final result = await filteringProvider.fetchFilters(); + if (result == false && context.mounted) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.errorLoadFilters, + color: Colors.red + ); + } + }, + icon: const Icon(Icons.refresh_rounded), + tooltip: AppLocalizations.of(context)!.refresh, + ), + ...actions + ], + ), + body: Builder( + builder: (context) { + switch (filteringProvider.loadStatus) { + case LoadStatus.loading: + return Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.loadingFilters, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ], + ); + + case LoadStatus.loaded: + return Row( + children: [ + Expanded( + flex: 1, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + AppLocalizations.of(context)!.whitelists, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500 + ), + ), + AddFiltersButton( + type: 'whitelist', + widget: (fn) => IconButton( + onPressed: fn, + icon: const Icon(Icons.add_rounded), + tooltip: AppLocalizations.of(context)!.addWhitelist, + ), + ) + ], + ), + ), + Expanded( + child: ListView.builder( + itemCount: filteringProvider.filtering!.whitelistFilters.length, + itemBuilder: (context, index) => ListOptionsMenu( + list: filteringProvider.filtering!.whitelistFilters[index], + listType: 'whitelist', + child: CustomListTile( + title: filteringProvider.filtering!.whitelistFilters[index].name, + subtitle: "${intFormat(filteringProvider.filtering!.whitelistFilters[index].rulesCount, Platform.localeName)} ${AppLocalizations.of(context)!.enabledRules}", + trailing: Icon( + filteringProvider.filtering!.whitelistFilters[index].enabled == true + ? Icons.check_circle_rounded + : Icons.cancel, + color: filteringProvider.filtering!.whitelistFilters[index].enabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red + ), + onTap: () => onOpenDetailsModal(filteringProvider.filtering!.whitelistFilters[index], 'whitelist'), + ), + ), + ), + ) + ], + ), + ), + Expanded( + flex: 1, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + AppLocalizations.of(context)!.blacklists, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500 + ), + ), + AddFiltersButton( + type: 'blacklist', + widget: (fn) => IconButton( + onPressed: fn, + icon: const Icon(Icons.add_rounded), + tooltip: AppLocalizations.of(context)!.addBlacklist, + ) + ) + ], + ), + ), + Expanded( + child: ListView.builder( + itemCount: filteringProvider.filtering!.filters.length, + itemBuilder: (context, index) => ListOptionsMenu( + list: filteringProvider.filtering!.filters[index], + listType: 'blacklist', + child: CustomListTile( + title: filteringProvider.filtering!.filters[index].name, + subtitle: "${intFormat(filteringProvider.filtering!.filters[index].rulesCount, Platform.localeName)} ${AppLocalizations.of(context)!.enabledRules}", + trailing: Icon( + filteringProvider.filtering!.filters[index].enabled == true + ? Icons.check_circle_rounded + : Icons.cancel, + color: filteringProvider.filtering!.filters[index].enabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red + ), + onTap: () => onOpenDetailsModal(filteringProvider.filtering!.filters[index], 'blacklist'), + ), + ), + ), + ) + ], + ), + ), + Expanded( + flex: 1, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + AppLocalizations.of(context)!.customRules, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500 + ), + ), + ), + const SizedBox(width: 16), + AddFiltersButton( + type: 'edit_custom_rule', + widget: (fn) => IconButton( + onPressed: fn, + icon: const Icon(Icons.edit_rounded), + tooltip: AppLocalizations.of(context)!.editCustomRules, + ) + ), + const SizedBox(width: 8), + AddFiltersButton( + type: 'add_custom_rule', + widget: (fn) => IconButton( + onPressed: fn, + icon: const Icon(Icons.add_rounded), + tooltip: AppLocalizations.of(context)!.addCustomRule, + ) + ), + ], + ), + ), + Expanded( + child: ListView.builder( + itemCount: filteringProvider.filtering!.userRules.length, + itemBuilder: (context, index) => ContextMenuArea( + builder: (context) => [ + CustomListTile( + title: AppLocalizations.of(context)!.copyClipboard, + icon: Icons.copy_rounded, + onTap: () { + copyToClipboard( + value: filteringProvider.filtering!.userRules[index], + successMessage: AppLocalizations.of(context)!.copiedClipboard, + ); + Navigator.pop(context); + } + ), + ], + child: OptionsMenu( + options: (_) => [ + MenuOption( + title: AppLocalizations.of(context)!.copyClipboard, + icon: Icons.copy_rounded, + action: () => copyToClipboard( + value: filteringProvider.filtering!.userRules[index], + successMessage: AppLocalizations.of(context)!.copiedClipboard, + ) + ) + ], + child: CustomListTile( + title: filteringProvider.filtering!.userRules[index], + subtitleWidget: generateSubtitle(filteringProvider.filtering!.userRules[index]), + trailing: IconButton( + onPressed: () => onRemoveCustomRule(filteringProvider.filtering!.userRules[index]), + icon: const Icon(Icons.delete), + tooltip: AppLocalizations.of(context)!.delete, + ), + ), + ), + ), + ), + ) + ], + ), + ), + ], + ); + + case LoadStatus.error: + return SizedBox.expand( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon( + Icons.error, + color: Colors.red, + size: 50, + ), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.filtersNotLoaded, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ], + ), + ); + + default: + return const SizedBox(); + } + }, + ) + ); + } +} \ No newline at end of file diff --git a/lib/screens/filters/list_details_screen.dart b/lib/screens/filters/list_details_screen.dart deleted file mode 100644 index eccb130..0000000 --- a/lib/screens/filters/list_details_screen.dart +++ /dev/null @@ -1,320 +0,0 @@ -// ignore_for_file: use_build_context_synchronously - -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:provider/provider.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -import 'package:adguard_home_manager/screens/filters/filter_list_tile.dart'; -import 'package:adguard_home_manager/screens/filters/add_list_modal.dart'; -import 'package:adguard_home_manager/screens/filters/delete_list_modal.dart'; - -import 'package:adguard_home_manager/functions/format_time.dart'; -import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/functions/snackbar.dart'; -import 'package:adguard_home_manager/classes/process_modal.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; -import 'package:adguard_home_manager/models/filtering.dart'; - -class ListDetailsScreen extends StatefulWidget { - final Filter list; - final String type; - - const ListDetailsScreen({ - Key? key, - required this.list, - required this.type, - }) : super(key: key); - - @override - State createState() => _ListDetailsScreenState(); -} - -class _ListDetailsScreenState extends State { - final ScrollController scrollController = ScrollController(); - String name = ""; - bool enabled = true; - bool fabVisible = true; - - @override - void initState() { - name = widget.list.name; - enabled = widget.list.enabled; - - scrollController.addListener(() { - if (scrollController.position.userScrollDirection == ScrollDirection.reverse) { - if (mounted && fabVisible == true) { - setState(() => fabVisible = false); - } - } - else { - if (scrollController.position.userScrollDirection == ScrollDirection.forward) { - if (mounted && fabVisible == false) { - setState(() => fabVisible = true); - } - } - } - }); - - super.initState(); - } - - @override - Widget build(BuildContext context) { - final serversProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - - void enableDisableList(Filter list, bool newStatus) async { - ProcessModal processModal = ProcessModal(context: context); - processModal.open( - enabled == true - ? AppLocalizations.of(context)!.disablingList - : AppLocalizations.of(context)!.enablingList, - ); - - final result = await updateFilterList(server: serversProvider.selectedServer!, data: { - "data": { - "enabled": newStatus, - "name": list.name, - "url": list.url - }, - "url": list.url, - "whitelist": widget.type == 'whitelist' ? true : false - }); - - processModal.close(); - - if (result['result'] == 'success') { - final result2 = await getFiltering(server: serversProvider.selectedServer!); - - if (result2['result'] == 'success') { - serversProvider.setFilteringData(result2['data']); - serversProvider.setFilteringLoadStatus(1, true); - } - else { - appConfigProvider.addLog(result2['log']); - serversProvider.setFilteringLoadStatus(2, true); - } - - setState(() => enabled = newStatus); - - showSnacbkar( - context: context, - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.listDataUpdated, - color: Colors.green - ); - } - else { - appConfigProvider.addLog(result['log']); - - showSnacbkar( - context: context, - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.listDataNotUpdated, - color: Colors.red - ); - } - } - - void confirmEditList({required Filter list, required String type}) async { - ProcessModal processModal = ProcessModal(context: context); - processModal.open(AppLocalizations.of(context)!.updatingListData); - - final result1 = await updateFilterList(server: serversProvider.selectedServer!, data: { - "data": { - "enabled": list.enabled, - "name": list.name, - "url": list.url - }, - "url": list.url, - "whitelist": type == 'whitelist' ? true : false - }); - - if (result1['result'] == 'success') { - final result2 = await getFiltering(server: serversProvider.selectedServer!); - - if (result2['result'] == 'success') { - serversProvider.setFilteringData(result2['data']); - serversProvider.setFilteringLoadStatus(1, true); - } - else { - appConfigProvider.addLog(result2['log']); - serversProvider.setFilteringLoadStatus(2, true); - } - - processModal.close(); - - setState(() => name = list.name); - - showSnacbkar( - context: context, - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.listDataUpdated, - color: Colors.green - ); - } - else { - processModal.close(); - appConfigProvider.addLog(result1['log']); - - showSnacbkar( - context: context, - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.listDataNotUpdated, - color: Colors.red - ); - } - } - - void deleteList(Filter list, String type) async { - ProcessModal processModal = ProcessModal(context: context); - processModal.open(AppLocalizations.of(context)!.deletingList); - - final result1 = await deleteFilterList(server: serversProvider.selectedServer!, data: { - "url": list.url, - "whitelist": type == 'whitelist' ? true : false - }); - - if (result1['result'] == 'success') { - final result2 = await getFiltering(server: serversProvider.selectedServer!); - - if (result2['result'] == 'success') { - serversProvider.setFilteringData(result2['data']); - serversProvider.setFilteringLoadStatus(1, true); - } - else { - appConfigProvider.addLog(result2['log']); - serversProvider.setFilteringLoadStatus(2, true); - } - - processModal.close(); - - Navigator.pop(context); // Closes the screen - - showSnacbkar( - context: context, - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.listDeleted, - color: Colors.green - ); - } - else { - processModal.close(); - appConfigProvider.addLog(result1['log']); - - showSnacbkar( - context: context, - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.listNotDeleted, - color: Colors.red - ); - } - } - - return Scaffold( - appBar: AppBar( - title: Text(AppLocalizations.of(context)!.listDetails), - actions: [ - IconButton( - onPressed: () => { - showModalBottomSheet( - context: context, - builder: (ctx) => AddListModal( - list: widget.list, - type: widget.type, - onEdit: confirmEditList - ), - isScrollControlled: true, - backgroundColor: Colors.transparent - ) - }, - icon: const Icon(Icons.edit), - tooltip: AppLocalizations.of(context)!.edit, - ), - IconButton( - onPressed: () { - showDialog( - context: context, - builder: (context) => DeleteListModal( - onConfirm: () => deleteList(widget.list, widget.type), - ) - ); - }, - icon: const Icon(Icons.delete), - tooltip: AppLocalizations.of(context)!.delete, - ), - const SizedBox(width: 10), - ], - ), - body: Stack( - children: [ - ListView( - children: [ - FilterListTile( - icon: Icons.shield_rounded, - title: AppLocalizations.of(context)!.currentStatus, - subtitle: enabled == true - ? AppLocalizations.of(context)!.enabled - : AppLocalizations.of(context)!.disabled, - color: enabled == true - ? appConfigProvider.useThemeColorForStatus == true - ? Theme.of(context).colorScheme.primary - : Colors.green - : appConfigProvider.useThemeColorForStatus == true - ? Colors.grey - : Colors.red, - bold: true, - ), - FilterListTile( - icon: Icons.badge_rounded, - title: AppLocalizations.of(context)!.name, - subtitle: name - ), - FilterListTile( - icon: Icons.link_rounded, - title: "URL", - subtitle: widget.list.url - ), - FilterListTile( - icon: Icons.list_rounded, - title: AppLocalizations.of(context)!.rules, - subtitle: widget.list.rulesCount.toString() - ), - FilterListTile( - icon: Icons.shield_rounded, - title: AppLocalizations.of(context)!.listType, - subtitle: widget.type == 'whitelist' - ? AppLocalizations.of(context)!.whitelist - : AppLocalizations.of(context)!.blacklist, - ), - if (widget.list.lastUpdated != null) FilterListTile( - icon: Icons.schedule_rounded, - title: AppLocalizations.of(context)!.latestUpdate, - subtitle: convertTimestampLocalTimezone(widget.list.lastUpdated!, 'dd-MM-yyyy HH:mm'), - ), - ], - ), - AnimatedPositioned( - duration: const Duration(milliseconds: 100), - curve: Curves.easeInOut, - bottom: fabVisible ? - appConfigProvider.showingSnackbar - ? 70 : 20 - : -70, - right: 20, - child: FloatingActionButton( - onPressed: () => enableDisableList(widget.list, !enabled), - child: Icon( - enabled == true - ? Icons.gpp_bad_rounded - : Icons.verified_user_rounded, - ), - ), - ) - ], - ), - ); - } -} \ No newline at end of file diff --git a/lib/screens/filters/list_options_menu.dart b/lib/screens/filters/list_options_menu.dart new file mode 100644 index 0000000..4d2f57f --- /dev/null +++ b/lib/screens/filters/list_options_menu.dart @@ -0,0 +1,187 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:contextmenu/contextmenu.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; +import 'package:adguard_home_manager/widgets/options_menu.dart'; +import 'package:adguard_home_manager/screens/filters/selection/selection_screen.dart'; + +import 'package:adguard_home_manager/functions/open_url.dart'; +import 'package:adguard_home_manager/classes/process_modal.dart'; +import 'package:adguard_home_manager/functions/snackbar.dart'; +import 'package:adguard_home_manager/models/filtering.dart'; +import 'package:adguard_home_manager/providers/filtering_provider.dart'; +import 'package:adguard_home_manager/functions/copy_clipboard.dart'; +import 'package:adguard_home_manager/models/menu_option.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; + +class ListOptionsMenu extends StatelessWidget { + final Filter list; + final Widget child; + final String listType; + + const ListOptionsMenu({ + super.key, + required this.list, + required this.child, + required this.listType, + }); + + @override + Widget build(BuildContext context) { + final filteringProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + + final width = MediaQuery.of(context).size.width; + + void enableDisable() async { + ProcessModal processModal = ProcessModal(); + processModal.open( + list.enabled == true + ? AppLocalizations.of(context)!.disablingList + : AppLocalizations.of(context)!.enablingList + ); + + final result = await filteringProvider.updateList( + list: list, + type: listType, + action: list.enabled == true + ? FilteringListActions.disable + : FilteringListActions.enable + ); + + processModal.close(); + + if (!context.mounted) return; + if (result == true) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.listDataUpdated, + color: Colors.green + ); + } + else { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.listDataNotUpdated, + color: Colors.red + ); + } + } + + void openSelectionMode() { + showGeneralDialog( + context: context, + barrierColor: !(width > 900 || !(Platform.isAndroid | Platform.isIOS)) + ?Colors.transparent + : Colors.black54, + transitionBuilder: (context, anim1, anim2, child) { + return SlideTransition( + position: Tween( + begin: const Offset(0, 1), + end: const Offset(0, 0) + ).animate( + CurvedAnimation( + parent: anim1, + curve: Curves.easeInOutCubicEmphasized + ) + ), + child: child, + ); + }, + pageBuilder: (context, animation, secondaryAnimation) => SelectionScreen( + isModal: width > 900 || !(Platform.isAndroid | Platform.isIOS) + ) + ); + } + + return ContextMenuArea( + builder: (context) => [ + CustomListTile( + title: list.enabled == true + ? AppLocalizations.of(context)!.disable + : AppLocalizations.of(context)!.enable, + icon: list.enabled == true + ? Icons.gpp_bad_rounded + : Icons.verified_user_rounded, + onTap: () { + Navigator.pop(context); // Closes the context menu + enableDisable(); + } + ), + CustomListTile( + title: AppLocalizations.of(context)!.copyListUrl, + icon: Icons.copy_rounded, + onTap: () { + Navigator.pop(context); // Closes the context menu + copyToClipboard( + value: list.url, + successMessage: AppLocalizations.of(context)!.listUrlCopied + ); + } + ), + CustomListTile( + title: AppLocalizations.of(context)!.openListUrl, + icon: Icons.open_in_browser_rounded, + onTap: () { + Navigator.pop(context); // Closes the context menu + openUrl(list.url); + } + ), + CustomListTile( + title: AppLocalizations.of(context)!.selectionMode, + icon: Icons.check_rounded, + onTap: () { + Navigator.pop(context); // Closes the context menu + openSelectionMode(); + } + ), + ], + child: Material( + color: Colors.transparent, + child: InkWell( + child: OptionsMenu( + options: (_) => [ + MenuOption( + title: list.enabled == true + ? AppLocalizations.of(context)!.disable + : AppLocalizations.of(context)!.enable, + icon: list.enabled == true + ? Icons.gpp_bad_rounded + : Icons.verified_user_rounded, + action: () => enableDisable() + ), + MenuOption( + title: AppLocalizations.of(context)!.copyListUrl, + icon: Icons.copy_rounded, + action: () => copyToClipboard( + value: list.url, + successMessage: AppLocalizations.of(context)!.listUrlCopied + ) + ), + MenuOption( + title: AppLocalizations.of(context)!.openListUrl, + icon: Icons.open_in_browser_rounded, + action: () => openUrl(list.url) + ), + MenuOption( + title: AppLocalizations.of(context)!.selectionMode, + icon: Icons.check_rounded, + action: () => Future.delayed( + const Duration(milliseconds: 0), + () => openSelectionMode() + ) + ), + ], + child: child + ) + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/filters/modals/blocked_services_screen.dart b/lib/screens/filters/modals/blocked_services_screen.dart new file mode 100644 index 0000000..ac54c61 --- /dev/null +++ b/lib/screens/filters/modals/blocked_services_screen.dart @@ -0,0 +1,398 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/models/blocked_services.dart'; +import 'package:adguard_home_manager/functions/snackbar.dart'; +import 'package:adguard_home_manager/classes/process_modal.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/providers/filtering_provider.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; + +class BlockedServicesScreen extends StatefulWidget { + final bool fullScreen; + + const BlockedServicesScreen({ + super.key, + required this.fullScreen + }); + + @override + State createState() => _BlockedServicesScreenStateWidget(); +} + +class _BlockedServicesScreenStateWidget extends State { + List values = []; + + @override + void initState() { + final filteringProvider = Provider.of(context, listen: false); + + if (filteringProvider.blockedServicesLoadStatus != LoadStatus.loaded) { + filteringProvider.loadBlockedServices(showLoading: true); + } + + values = filteringProvider.filtering!.blockedServices; + + super.initState(); + } + + @override + Widget build(BuildContext context) { + final filteringProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + + void updateValues(bool value, BlockedService item) { + if (value == true) { + setState(() { + values = values.where((v) => v != item.id).toList(); + }); + } + else { + setState(() { + values.add(item.id); + }); + } + } + + void updateBlockedServices() async { + ProcessModal processModal = ProcessModal(); + processModal.open(AppLocalizations.of(context)!.updating); + + final result = await filteringProvider.updateBlockedServices(values); + + processModal.close(); + + if (result == true) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.blockedServicesUpdated, + color: Colors.green + ); + } + else { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.blockedServicesNotUpdated, + color: Colors.red + ); + } + } + + if (widget.fullScreen == true) { + return Dialog.fullscreen( + child: Material( + child: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverAppBar.large( + pinned: true, + floating: true, + centerTitle: false, + forceElevated: innerBoxIsScrolled, + leading: CloseButton(onPressed: () => Navigator.pop(context)), + title: Text(AppLocalizations.of(context)!.blockedServices), + actions: [ + IconButton( + onPressed: updateBlockedServices, + icon: const Icon( + Icons.save_rounded + ), + tooltip: AppLocalizations.of(context)!.save, + ), + const SizedBox(width: 10) + ], + ) + ) + ], + body: SafeArea( + top: false, + bottom: true, + child: Builder( + builder: (context) => CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + ), + if (filteringProvider.blockedServicesLoadStatus == LoadStatus.loading) SliverFillRemaining( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + width: double.maxFinite, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.loadingBlockedServicesList, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ), + ), + if (filteringProvider.blockedServicesLoadStatus == LoadStatus.loaded) SliverList.builder( + itemCount: filteringProvider.blockedServices!.services.length, + itemBuilder: (context, index) => Material( + color: Colors.transparent, + child: InkWell( + onTap: () => updateValues( + values.contains(filteringProvider.blockedServices!.services[index].id), + filteringProvider.blockedServices!.services[index] + ), + child: Padding( + padding: const EdgeInsets.only( + top: 6, + bottom: 6, + right: 12, + left: 24 + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + filteringProvider.blockedServices!.services[index].name, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface + ), + ), + Checkbox( + value: values.contains(filteringProvider.blockedServices!.services[index].id), + onChanged: (value) => updateValues( + value!, + filteringProvider.blockedServices!.services[index] + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5) + ), + ) + ], + ), + ), + ), + ) + ), + if (filteringProvider.blockedServicesLoadStatus == LoadStatus.error) SliverFillRemaining( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + width: double.maxFinite, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon( + Icons.error, + color: Colors.red, + size: 50, + ), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.blockedServicesListNotLoaded, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ), + ) + ], + ), + ) + ) + ), + ), + ); + } + else { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 400 + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.clear_rounded), + tooltip: AppLocalizations.of(context)!.close, + ), + const SizedBox(width: 8), + Text( + AppLocalizations.of(context)!.blockedServices, + style: const TextStyle( + fontSize: 22 + ), + ) + ], + ), + IconButton( + onPressed: updateBlockedServices, + icon: const Icon( + Icons.save_rounded + ), + tooltip: AppLocalizations.of(context)!.save, + ), + ], + ), + ), + Expanded( + child: Builder( + builder: (ctx) { + switch (filteringProvider.blockedServicesLoadStatus) { + case LoadStatus.loading: + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + width: double.maxFinite, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.loadingBlockedServicesList, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ); + + case LoadStatus.loaded: + return ListView.builder( + itemCount: filteringProvider.blockedServices!.services.length, + itemBuilder: (context, index) => Material( + color: Colors.transparent, + child: InkWell( + onTap: () => updateValues( + values.contains(filteringProvider.blockedServices!.services[index].id), + filteringProvider.blockedServices!.services[index] + ), + child: Padding( + padding: const EdgeInsets.only( + top: 6, + bottom: 6, + right: 12, + left: 24 + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + filteringProvider.blockedServices!.services[index].name, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface + ), + ), + Checkbox( + value: values.contains(filteringProvider.blockedServices!.services[index].id), + onChanged: (value) => updateValues( + value!, + filteringProvider.blockedServices!.services[index] + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5) + ), + ) + ], + ), + ), + ), + ) + ); + + case LoadStatus.error: + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + width: double.maxFinite, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon( + Icons.error, + color: Colors.red, + size: 50, + ), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.blockedServicesListNotLoaded, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ); + + default: + return const SizedBox(); + } + }, + ) + ), + ], + ) + ), + ); + } + } +} + + +void openBlockedServicesModal({ + required BuildContext context, + required double width, +}) { + showGeneralDialog( + context: context, + barrierColor: !(width > 700 || !(Platform.isAndroid || Platform.isIOS)) + ?Colors.transparent + : Colors.black54, + transitionBuilder: (context, anim1, anim2, child) { + return SlideTransition( + position: Tween( + begin: const Offset(0, 1), + end: const Offset(0, 0) + ).animate( + CurvedAnimation( + parent: anim1, + curve: Curves.easeInOutCubicEmphasized + ) + ), + child: child, + ); + }, + pageBuilder: (context, animation, secondaryAnimation) => BlockedServicesScreen( + fullScreen: !(width > 700 || !(Platform.isAndroid || Platform.isIOS)), + ), + ); +} \ No newline at end of file diff --git a/lib/screens/filters/modals/custom_rules/add_custom_rule.dart b/lib/screens/filters/modals/custom_rules/add_custom_rule.dart new file mode 100644 index 0000000..674c200 --- /dev/null +++ b/lib/screens/filters/modals/custom_rules/add_custom_rule.dart @@ -0,0 +1,354 @@ +import 'package:adguard_home_manager/constants/regexps.dart'; +import 'package:flutter/material.dart'; +import 'package:segmented_button_slide/segmented_button_slide.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/screens/filters/modals/custom_rules/custom_rule_docs.dart'; + +enum _BlockingPresets { block, unblock, custom } + +class AddCustomRule extends StatefulWidget { + final void Function(String) onConfirm; + final bool fullScreen; + + const AddCustomRule({ + super.key, + required this.onConfirm, + required this.fullScreen + }); + + @override + State createState() => _AddCustomRuleState(); +} + +class _AddCustomRuleState extends State { + final TextEditingController _domainController = TextEditingController(); + String? _domainError; + + _BlockingPresets _preset = _BlockingPresets.block; + + bool _addImportant = false; + + bool _checkValidValues() { + if ( + _domainController.text != '' && + _domainError == null + ) { + return true; + } + else { + return false; + } + } + + void validateDomain(String value) { + if (Regexps.domain.hasMatch(value)) { + setState(() => _domainError = null); + } + else { + setState(() => _domainError = AppLocalizations.of(context)!.domainNotValid); + } + _checkValidValues(); + } + + @override + Widget build(BuildContext context) { + if (widget.fullScreen == true) { + return Dialog.fullscreen( + child: Material( + child: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverAppBar.large( + pinned: true, + floating: true, + centerTitle: false, + forceElevated: innerBoxIsScrolled, + leading: CloseButton(onPressed: () => Navigator.pop(context)), + title: Text(AppLocalizations.of(context)!.addCustomRule), + actions: [ + IconButton( + onPressed: _checkValidValues() == true + ? () { + Navigator.pop(context); + widget.onConfirm( + _buildRule( + domainController: _domainController, + important: _addImportant, + preset: _preset + ) + ); + } + : null, + icon: const Icon(Icons.check) + ), + const SizedBox(width: 10) + ], + ) + ) + ], + body: SafeArea( + top: false, + bottom: true, + child: Builder( + builder: (context) => CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + ), + SliverList.list( + children: [ + _CustomRuleEditor( + domainController: _domainController, + domainError: _domainError, + important: _addImportant, + preset: _preset, + setImportant: (v) => setState(() => _addImportant = v), + setPreset: (v) => setState(() => _preset = v), + validateDomain: validateDomain + ) + ] + ) + ], + ), + ) + ) + ), + ), + ); + } + else { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500 + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.clear_rounded), + tooltip: AppLocalizations.of(context)!.close, + ), + const SizedBox(width: 8), + Text( + AppLocalizations.of(context)!.addCustomRule, + style: const TextStyle( + fontSize: 22 + ), + ), + ], + ), + IconButton( + onPressed: _checkValidValues() == true + ? () { + Navigator.pop(context); + widget.onConfirm( + _buildRule( + domainController: _domainController, + important: _addImportant, + preset: _preset + ) + ); + } + : null, + icon: const Icon(Icons.check) + ) + ], + ), + ), + Flexible( + child: SingleChildScrollView( + child: Wrap( + alignment: WrapAlignment.center, + children: [ + _CustomRuleEditor( + domainController: _domainController, + domainError: _domainError, + important: _addImportant, + preset: _preset, + setImportant: (v) => setState(() => _addImportant = v), + setPreset: (v) => setState(() => _preset = v), + validateDomain: validateDomain + ) + ] + ), + ), + ) + ], + ), + ), + ); + } + } +} + +class _CustomRuleEditor extends StatelessWidget { + final TextEditingController domainController; + final String? domainError; + final bool important; + final void Function(bool) setImportant; + final _BlockingPresets preset; + final void Function(_BlockingPresets) setPreset; + final void Function(String) validateDomain; + + const _CustomRuleEditor({ + required this.domainController, + required this.domainError, + required this.important, + required this.setImportant, + required this.preset, + required this.setPreset, + required this.validateDomain, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 5 + ), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(30), + border: Border.all( + color: Theme.of(context).colorScheme.primary + ) + ), + child: Text( + _buildRule( + domainController: domainController, + important: important, + preset: preset, + ), + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.w500 + ), + ) + ), + ], + ), + Container(height: 30), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: TextFormField( + controller: domainController, + onChanged: validateDomain, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.link_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + errorText: domainError, + labelText: AppLocalizations.of(context)!.domain, + ), + ), + ), + Container(height: 30), + SegmentedButtonSlide( + entries: [ + SegmentedButtonSlideEntry(label: AppLocalizations.of(context)!.block), + SegmentedButtonSlideEntry(label: AppLocalizations.of(context)!.unblock), + SegmentedButtonSlideEntry(label: AppLocalizations.of(context)!.custom), + ], + selectedEntry: preset.index, + onChange: (v) => setPreset(_BlockingPresets.values[v]), + colors: SegmentedButtonSlideColors( + barColor: Theme.of(context).colorScheme.primary.withOpacity(0.2), + backgroundSelectedColor: Theme.of(context).colorScheme.primary, + ), + textOverflow: TextOverflow.ellipsis, + height: 40, + margin: const EdgeInsets.symmetric( + horizontal: 24, + ), + selectedTextStyle: TextStyle( + color: Theme.of(context).colorScheme.onPrimary, + fontWeight: FontWeight.w700 + ), + unselectedTextStyle: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + hoverTextStyle: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + Container(height: 20), + Material( + color: Colors.transparent, + child: InkWell( + onTap: () => setImportant(!important), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 28), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 10), + child: Text( + AppLocalizations.of(context)!.addImportant, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface + ), + ), + ), + Switch( + value: important, + onChanged: setImportant, + ) + ], + ), + ), + ), + ), + Container(height: 20), + const CustomRuleDocs(), + Container(height: 20) + ] + ); + } +} + + String _buildRule({ + String? value, + required TextEditingController domainController, + required _BlockingPresets preset, + required bool important +}) { + String rule = ""; + String fieldValue = value ?? domainController.text; + if (preset == _BlockingPresets.block) { + rule = "||${fieldValue.trim()}^"; + } + else if (preset == _BlockingPresets.unblock) { + rule = "@@||${fieldValue.trim()}^"; + } + else { + rule = fieldValue.trim(); + } + if (important == true) { + rule = "$rule\$important"; + } + return rule; + } \ No newline at end of file diff --git a/lib/screens/filters/modals/custom_rules/custom_rule_docs.dart b/lib/screens/filters/modals/custom_rules/custom_rule_docs.dart new file mode 100644 index 0000000..3d2c359 --- /dev/null +++ b/lib/screens/filters/modals/custom_rules/custom_rule_docs.dart @@ -0,0 +1,166 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/constants/urls.dart'; +import 'package:adguard_home_manager/functions/open_url.dart'; + +class CustomRuleDocs extends StatelessWidget { + const CustomRuleDocs({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Row( + children: [ + Icon( + Icons.info, + color: Theme.of(context).colorScheme.onSurface + ), + const SizedBox(width: 20), + Text( + AppLocalizations.of(context)!.examples, + style: TextStyle( + fontSize: 18, + color: Theme.of(context).colorScheme.onSurface + ), + ) + ], + ), + const SizedBox(height: 20), + SizedBox( + width: double.maxFinite, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "||example.org^", + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.primary + ), + ), + const SizedBox(height: 5), + Text( + AppLocalizations.of(context)!.example1, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.primary + ), + ), + const SizedBox(height: 20), + Text( + "@@||example.org^", + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.primary + ), + ), + const SizedBox(height: 5), + Text( + AppLocalizations.of(context)!.example2, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.primary + ), + ), + const SizedBox(height: 20), + Text( + "! Here goes a comment", + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.primary + ), + ), + Text( + "# Also a comment", + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.primary + ), + ), + const SizedBox(height: 5), + Text( + AppLocalizations.of(context)!.example3, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.primary + ), + ), + const SizedBox(height: 20), + Text( + "/REGEX/", + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.primary + ), + ), + const SizedBox(height: 5), + Text( + AppLocalizations.of(context)!.example4, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.primary + ), + ), + ], + ), + ) + ], + ), + ), + ), + ), + Container(height: 8), + Material( + color: Colors.transparent, + child: InkWell( + onTap: () => openUrl(Urls.customRuleDocs), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 10), + child: Text( + AppLocalizations.of(context)!.moreInformation, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface + ), + ), + ), + Padding( + padding: const EdgeInsets.only(right: 15), + child: Icon( + Icons.open_in_new, + color: Theme.of(context).colorScheme.onSurface + ), + ) + ], + ), + ), + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/filters/modals/custom_rules/edit_custom_rules.dart b/lib/screens/filters/modals/custom_rules/edit_custom_rules.dart new file mode 100644 index 0000000..b063411 --- /dev/null +++ b/lib/screens/filters/modals/custom_rules/edit_custom_rules.dart @@ -0,0 +1,188 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/screens/filters/modals/custom_rules/custom_rule_docs.dart'; + +import 'package:adguard_home_manager/providers/filtering_provider.dart'; + +class EditCustomRules extends StatefulWidget { + final bool fullScreen; + final void Function(List) onConfirm; + + const EditCustomRules({ + super.key, + required this.fullScreen, + required this.onConfirm, + }); + + @override + State createState() => _EditCustomRulesState(); +} + +class _EditCustomRulesState extends State { + final _fieldController = TextEditingController(); + + @override + void initState() { + final filteringProvider = Provider.of(context, listen: false); + if (filteringProvider.filtering != null) { + _fieldController.text = filteringProvider.filtering!.userRules.join("\n"); + } + + super.initState(); + } + + @override + Widget build(BuildContext context) { + if (widget.fullScreen == true) { + return Dialog.fullscreen( + child: Material( + child: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverAppBar.large( + pinned: true, + floating: true, + centerTitle: false, + forceElevated: innerBoxIsScrolled, + leading: CloseButton(onPressed: () => Navigator.pop(context)), + title: Text(AppLocalizations.of(context)!.editCustomRules), + actions: [ + IconButton( + onPressed: () { + Navigator.pop(context); + widget.onConfirm(_fieldController.text.split("\n")); + }, + icon: const Icon(Icons.save_rounded), + tooltip: AppLocalizations.of(context)!.save, + ), + const SizedBox(width: 10) + ], + ) + ) + ], + body: SafeArea( + top: false, + bottom: true, + child: Builder( + builder: (context) => CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + ), + SliverList.list( + children: [ + _CustomRulesRawEditor(fieldController: _fieldController) + ] + ) + ], + ), + ) + ) + ), + ), + ); + } + else { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500 + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.clear_rounded), + tooltip: AppLocalizations.of(context)!.close, + ), + const SizedBox(width: 8), + Text( + AppLocalizations.of(context)!.editCustomRules, + style: const TextStyle( + fontSize: 22 + ), + ), + ], + ), + IconButton( + onPressed: () { + Navigator.pop(context); + widget.onConfirm(_fieldController.text.split("\n")); + }, + icon: const Icon(Icons.save_rounded), + tooltip: AppLocalizations.of(context)!.save, + ) + ], + ), + ), + Flexible( + child: SingleChildScrollView( + child: Wrap( + alignment: WrapAlignment.center, + children: [ + _CustomRulesRawEditor(fieldController: _fieldController) + ] + ), + ), + ) + ], + ), + ), + ); + } + } +} + +class _CustomRulesRawEditor extends StatelessWidget { + final TextEditingController fieldController; + + const _CustomRulesRawEditor({ + required this.fieldController + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const SizedBox(height: 24), + SizedBox( + height: 300, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 28), + child: TextField( + controller: fieldController, + decoration: InputDecoration( + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + labelText: AppLocalizations.of(context)!.rules, + floatingLabelBehavior: FloatingLabelBehavior.always + ), + autocorrect: false, + expands: true, + minLines: null, + maxLines: null, + textAlignVertical: TextAlignVertical.top, + ), + ), + ), + const SizedBox(height: 24), + const CustomRuleDocs(), + const SizedBox(height: 16), + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/filters/modals/custom_rules/sort_rules.dart b/lib/screens/filters/modals/custom_rules/sort_rules.dart new file mode 100644 index 0000000..7ae0870 --- /dev/null +++ b/lib/screens/filters/modals/custom_rules/sort_rules.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/constants/enums.dart'; + +class SortCustomRulesModal extends StatelessWidget { + final CustomRulesSorting sortingMethod; + final void Function(CustomRulesSorting) onSelect; + + const SortCustomRulesModal({ + super.key, + required this.sortingMethod, + required this.onSelect, + }); + + @override + Widget build(BuildContext context) { + return AlertDialog( + contentPadding: const EdgeInsets.symmetric(vertical: 16), + scrollable: true, + title: Column( + children: [ + Icon( + Icons.sort_rounded, + size: 24, + color: Theme.of(context).listTileTheme.iconColor + ), + const SizedBox(height: 16), + Text( + AppLocalizations.of(context)!.sortingOptions, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface + ), + ) + ], + ), + content: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500 + ), + child: Column( + children: [ + _CustomListTileDialog( + title: AppLocalizations.of(context)!.topToBottom, + icon: Icons.arrow_downward_rounded, + onTap: () { + Navigator.pop(context); + onSelect(CustomRulesSorting.topBottom); + }, + isSelected: sortingMethod == CustomRulesSorting.topBottom + ), + _CustomListTileDialog( + title: AppLocalizations.of(context)!.bottomToTop, + icon: Icons.arrow_upward_rounded, + onTap: () { + Navigator.pop(context); + onSelect(CustomRulesSorting.bottomTop); + }, + isSelected: sortingMethod == CustomRulesSorting.bottomTop + ), + ] + ), + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.close) + ) + ], + ) + ], + ); + } +} + +class _CustomListTileDialog extends StatelessWidget { + final String title; + final IconData? icon; + final void Function()? onTap; + final bool isSelected; + + const _CustomListTileDialog({ + required this.title, + required this.icon, + required this.onTap, + required this.isSelected, + }); + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + child: Row( + children: [ + Icon( + icon, + color: Theme.of(context).colorScheme.onSurface, + ), + const SizedBox(width: 24), + Flexible( + child: Text( + title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + const SizedBox(width: 24), + Icon( + isSelected == true ? Icons.radio_button_checked_rounded : Icons.radio_button_unchecked_rounded, + color: Theme.of(context).colorScheme.primary, + ) + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/filters/delete_list_modal.dart b/lib/screens/filters/modals/delete_list_modal.dart similarity index 97% rename from lib/screens/filters/delete_list_modal.dart rename to lib/screens/filters/modals/delete_list_modal.dart index 5d751e4..86e7e88 100644 --- a/lib/screens/filters/delete_list_modal.dart +++ b/lib/screens/filters/modals/delete_list_modal.dart @@ -5,9 +5,9 @@ class DeleteListModal extends StatelessWidget { final void Function() onConfirm; const DeleteListModal({ - Key? key, + super.key, required this.onConfirm - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/screens/filters/remove_custom_rule_modal.dart b/lib/screens/filters/modals/remove_custom_rule_modal.dart similarity index 97% rename from lib/screens/filters/remove_custom_rule_modal.dart rename to lib/screens/filters/modals/remove_custom_rule_modal.dart index f1c4352..ec281f7 100644 --- a/lib/screens/filters/remove_custom_rule_modal.dart +++ b/lib/screens/filters/modals/remove_custom_rule_modal.dart @@ -5,9 +5,9 @@ class RemoveCustomRule extends StatelessWidget { final void Function() onConfirm; const RemoveCustomRule({ - Key? key, + super.key, required this.onConfirm - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/screens/filters/modals/update_interval_lists_modal.dart b/lib/screens/filters/modals/update_interval_lists_modal.dart new file mode 100644 index 0000000..ce7058b --- /dev/null +++ b/lib/screens/filters/modals/update_interval_lists_modal.dart @@ -0,0 +1,256 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/widgets/option_box.dart'; + + +class UpdateIntervalListsModal extends StatefulWidget { + final int interval; + final void Function(int) onChange; + final bool dialog; + + const UpdateIntervalListsModal({ + super.key, + required this.interval, + required this.onChange, + required this.dialog + }); + + @override + State createState() => _UpdateIntervalListsModalState(); +} + +class _UpdateIntervalListsModalState extends State { + int? selectedOption; + + void _updateRadioValue(int value) { + setState(() { + selectedOption = value; + }); + } + + @override + void initState() { + selectedOption = widget.interval; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final MediaQueryData mediaQueryData = MediaQuery.of(context); + + if (widget.dialog == true) { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500 + ), + child: _Content( + selectedOption: selectedOption, + onUpdateValue: _updateRadioValue, + onConfirm: () => widget.onChange(selectedOption!), + ) + ), + ); + } + else { + return Padding( + padding: mediaQueryData.viewInsets, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).dialogBackgroundColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28) + ), + ), + child: SafeArea( + child: _Content( + selectedOption: selectedOption, + onUpdateValue: _updateRadioValue, + onConfirm: () => widget.onChange(selectedOption!), + ), + ) + ), + ); + } + } +} + +class _Content extends StatelessWidget { + final int? selectedOption; + final void Function(int) onUpdateValue; + final void Function() onConfirm; + + const _Content({ + required this.selectedOption, + required this.onUpdateValue, + required this.onConfirm, + }); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: SingleChildScrollView( + child: Wrap( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 24), + child: Icon( + Icons.update_rounded, + size: 24, + color: Theme.of(context).listTileTheme.iconColor + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 16 + ), + child: Text( + AppLocalizations.of(context)!.updateFrequency, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 24, + color: Theme.of(context).colorScheme.onSurface + ), + ), + ), + ], + ), + ) + ], + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Wrap( + runSpacing: 16, + children: [ + FractionallySizedBox( + widthFactor: 0.5, + child: Padding( + padding: const EdgeInsets.only(right: 6), + child: OptionBox( + optionsValue: selectedOption, + itemValue: 0, + onTap: (v) => onUpdateValue(v as int), + label: AppLocalizations.of(context)!.never, + ), + ), + ), + FractionallySizedBox( + widthFactor: 0.5, + child: Padding( + padding: const EdgeInsets.only(left: 6), + child: OptionBox( + optionsValue: selectedOption, + itemValue: 1, + onTap: (v) => onUpdateValue(v as int), + label: AppLocalizations.of(context)!.hour1, + ), + ), + ), + FractionallySizedBox( + widthFactor: 0.5, + child: Padding( + padding: const EdgeInsets.only(right: 6), + child: OptionBox( + optionsValue: selectedOption, + itemValue: 12, + onTap: (v) => onUpdateValue(v as int), + label: AppLocalizations.of(context)!.hours12, + ), + ), + ), + FractionallySizedBox( + widthFactor: 0.5, + child: Padding( + padding: const EdgeInsets.only(left: 6), + child: OptionBox( + optionsValue: selectedOption, + itemValue: 24, + onTap: (v) => onUpdateValue(v as int), + label: AppLocalizations.of(context)!.hours24, + ), + ), + ), + FractionallySizedBox( + widthFactor: 0.5, + child: Padding( + padding: const EdgeInsets.only(right: 6), + child: OptionBox( + optionsValue: selectedOption, + itemValue: 72, + onTap: (v) => onUpdateValue(v as int), + label: AppLocalizations.of(context)!.days3, + ), + ), + ), + FractionallySizedBox( + widthFactor: 0.5, + child: Padding( + padding: const EdgeInsets.only(left: 6), + child: OptionBox( + optionsValue: selectedOption, + itemValue: 168, + onTap: (v) => onUpdateValue(v as int), + label: AppLocalizations.of(context)!.days7, + ), + ), + ), + ], + ), + ) + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(24), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.cancel), + ), + const SizedBox(width: 20), + TextButton( + onPressed: selectedOption != null + ? () { + Navigator.pop(context); + onConfirm(); + } + : null, + style: ButtonStyle( + overlayColor: WidgetStateProperty.all( + Theme.of(context).colorScheme.primary.withOpacity(0.1) + ), + foregroundColor: WidgetStateProperty.all( + selectedOption != null + ? Theme.of(context).colorScheme.primary + : Colors.grey, + ), + ), + child: Text(AppLocalizations.of(context)!.confirm), + ), + ], + ), + ), + if (Platform.isIOS) const SizedBox(height: 16) + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/filters/selection/delete_selection_modal.dart b/lib/screens/filters/selection/delete_selection_modal.dart new file mode 100644 index 0000000..ecf77e5 --- /dev/null +++ b/lib/screens/filters/selection/delete_selection_modal.dart @@ -0,0 +1,150 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/models/filtering.dart'; + +class DeleteSelectionModal extends StatefulWidget { + final List selectedWhitelists; + final List selectedBlacklists; + final void Function() onDelete; + + const DeleteSelectionModal({ + super.key, + required this.selectedBlacklists, + required this.selectedWhitelists, + required this.onDelete, + }); + + @override + State createState() => _DeleteSelectionModalState(); +} + +class _DeleteSelectionModalState extends State { + bool _whitelistExpanded = true; + bool _blacklistExpanded = true; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Column( + children: [ + Icon( + Icons.shield_rounded, + size: 24, + color: Theme.of(context).listTileTheme.iconColor + ), + const SizedBox(height: 16), + Text( + AppLocalizations.of(context)!.deleteSelectedLists, + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface + ), + ) + ], + ), + content: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500 + ), + child: SizedBox( + width: double.maxFinite, + child: SingleChildScrollView( + child: Wrap( + children: [ + if (widget.selectedWhitelists.isNotEmpty) ExpansionPanelList( + expandedHeaderPadding: const EdgeInsets.all(0), + elevation: 0, + expansionCallback: (_, isExpanded) => setState(() => _whitelistExpanded = isExpanded), + animationDuration: const Duration(milliseconds: 250), + children: [ + ExpansionPanel( + backgroundColor: Colors.transparent, + headerBuilder: (context, isExpanded) => Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppLocalizations.of(context)!.whitelists, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface + ), + ), + ], + ), + body: ListView.builder( + primary: false, + shrinkWrap: true, + itemCount: widget.selectedWhitelists.length, + itemBuilder: (context, index) => Padding( + padding: const EdgeInsets.all(4), + child: Text("• ${widget.selectedWhitelists[index].name}"), + ), + ), + isExpanded: _whitelistExpanded + ), + ], + ), + if (widget.selectedWhitelists.isNotEmpty && widget.selectedBlacklists.isNotEmpty) const Padding(padding: EdgeInsets.all(8)), + if (widget.selectedBlacklists.isNotEmpty) ExpansionPanelList( + expandedHeaderPadding: const EdgeInsets.all(0), + elevation: 0, + expansionCallback: (_, isExpanded) => setState(() => _blacklistExpanded = isExpanded), + animationDuration: const Duration(milliseconds: 250), + children: [ + ExpansionPanel( + backgroundColor: Colors.transparent, + headerBuilder: (context, isExpanded) => Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppLocalizations.of(context)!.blacklists, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface + ), + ), + ], + ), + body: ListView.builder( + primary: false, + shrinkWrap: true, + itemCount: widget.selectedBlacklists.length, + itemBuilder: (context, index) => Padding( + padding: const EdgeInsets.all(4), + child: Text("• ${widget.selectedBlacklists[index].name}"), + ), + ), + isExpanded: _blacklistExpanded + ), + ], + ) + ], + ), + ), + ), + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.cancel) + ), + const SizedBox(width: 8), + TextButton( + onPressed: widget.onDelete, + child: Text(AppLocalizations.of(context)!.confirm) + ), + ], + ) + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/filters/selection/enable_disable_selection_modal.dart b/lib/screens/filters/selection/enable_disable_selection_modal.dart new file mode 100644 index 0000000..d14fe7c --- /dev/null +++ b/lib/screens/filters/selection/enable_disable_selection_modal.dart @@ -0,0 +1,193 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/models/filtering.dart'; + +class EnableDisableSelectionModal extends StatefulWidget { + final List selectedWhitelists; + final List selectedBlacklists; + final void Function() onDelete; + + const EnableDisableSelectionModal({ + super.key, + required this.selectedBlacklists, + required this.selectedWhitelists, + required this.onDelete, + }); + + @override + State createState() => _EnableDisableSelectionModalState(); +} + +class _EnableDisableSelectionModalState extends State { + bool _whitelistExpanded = true; + bool _blacklistExpanded = true; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Column( + children: [ + Icon( + Icons.remove_moderator_rounded, + size: 24, + color: Theme.of(context).listTileTheme.iconColor + ), + const SizedBox(height: 16), + Text( + AppLocalizations.of(context)!.enableDisableSelected, + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface + ), + ) + ], + ), + content: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500 + ), + child: SizedBox( + width: double.maxFinite, + child: SingleChildScrollView( + child: Wrap( + children: [ + if (widget.selectedWhitelists.isNotEmpty) ExpansionPanelList( + expandedHeaderPadding: const EdgeInsets.all(0), + elevation: 0, + expansionCallback: (_, isExpanded) => setState(() => _whitelistExpanded = isExpanded), + animationDuration: const Duration(milliseconds: 250), + children: [ + ExpansionPanel( + backgroundColor: Colors.transparent, + headerBuilder: (context, isExpanded) => Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppLocalizations.of(context)!.whitelists, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface + ), + ), + ], + ), + body: ListView.builder( + primary: false, + shrinkWrap: true, + itemCount: widget.selectedWhitelists.length, + itemBuilder: (context, index) => Padding( + padding: const EdgeInsets.all(4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + "• ${widget.selectedWhitelists[index].name}", + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 8), + Text( + widget.selectedWhitelists[index].enabled == true + ? AppLocalizations.of(context)!.disable + : AppLocalizations.of(context)!.enable, + style: TextStyle( + color: widget.selectedWhitelists[index].enabled == true + ? Colors.red + : Colors.green, + fontWeight: FontWeight.w500 + ), + ) + ], + ), + ), + ), + isExpanded: _whitelistExpanded + ), + ], + ), + if (widget.selectedWhitelists.isNotEmpty && widget.selectedBlacklists.isNotEmpty) const Padding(padding: EdgeInsets.all(8)), + if (widget.selectedBlacklists.isNotEmpty) ExpansionPanelList( + expandedHeaderPadding: const EdgeInsets.all(0), + elevation: 0, + expansionCallback: (_, isExpanded) => setState(() => _blacklistExpanded = isExpanded), + animationDuration: const Duration(milliseconds: 250), + children: [ + ExpansionPanel( + backgroundColor: Colors.transparent, + headerBuilder: (context, isExpanded) => Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppLocalizations.of(context)!.blacklists, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface + ), + ), + ], + ), + body: ListView.builder( + primary: false, + shrinkWrap: true, + itemCount: widget.selectedBlacklists.length, + itemBuilder: (context, index) => Padding( + padding: const EdgeInsets.all(4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + "• ${widget.selectedBlacklists[index].name}", + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 8), + Text( + widget.selectedBlacklists[index].enabled == true + ? AppLocalizations.of(context)!.disable + : AppLocalizations.of(context)!.enable, + style: TextStyle( + color: widget.selectedBlacklists[index].enabled == true + ? Colors.red + : Colors.green, + fontWeight: FontWeight.w500 + ), + ) + ], + ), + ), + ), + isExpanded: _blacklistExpanded + ), + ], + ) + ], + ), + ), + ), + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.cancel) + ), + const SizedBox(width: 8), + TextButton( + onPressed: widget.onDelete, + child: Text(AppLocalizations.of(context)!.confirm) + ), + ], + ) + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/filters/selection/selection_lists.dart b/lib/screens/filters/selection/selection_lists.dart new file mode 100644 index 0000000..a7988e9 --- /dev/null +++ b/lib/screens/filters/selection/selection_lists.dart @@ -0,0 +1,311 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/models/filtering.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; + +class SelectionList extends StatelessWidget { + final List lists; + final List selectedLists; + final void Function(Filter) onSelect; + final void Function() selectAll; + final void Function() unselectAll; + + const SelectionList({ + super.key, + required this.lists, + required this.selectedLists, + required this.onSelect, + required this.selectAll, + required this.unselectAll, + }); + + @override + Widget build(BuildContext context) { + return ListView.builder( + itemCount: lists.length+1, + itemBuilder: (context, index) { + if (index == 0) { + return Card( + margin: const EdgeInsets.all(16), + child: CheckboxListTile( + title: Text( + AppLocalizations.of(context)!.selectAll, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface + ), + ), + value: lists.length == selectedLists.length, + onChanged: (value) { + if (value == true) { + selectAll(); + } + else { + unselectAll(); + } + }, + checkboxShape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4) + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12) + ), + ), + ); + } + return _CheckboxTile( + list: lists[index-1], + onSelect: onSelect, + isSelected: selectedLists.contains(lists[index-1]), + ); + } + ); + } +} + +class SelectionSliverList extends StatelessWidget { + final List lists; + final List selectedLists; + final void Function(Filter) onSelect; + final void Function() selectAll; + final void Function() unselectAll; + + const SelectionSliverList({ + super.key, + required this.lists, + required this.selectedLists, + required this.onSelect, + required this.selectAll, + required this.unselectAll, + }); + + @override + Widget build(BuildContext context) { + return SafeArea( + top: false, + child: Builder( + builder: (BuildContext context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + ), + if (lists.isNotEmpty) SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index == 0) { + return Card( + margin: const EdgeInsets.all(16), + child: CheckboxListTile( + title: Text(AppLocalizations.of(context)!.selectAll), + value: lists.length == selectedLists.length, + onChanged: (value) { + if (value == true) { + selectAll(); + } + else { + unselectAll(); + } + }, + checkboxShape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4) + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12) + ), + ), + ); + } + return _Tile( + list: lists[index-1], + onSelect: onSelect, + isSelected: selectedLists.contains(lists[index-1]), + ); + }, + childCount: lists.length+1 + ), + ), + if (lists.isEmpty) SliverFillRemaining( + child: Center( + child: Text( + AppLocalizations.of(context)!.noItems, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ) + ) + ) + ], + ); + }, + ), + ); + } +} + +class _Tile extends StatelessWidget { + final Filter list; + final void Function(Filter) onSelect; + final bool isSelected; + + const _Tile({ + required this.list, + required this.onSelect, + required this.isSelected, + }); + + @override + Widget build(BuildContext context) { + final appConfigProvider = Provider.of(context); + + return ListTile( + title: Text( + list.name, + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + list.url, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Icon( + list.enabled == true + ? Icons.check_rounded + : Icons.close_rounded, + size: 16, + color: list.enabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red + ), + const SizedBox(width: 4), + Text( + list.enabled == true + ? AppLocalizations.of(context)!.enabled + : AppLocalizations.of(context)!.disabled, + style: TextStyle( + fontSize: 12, + color: list.enabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red + ), + ) + ], + ) + ], + ), + isThreeLine: true, + tileColor: isSelected == true + ? Theme.of(context).colorScheme.primaryContainer + : null, + contentPadding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16 + ), + selected: isSelected, + selectedTileColor: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.5), + selectedColor: Theme.of(context).colorScheme.onSurface, + onTap: () => onSelect(list), + ); + } +} + +class _CheckboxTile extends StatelessWidget { + final Filter list; + final void Function(Filter) onSelect; + final bool isSelected; + + const _CheckboxTile({ + required this.list, + required this.onSelect, + required this.isSelected, + }); + + @override + Widget build(BuildContext context) { + final appConfigProvider = Provider.of(context); + + return ListTile( + leading: Checkbox( + value: isSelected, + onChanged: (_) => onSelect(list), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4) + ), + ), + title: Text( + list.name, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface + ), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + list.url, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Icon( + list.enabled == true + ? Icons.check_rounded + : Icons.close_rounded, + size: 16, + color: list.enabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red + ), + const SizedBox(width: 4), + Text( + list.enabled == true + ? AppLocalizations.of(context)!.enabled + : AppLocalizations.of(context)!.disabled, + style: TextStyle( + fontSize: 12, + color: list.enabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red + ), + ) + ], + ) + ], + ), + isThreeLine: true, + contentPadding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16 + ), + onTap: () => onSelect(list), + ); + } +} + diff --git a/lib/screens/filters/selection/selection_result_modal.dart b/lib/screens/filters/selection/selection_result_modal.dart new file mode 100644 index 0000000..02508ca --- /dev/null +++ b/lib/screens/filters/selection/selection_result_modal.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/models/filtering.dart'; + +enum SelectionResultMode { delete, enableDisable } + +class SelectionResultModal extends StatelessWidget { + final List results; + final void Function() onClose; + final SelectionResultMode mode; + + const SelectionResultModal({ + super.key, + required this.results, + required this.onClose, + required this.mode, + }); + + @override + Widget build(BuildContext context) { + final failedItems = results.where((r) => r.successful == false).toList(); + + return AlertDialog( + title: Column( + children: [ + Icon( + mode == SelectionResultMode.delete + ? Icons.delete_rounded + : Icons.remove_moderator_rounded, + size: 24, + color: Theme.of(context).listTileTheme.iconColor + ), + const SizedBox(height: 16), + Text( + mode == SelectionResultMode.delete + ? AppLocalizations.of(context)!.deletionResult + : AppLocalizations.of(context)!.enableDisableResult, + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface + ), + ) + ], + ), + content: failedItems.isEmpty + ? Text( + mode == SelectionResultMode.delete + ? AppLocalizations.of(context)!.allSelectedListsDeletedSuccessfully + : AppLocalizations.of(context)!.selectedListsEnabledDisabledSuccessfully, + ) + : SizedBox( + width: double.maxFinite, + child: SingleChildScrollView( + child: Wrap( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 16, top: 8), + child: Text( + AppLocalizations.of(context)!.failedElements, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500 + ), + ), + ), + ], + ), + ListView.builder( + primary: false, + shrinkWrap: true, + itemCount: failedItems.length, + itemBuilder: (context, index) => Padding( + padding: const EdgeInsets.all(4), + child: Text("• ${failedItems[index].list.name}"), + ), + ), + ], + ), + ), + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () { + Navigator.pop(context); + onClose(); + }, + child: Text(AppLocalizations.of(context)!.close) + ), + ], + ) + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/filters/selection/selection_screen.dart b/lib/screens/filters/selection/selection_screen.dart new file mode 100644 index 0000000..5907012 --- /dev/null +++ b/lib/screens/filters/selection/selection_screen.dart @@ -0,0 +1,376 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/screens/filters/selection/enable_disable_selection_modal.dart'; +import 'package:adguard_home_manager/screens/filters/selection/delete_selection_modal.dart'; +import 'package:adguard_home_manager/screens/filters/selection/selection_result_modal.dart'; +import 'package:adguard_home_manager/screens/filters/selection/selection_lists.dart'; + +import 'package:adguard_home_manager/classes/process_modal.dart'; +import 'package:adguard_home_manager/models/filtering.dart'; +import 'package:adguard_home_manager/providers/filtering_provider.dart'; + +enum ListType { blacklist, whitelist } + +class SelectionScreen extends StatefulWidget { + final bool isModal; + + const SelectionScreen({ + super.key, + required this.isModal + }); + + @override + State createState() => _SelectionScreenState(); +} + +class _SelectionScreenState extends State with TickerProviderStateMixin { + late TabController _tabController; + + List _selectedWhitelists = []; + List _selectedBlacklists = []; + + @override + void initState() { + super.initState(); + _tabController = TabController( + initialIndex: 0, + length: 2, + vsync: this, + ); + } + + void handleSelect(Filter list, ListType type) { + if (type == ListType.blacklist) { + final isContained = _selectedBlacklists.contains(list); + if (isContained) { + setState(() => _selectedBlacklists = _selectedBlacklists.where((l) => l != list).toList()); + } + else { + setState(() => _selectedBlacklists.add(list)); + } + } + else if (type == ListType.whitelist) { + final isContained = _selectedWhitelists.contains(list); + if (isContained) { + setState(() => _selectedWhitelists = _selectedWhitelists.where((l) => l != list).toList()); + } + else { + setState(() => _selectedWhitelists.add(list)); + } + } + } + + @override + Widget build(BuildContext context) { + final filteringProvider = Provider.of(context); + + final somethingSelected = _selectedBlacklists.isNotEmpty || _selectedWhitelists.isNotEmpty; + + void enableDisableSelected() { + showDialog( + context: context, + builder: (ctx) => EnableDisableSelectionModal( + selectedWhitelists: _selectedWhitelists, + selectedBlacklists: _selectedBlacklists, + onDelete: () async { + Navigator.pop(context); + final processModal = ProcessModal(); + processModal.open(AppLocalizations.of(context)!.processingLists); + final result = await filteringProvider.enableDisableMultipleLists( + blacklists: _selectedBlacklists, + whitelists: _selectedWhitelists + ); + processModal.close(); + if (!context.mounted) return; + showDialog( + context: context, + builder: (ctx) => SelectionResultModal( + mode: SelectionResultMode.enableDisable, + results: result, + onClose: () => Navigator.pop(context), + ), + barrierDismissible: false + ); + }, + ), + barrierDismissible: false + ); + } + + void deleteSelected() { + showDialog( + context: context, + builder: (ctx) => DeleteSelectionModal( + selectedWhitelists: _selectedWhitelists, + selectedBlacklists: _selectedBlacklists, + onDelete: () async { + Navigator.pop(context); + final processModal = ProcessModal(); + processModal.open(AppLocalizations.of(context)!.deletingLists); + final result = await filteringProvider.deleteMultipleLists( + blacklists: _selectedBlacklists, + whitelists: _selectedWhitelists + ); + processModal.close(); + if (!context.mounted) return; + showDialog( + context: context, + builder: (ctx) => SelectionResultModal( + mode: SelectionResultMode.delete, + results: result, + onClose: () => Navigator.pop(context), + ), + barrierDismissible: false + ); + }, + ), + barrierDismissible: false + ); + } + + if (widget.isModal == true) { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500 + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.clear_rounded), + tooltip: AppLocalizations.of(context)!.close, + ), + const SizedBox(width: 8), + Text( + AppLocalizations.of(context)!.selectionMode, + style: const TextStyle( + fontSize: 22 + ), + ) + ], + ), + Row( + children: [ + IconButton( + onPressed: somethingSelected == true + ? () => enableDisableSelected() + : null, + icon: const Icon(Icons.remove_moderator_rounded), + tooltip: AppLocalizations.of(context)!.enableDisableSelected, + ), + const SizedBox(width: 8), + IconButton( + onPressed: somethingSelected == true + ? () => deleteSelected() + : null, + icon: const Icon(Icons.delete_rounded), + tooltip: AppLocalizations.of(context)!.deleteSelectedLists, + ), + ], + ) + ], + ), + ), + Expanded( + child: DefaultTabController( + length: 2, + child: Column( + children: [ + TabBar( + controller: _tabController, + unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant, + tabs: [ + _Tab( + icon: Icons.verified_user_rounded, + text: AppLocalizations.of(context)!.whitelists, + quantity: _selectedWhitelists.length + ), + _Tab( + icon: Icons.gpp_bad_rounded, + text: AppLocalizations.of(context)!.blacklists, + quantity: _selectedBlacklists.length + ), + ] + ), + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + SelectionList( + lists: filteringProvider.filtering!.whitelistFilters, + selectedLists: _selectedWhitelists, + onSelect: (list) => handleSelect(list, ListType.whitelist), + selectAll: () => setState(() => _selectedWhitelists = filteringProvider.filtering!.whitelistFilters), + unselectAll: () => setState(() => _selectedWhitelists = []) + ), + SelectionList( + lists: filteringProvider.filtering!.filters, + selectedLists: _selectedBlacklists, + onSelect: (list) => handleSelect(list, ListType.blacklist), + selectAll: () => setState(() => _selectedBlacklists = filteringProvider.filtering!.filters), + unselectAll: () => setState(() => _selectedBlacklists = []) + ), + ] + ), + ) + ], + ) + ), + ) + ], + ), + ), + ); + } + else { + return Dialog.fullscreen( + child: Stack( + children: [ + DefaultTabController( + length: 2, + child: NestedScrollView( + headerSliverBuilder: ((context, innerBoxIsScrolled) { + return [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverAppBar( + leading: CloseButton( + onPressed: () => Navigator.pop(context), + ), + title: Text(AppLocalizations.of(context)!.selectionMode), + pinned: true, + floating: true, + forceElevated: innerBoxIsScrolled, + centerTitle: false, + bottom: TabBar( + controller: _tabController, + unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant, + tabs: [ + _Tab( + icon: Icons.verified_user_rounded, + text: AppLocalizations.of(context)!.whitelists, + quantity: _selectedWhitelists.length + ), + _Tab( + icon: Icons.gpp_bad_rounded, + text: AppLocalizations.of(context)!.blacklists, + quantity: _selectedBlacklists.length + ), + ] + ), + actions: [ + IconButton( + onPressed: somethingSelected == true + ? () => enableDisableSelected() + : null, + icon: const Icon(Icons.remove_moderator_rounded), + tooltip: AppLocalizations.of(context)!.enableDisableSelected, + ), + IconButton( + onPressed: somethingSelected == true + ? () => deleteSelected() + : null, + icon: const Icon(Icons.delete_rounded), + tooltip: AppLocalizations.of(context)!.deleteSelectedLists, + ), + const SizedBox(width: 8), + ], + ), + ) + ]; + }), + body: TabBarView( + controller: _tabController, + children: [ + SelectionSliverList( + lists: filteringProvider.filtering!.whitelistFilters, + selectedLists: _selectedWhitelists, + onSelect: (list) => handleSelect(list, ListType.whitelist), + selectAll: () => setState(() => _selectedWhitelists = filteringProvider.filtering!.whitelistFilters), + unselectAll: () => setState(() => _selectedWhitelists = []), + ), + SelectionSliverList( + lists: filteringProvider.filtering!.filters, + selectedLists: _selectedBlacklists, + onSelect: (list) => handleSelect(list, ListType.blacklist), + selectAll: () => setState(() => _selectedBlacklists = filteringProvider.filtering!.filters), + unselectAll: () => setState(() => _selectedBlacklists = []), + ), + ] + ) + ) + ), + ], + ), + ); + } + } +} + +class _Tab extends StatelessWidget { + final IconData icon; + final String text; + final int quantity; + + const _Tab({ + required this.icon, + required this.text, + required this.quantity + }); + + @override + Widget build(BuildContext context) { + return Tab( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon), + const SizedBox(width: 8), + Flexible( + child: Text( + text, + overflow: TextOverflow.ellipsis, + ) + ), + const SizedBox(width: 8), + Container( + height: 22, + padding: const EdgeInsets.symmetric( + horizontal: 2 + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + color: Theme.of(context).colorScheme.primaryContainer + ), + child: ConstrainedBox( + constraints: const BoxConstraints( + minWidth: 18 + ), + child: Center( + child: Text( + quantity.toString(), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: Theme.of(context).colorScheme.onPrimaryContainer + ), + ), + ), + ), + ) + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/filters/update_interval_lists_modal.dart b/lib/screens/filters/update_interval_lists_modal.dart deleted file mode 100644 index c471c96..0000000 --- a/lib/screens/filters/update_interval_lists_modal.dart +++ /dev/null @@ -1,307 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -import 'package:adguard_home_manager/widgets/option_box.dart'; - - -class UpdateIntervalListsModal extends StatefulWidget { - final int interval; - final void Function(int) onChange; - - const UpdateIntervalListsModal({ - Key? key, - required this.interval, - required this.onChange, - }) : super(key: key); - - @override - State createState() => _UpdateIntervalListsModalState(); -} - -class _UpdateIntervalListsModalState extends State { - int? selectedOption; - - void _updateRadioValue(value) { - setState(() { - selectedOption = value; - }); - } - - @override - void initState() { - selectedOption = widget.interval; - super.initState(); - } - - @override - Widget build(BuildContext context) { - final MediaQueryData mediaQueryData = MediaQuery.of(context); - - return Padding( - padding: mediaQueryData.viewInsets, - child: Container( - height: 390, - decoration: BoxDecoration( - color: Theme.of(context).dialogBackgroundColor, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28) - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: ListView( - physics: 410 < MediaQuery.of(context).size.height - ? const NeverScrollableScrollPhysics() - : null, - children: [ - Padding( - padding: const EdgeInsets.only(top: 24), - child: Icon( - Icons.update_rounded, - size: 24, - color: Theme.of(context).listTileTheme.iconColor - ), - ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 16 - ), - width: double.maxFinite, - child: Text( - AppLocalizations.of(context)!.updateFrequency, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSurface - ), - ), - ), - SizedBox( - width: double.maxFinite, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Container( - width: (mediaQueryData.size.width-70)/2, - margin: const EdgeInsets.only( - top: 10, - right: 5, - bottom: 5 - ), - child: OptionBox( - optionsValue: selectedOption, - itemValue: 0, - onTap: _updateRadioValue, - child: Center( - child: AnimatedDefaultTextStyle( - duration: const Duration(milliseconds: 250), - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - color: selectedOption == 0 - ? Theme.of(context).colorScheme.onInverseSurface - : Theme.of(context).colorScheme.onSurface - ), - child: Text(AppLocalizations.of(context)!.never), - ), - ), - ), - ), - Container( - width: (mediaQueryData.size.width-70)/2, - margin: const EdgeInsets.only( - top: 10, - left: 5, - bottom: 5 - ), - child: OptionBox( - optionsValue: selectedOption, - itemValue: 1, - onTap: _updateRadioValue, - child: Center( - child: AnimatedDefaultTextStyle( - duration: const Duration(milliseconds: 250), - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - color: selectedOption == 1 - ? Theme.of(context).colorScheme.onInverseSurface - : Theme.of(context).colorScheme.onSurface - ), - child: Text(AppLocalizations.of(context)!.hour1), - ), - ), - ), - ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Container( - width: (mediaQueryData.size.width-70)/2, - margin: const EdgeInsets.only( - top: 5, - right: 5, - bottom: 5 - ), - child: OptionBox( - optionsValue: selectedOption, - itemValue: 12, - onTap: _updateRadioValue, - child: Center( - child: AnimatedDefaultTextStyle( - duration: const Duration(milliseconds: 250), - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - color: selectedOption == 12 - ? Theme.of(context).colorScheme.onInverseSurface - : Theme.of(context).colorScheme.onSurface - ), - child: Text(AppLocalizations.of(context)!.hours12), - ), - ), - ), - ), - Container( - width: (mediaQueryData.size.width-70)/2, - margin: const EdgeInsets.only( - top: 5, - left: 5, - bottom: 5 - ), - child: OptionBox( - optionsValue: selectedOption, - itemValue: 24, - onTap: _updateRadioValue, - child: Center( - child: AnimatedDefaultTextStyle( - duration: const Duration(milliseconds: 250), - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - color: selectedOption == 24 - ? Theme.of(context).colorScheme.onInverseSurface - : Theme.of(context).colorScheme.onSurface - ), - child: Text(AppLocalizations.of(context)!.hours24), - ), - ), - ), - ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Container( - width: (mediaQueryData.size.width-70)/2, - margin: const EdgeInsets.only( - top: 5, - right: 5, - bottom: 10 - ), - child: OptionBox( - optionsValue: selectedOption, - itemValue: 72, - onTap: _updateRadioValue, - child: Center( - child: AnimatedDefaultTextStyle( - duration: const Duration(milliseconds: 250), - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - color: selectedOption == 72 - ? Theme.of(context).colorScheme.onInverseSurface - : Theme.of(context).colorScheme.onSurface - ), - child: Text(AppLocalizations.of(context)!.days3), - ), - ), - ), - ), - Container( - width: (mediaQueryData.size.width-70)/2, - margin: const EdgeInsets.only( - top: 5, - left: 5, - bottom: 10 - ), - child: OptionBox( - optionsValue: selectedOption, - itemValue: 168, - onTap: _updateRadioValue, - child: Center( - child: AnimatedDefaultTextStyle( - duration: const Duration(milliseconds: 250), - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - color: selectedOption == 168 - ? Theme.of(context).colorScheme.onInverseSurface - : Theme.of(context).colorScheme.onSurface - ), - child: Text(AppLocalizations.of(context)!.days7), - ), - ), - ), - ), - ], - ), - ], - ), - ), - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.all(24), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(AppLocalizations.of(context)!.cancel), - ), - const SizedBox(width: 20), - TextButton( - onPressed: selectedOption != null - ? () { - Navigator.pop(context); - widget.onChange(selectedOption!); - } - : null, - style: ButtonStyle( - overlayColor: MaterialStateProperty.all( - Theme.of(context).colorScheme.primary.withOpacity(0.1) - ), - foregroundColor: MaterialStateProperty.all( - selectedOption != null - ? Theme.of(context).colorScheme.primary - : Colors.grey, - ), - ), - child: Text(AppLocalizations.of(context)!.confirm), - ), - ], - ), - ) - ], - ), - ), - ); - } -} \ No newline at end of file diff --git a/lib/screens/home/appbar.dart b/lib/screens/home/appbar.dart index dc63aff..d4b447c 100644 --- a/lib/screens/home/appbar.dart +++ b/lib/screens/home/appbar.dart @@ -1,135 +1,160 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_web_browser/flutter_web_browser.dart'; import 'package:adguard_home_manager/screens/servers/servers.dart'; +import 'package:adguard_home_manager/functions/desktop_mode.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/providers/status_provider.dart'; +import 'package:adguard_home_manager/functions/open_url.dart'; import 'package:adguard_home_manager/models/server.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; -class HomeAppBar extends StatelessWidget with PreferredSizeWidget { - const HomeAppBar({Key? key}) : super(key: key); +class HomeAppBar extends StatelessWidget { + final bool innerBoxScrolled; + + const HomeAppBar({ + super.key, + required this.innerBoxScrolled + }); @override - PreferredSizeWidget build(BuildContext context) { + Widget build(BuildContext context) { final serversProvider = Provider.of(context); + final statusProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + final Server? server = serversProvider.selectedServer; void navigateServers() { Future.delayed(const Duration(milliseconds: 0), (() { + if (!context.mounted) return; Navigator.of(context).push( MaterialPageRoute(builder: (context) => const Servers()) ); })); } - void openWebAdminPanel() { - FlutterWebBrowser.openWebPage( - url: "${server!.connectionMethod}://${server.domain}${server.path ?? ""}${server.port != null ? ':${server.port}' : ""}", - customTabsOptions: const CustomTabsOptions( - instantAppsEnabled: true, - showTitle: true, - urlBarHidingEnabled: false, - ), - safariVCOptions: const SafariViewControllerOptions( - barCollapsingEnabled: true, - dismissButtonStyle: SafariViewControllerDismissButtonStyle.close, - modalPresentationCapturesStatusBarAppearance: true, - ) - ); - } - - return AppBar( - toolbarHeight: 70, - title: Padding( - padding: const EdgeInsets.only(bottom: 5), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Row( + return SliverAppBar.large( + pinned: true, + floating: true, + centerTitle: false, + forceElevated: innerBoxScrolled, + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, + leading: Stack( + children: [ + Center( + child: Icon( + serversProvider.selectedServer != null && statusProvider.serverStatus != null + ? statusProvider.serverStatus!.generalEnabled == true + ? Icons.gpp_good_rounded + : Icons.gpp_bad_rounded + : Icons.shield, + size: 30, + color: serversProvider.selectedServer != null && statusProvider.serverStatus != null + ? statusProvider.serverStatus!.generalEnabled == true + ? appConfigProvider.useThemeColorForStatus + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.onSurface.withOpacity(0.38) + : Colors.red + : Theme.of(context).colorScheme.onSurface.withOpacity(0.38) + ), + ), + if (statusProvider.remainingTime > 0) Positioned( + bottom: 15, + right: 15, + child: Stack( children: [ - Icon( - serversProvider.selectedServer != null && serversProvider.serverStatus.data != null - ? serversProvider.serverStatus.data!.generalEnabled == true - ? Icons.gpp_good_rounded - : Icons.gpp_bad_rounded - : Icons.shield, - size: 30, - color: serversProvider.selectedServer != null && serversProvider.serverStatus.data != null - ? serversProvider.serverStatus.data!.generalEnabled == true - ? appConfigProvider.useThemeColorForStatus - ? Theme.of(context).colorScheme.primary - : Colors.green - : appConfigProvider.useThemeColorForStatus == true - ? Theme.of(context).colorScheme.onSurface.withOpacity(0.38) - : Colors.red - : Theme.of(context).colorScheme.onSurface.withOpacity(0.38) - ), - const SizedBox(width: 20), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (serversProvider.selectedServer != null) ...[ - Text( - server!.name, - style: const TextStyle( - fontSize: 20 - ), - ), - const SizedBox(height: 5), - Text( - "${server.connectionMethod}://${server.domain}${server.path ?? ""}${server.port != null ? ':${server.port}' : ""}", - style: TextStyle( - fontSize: 14, - color: Theme.of(context).listTileTheme.textColor - ), - ) - ], - if (serversProvider.selectedServer == null) Text( - AppLocalizations.of(context)!.noServerSelected, - style: const TextStyle( - fontSize: 20 - ), - ), - ], + Container( + padding: const EdgeInsets.all(1), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + color: Theme.of(context).colorScheme.surface + ), + child: Icon( + Icons.timer_rounded, + size: 12, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), ], ), - PopupMenuButton( - itemBuilder: (context) => [ - PopupMenuItem( - onTap: navigateServers, - child: Row( - children: [ - const Icon(Icons.storage_rounded), - const SizedBox(width: 10), - Text(AppLocalizations.of(context)!.servers) - ], - ), - ), - if (serversProvider.selectedServer != null && serversProvider.serverStatus.loadStatus == 1) PopupMenuItem( - onTap: openWebAdminPanel, - child: Row( - children: [ - const Icon(Icons.web_rounded), - const SizedBox(width: 10), - Text(AppLocalizations.of(context)!.webAdminPanel) - ], - ), - ) - ] - ) - ], - ), + ) + ], ), + title: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (serversProvider.selectedServer != null) ...[ + Text( + server!.name, + style: !appConfigProvider.hideServerAddress ? const TextStyle( + fontSize: 20 + ) : null, + ), + if (!appConfigProvider.hideServerAddress) ...[ + const SizedBox(height: 5), + Text( + "${server.connectionMethod}://${server.domain}${server.path ?? ""}${server.port != null ? ':${server.port}' : ""}", + style: TextStyle( + fontSize: 14, + color: Theme.of(context).listTileTheme.textColor + ), + ) + ] + ], + if (serversProvider.selectedServer == null) Text( + AppLocalizations.of(context)!.noServerSelected, + style: const TextStyle( + fontSize: 20 + ), + ), + ], + ), + actions: [ + if (!(Platform.isAndroid || Platform.isIOS)) ...[ + IconButton( + onPressed: () => statusProvider.getServerStatus(), + icon: const Icon(Icons.refresh_rounded), + tooltip: AppLocalizations.of(context)!.refresh, + ), + const SizedBox(width: 8), + ], + PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + onTap: navigateServers, + child: Row( + children: [ + const Icon(Icons.storage_rounded), + const SizedBox(width: 10), + Text(AppLocalizations.of(context)!.servers) + ], + ), + ), + if (serversProvider.selectedServer != null && statusProvider.loadStatus == LoadStatus.loaded) PopupMenuItem( + onTap: () => openUrl("${server!.connectionMethod}://${server.domain}${server.path ?? ""}${server.port != null ? ':${server.port}' : ""}"), + child: Row( + children: [ + const Icon(Icons.web_rounded), + const SizedBox(width: 10), + Text(AppLocalizations.of(context)!.webAdminPanel) + ], + ), + ) + ] + ), + const SizedBox(width: 8), + ], ); } - - @override - Size get preferredSize => const Size.fromHeight(70); } \ No newline at end of file diff --git a/lib/screens/home/chart.dart b/lib/screens/home/chart.dart index 315d421..ac1fce5 100644 --- a/lib/screens/home/chart.dart +++ b/lib/screens/home/chart.dart @@ -1,43 +1,58 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/widgets/line_chart.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; -class HomeChart extends StatelessWidget { +class HomeChart extends StatefulWidget { final List data; final String label; final String primaryValue; final String secondaryValue; final Color color; + final int hoursInterval; + final void Function() onTapTitle; + final bool isDesktop; const HomeChart({ - Key? key, + super.key, required this.data, required this.label, required this.primaryValue, required this.secondaryValue, - required this.color - }) : super(key: key); + required this.color, + required this.hoursInterval, + required this.onTapTitle, + required this.isDesktop, + }); + + @override + State createState() => _HomeChartState(); +} + +class _HomeChartState extends State { + bool _isHover = false; @override Widget build(BuildContext context) { final appConfigProvider = Provider.of(context); - bool isEmpty = true; - for (int item in data) { - if (item > 0) { - isEmpty = false; - break; - } - } + final bool isEmpty = widget.data.every((i) => i == 0); if (!(appConfigProvider.hideZeroValues == true && isEmpty == true)) { + List dateTimes = []; + DateTime currentDate = DateTime.now().subtract(Duration(hours: widget.hoursInterval*widget.data.length)); + for (var i = 0; i < widget.data.length; i++) { + currentDate = currentDate.add(Duration(hours: widget.hoursInterval)); + dateTimes.add(currentDate); + } + return Column( children: [ Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), + padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( children: [ Padding( @@ -46,57 +61,87 @@ class HomeChart extends StatelessWidget { ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: isEmpty + ? CrossAxisAlignment.center + : CrossAxisAlignment.start, children: [ - Text( - label, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.onSurface + Flexible( + child: MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (_) => setState(() => _isHover = true), + onExit: (_) => setState(() => _isHover = false), + child: GestureDetector( + onTapDown: (_) => setState(() => _isHover = true), + onTapUp: (_) => setState(() => _isHover = false), + onTap: !isEmpty ? () => widget.onTapTitle () : null, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Text( + widget.label, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + color: _isHover && !isEmpty + ? Theme.of(context).colorScheme.onSurfaceVariant + : Theme.of(context).colorScheme.onSurface, + ), + ), + ), + if (!isEmpty) ...[ + const SizedBox(width: 4), + Icon( + Icons.chevron_right_rounded, + size: 20, + color: _isHover && !isEmpty + ? Theme.of(context).colorScheme.onSurfaceVariant + : Theme.of(context).colorScheme.onSurface, + ) + ], + ], + ), + ), ), ), - !isEmpty - ? Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - primaryValue, - style: TextStyle( - color: color, - fontSize: 18, - fontWeight: FontWeight.w500 - ), - ), - Text( - secondaryValue, - style: TextStyle( - fontSize: 12, - color: color - ), - ) - ], + if (!isEmpty) Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + widget.primaryValue, + style: TextStyle( + color: widget.color, + fontSize: 18, + fontWeight: FontWeight.w500 + ), + ), + Text( + widget.secondaryValue, + style: TextStyle( + fontSize: 12, + color: widget.color + ), ) - : Row( - children: [ - Text( - primaryValue, - style: TextStyle( - color: color, - fontSize: 18, - fontWeight: FontWeight.w500 - ), - ), - const SizedBox(width: 10), - Text( - "($secondaryValue)", - style: TextStyle( - fontSize: 12, - color: color - ), - ) - ], + ], + ), + if (isEmpty && !widget.isDesktop) Column( + children: [ + Icon( + Icons.show_chart_rounded, + color: Theme.of(context).colorScheme.onSurfaceVariant, + size: 16, + ), + Text( + AppLocalizations.of(context)!.noData, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ) + ], + ) ], ), ), @@ -104,21 +149,46 @@ class HomeChart extends StatelessWidget { width: double.maxFinite, height: 150, child: CustomLineChart( - data: data, - color: color, + data: widget.data, + color: widget.color, + dates: dateTimes, + daysInterval: widget.hoursInterval == 24, + context: context, ) ), + if (isEmpty && widget.isDesktop) SizedBox( + height: 150, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.show_chart_rounded, + color: Theme.of(context).colorScheme.onSurfaceVariant, + size: 30, + ), + const SizedBox(height: 20), + Text( + AppLocalizations.of(context)!.noDataChart, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ) + ], + ), + ) ], ), ), Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), + padding: const EdgeInsets.symmetric(horizontal: 16), child: Divider( thickness: 1, color: Theme.of(context).colorScheme.onSurface.withOpacity(0.2), ), ), - const SizedBox(height: 20), + const SizedBox(height: 16), ], ); } diff --git a/lib/screens/home/combined_chart.dart b/lib/screens/home/combined_chart.dart new file mode 100644 index 0000000..f951cea --- /dev/null +++ b/lib/screens/home/combined_chart.dart @@ -0,0 +1,315 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/widgets/combined_line_chart.dart'; + +import 'package:adguard_home_manager/functions/number_format.dart'; +import 'package:adguard_home_manager/providers/status_provider.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; + +class CombinedChartData { + final CombinedChartItem totalQueries; + final CombinedChartItem? blockedFilters; + final CombinedChartItem? replacedSafeBrowsing; + final CombinedChartItem? replacedParental; + + const CombinedChartData({ + required this.totalQueries, + this.blockedFilters, + this.replacedSafeBrowsing, + this.replacedParental, + }); +} + +class CombinedChartItem { + final String label; + final Color color; + final List data; + + const CombinedChartItem({ + required this.label, + required this.color, + required this.data + }); +} + +class CombinedHomeChart extends StatelessWidget { + const CombinedHomeChart({super.key}); + + List? removeZero(List list) { + final removed = list.where((i) => i > 0); + if (removed.isNotEmpty) { + return list; + } + else { + return null; + } + } + + @override + Widget build(BuildContext context) { + final statusProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + + final width = MediaQuery.of(context).size.width; + + if (statusProvider.serverStatus != null) { + final data = CombinedChartData( + totalQueries: CombinedChartItem( + label: AppLocalizations.of(context)!.dnsQueries, + color: Colors.blue, + data: statusProvider.serverStatus!.stats.dnsQueries + ), + blockedFilters: appConfigProvider.hideZeroValues == true + ? removeZero(statusProvider.serverStatus!.stats.blockedFiltering) != null + ? CombinedChartItem( + label: AppLocalizations.of(context)!.blockedFilters, + color: Colors.red, + data: statusProvider.serverStatus!.stats.blockedFiltering + ) + : null + : CombinedChartItem( + label: AppLocalizations.of(context)!.blockedFilters, + color: Colors.red, + data: statusProvider.serverStatus!.stats.blockedFiltering + ) , + replacedSafeBrowsing: appConfigProvider.hideZeroValues == true + ? removeZero(statusProvider.serverStatus!.stats.replacedSafebrowsing) != null + ? CombinedChartItem( + label: AppLocalizations.of(context)!.malwarePhishingBlocked, + color: Colors.green, + data: statusProvider.serverStatus!.stats.replacedSafebrowsing + ) + : null + : CombinedChartItem( + label: AppLocalizations.of(context)!.malwarePhishingBlocked, + color: Colors.green, + data: statusProvider.serverStatus!.stats.replacedSafebrowsing + ) , + replacedParental: appConfigProvider.hideZeroValues == true + ? removeZero(statusProvider.serverStatus!.stats.replacedParental) != null + ? CombinedChartItem( + label: AppLocalizations.of(context)!.blockedAdultWebsites, + color: Colors.orange, + data: statusProvider.serverStatus!.stats.replacedParental + ) + : null + : CombinedChartItem( + label: AppLocalizations.of(context)!.blockedAdultWebsites, + color: Colors.orange, + data: statusProvider.serverStatus!.stats.replacedParental + ) , + ); + + final hoursInterval = statusProvider.serverStatus!.stats.timeUnits == "days" ? 24 : 1; + + List dateTimes = []; + DateTime currentDate = DateTime.now().subtract(Duration(hours: hoursInterval*statusProvider.serverStatus!.stats.dnsQueries.length+1)); + for (var i = 0; i < statusProvider.serverStatus!.stats.dnsQueries.length; i++) { + currentDate = currentDate.add(Duration(hours: hoursInterval)); + dateTimes.add(currentDate); + } + + if (width > 700) { + return Column( + children: [ + Text( + AppLocalizations.of(context)!.statistics, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface + ), + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + flex: 3, + child: SizedBox( + height: 300, + width: double.maxFinite, + child: CustomCombinedLineChart( + inputData: data, + context: context, + dates: dateTimes, + daysInterval: hoursInterval == 24, + ), + ), + ), + const SizedBox(width: 16), + Expanded( + flex: 2, + child: SizedBox( + height: 300, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _Legend( + label: data.totalQueries.label, + color: data.totalQueries.color, + primaryValue: intFormat(statusProvider.serverStatus!.stats.numDnsQueries, Platform.localeName), + secondaryValue: "${doubleFormat(statusProvider.serverStatus!.stats.avgProcessingTime*1000, Platform.localeName)} ms", + ), + const SizedBox(height: 16), + if (data.blockedFilters != null) _Legend( + label: data.blockedFilters!.label, + color: data.blockedFilters!.color, + primaryValue: intFormat(statusProvider.serverStatus!.stats.numBlockedFiltering, Platform.localeName), + secondaryValue: "${statusProvider.serverStatus!.stats.numDnsQueries > 0 ? doubleFormat((statusProvider.serverStatus!.stats.numBlockedFiltering/statusProvider.serverStatus!.stats.numDnsQueries)*100, Platform.localeName) : 0}%", + ), + const SizedBox(height: 16), + if (data.replacedSafeBrowsing != null) _Legend( + label: data.replacedSafeBrowsing!.label, + color: data.replacedSafeBrowsing!.color, + primaryValue: intFormat(statusProvider.serverStatus!.stats.numReplacedSafebrowsing, Platform.localeName), + secondaryValue: "${statusProvider.serverStatus!.stats.numDnsQueries > 0 ? doubleFormat((statusProvider.serverStatus!.stats.numReplacedSafebrowsing/statusProvider.serverStatus!.stats.numDnsQueries)*100, Platform.localeName) : 0}%", + ), + const SizedBox(height: 16), + if (data.replacedParental != null) _Legend( + label: data.replacedParental!.label, + color: data.replacedParental!.color, + primaryValue: intFormat(statusProvider.serverStatus!.stats.numReplacedParental, Platform.localeName), + secondaryValue: "${statusProvider.serverStatus!.stats.numDnsQueries > 0 ? doubleFormat((statusProvider.serverStatus!.stats.numReplacedParental/statusProvider.serverStatus!.stats.numDnsQueries)*100, Platform.localeName) : 0}%", + ), + ], + ), + ) + ), + ], + ), + Divider( + thickness: 1, + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.2), + ), + const SizedBox(height: 16) + ], + ); + } + else { + return Column( + children: [ + Column( + children: [ + Text( + AppLocalizations.of(context)!.statistics, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface + ), + ), + const SizedBox(height: 16), + SizedBox( + height: 300, + width: double.maxFinite, + child: CustomCombinedLineChart( + inputData: data, + context: context, + dates: dateTimes, + daysInterval: hoursInterval == 24, + ), + ), + const SizedBox(height: 16), + _Legend( + label: data.totalQueries.label, + color: data.totalQueries.color, + primaryValue: intFormat(statusProvider.serverStatus!.stats.numDnsQueries, Platform.localeName), + secondaryValue: "${doubleFormat(statusProvider.serverStatus!.stats.avgProcessingTime*1000, Platform.localeName)} ms", + ), + const SizedBox(height: 16), + if (data.blockedFilters != null) _Legend( + label: data.blockedFilters!.label, + color: data.blockedFilters!.color, + primaryValue: intFormat(statusProvider.serverStatus!.stats.numBlockedFiltering, Platform.localeName), + secondaryValue: "${statusProvider.serverStatus!.stats.numDnsQueries > 0 ? doubleFormat((statusProvider.serverStatus!.stats.numBlockedFiltering/statusProvider.serverStatus!.stats.numDnsQueries)*100, Platform.localeName) : 0}%", + ), + const SizedBox(height: 16), + if (data.replacedSafeBrowsing != null) _Legend( + label: data.replacedSafeBrowsing!.label, + color: data.replacedSafeBrowsing!.color, + primaryValue: intFormat(statusProvider.serverStatus!.stats.numReplacedSafebrowsing, Platform.localeName), + secondaryValue: "${statusProvider.serverStatus!.stats.numDnsQueries > 0 ? doubleFormat((statusProvider.serverStatus!.stats.numReplacedSafebrowsing/statusProvider.serverStatus!.stats.numDnsQueries)*100, Platform.localeName) : 0}%", + ), + const SizedBox(height: 16), + if (data.replacedParental != null) _Legend( + label: data.replacedParental!.label, + color: data.replacedParental!.color, + primaryValue: intFormat(statusProvider.serverStatus!.stats.numReplacedParental, Platform.localeName), + secondaryValue: "${statusProvider.serverStatus!.stats.numDnsQueries > 0 ? doubleFormat((statusProvider.serverStatus!.stats.numReplacedParental/statusProvider.serverStatus!.stats.numDnsQueries)*100, Platform.localeName) : 0}%", + ), + const SizedBox(height: 16), + ], + ), + Divider( + thickness: 1, + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.2), + ), + const SizedBox(height: 16) + ], + ); + } + } + else { + return const SizedBox(); + } + } +} + +class _Legend extends StatelessWidget { + final String label; + final Color color; + final String primaryValue; + final String? secondaryValue; + + const _Legend({ + required this.label, + required this.color, + required this.primaryValue, + this.secondaryValue + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + child: Text( + label, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: color + ), + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + primaryValue, + style: TextStyle( + color: color, + fontSize: 16, + fontWeight: FontWeight.w500 + ), + ), + if (secondaryValue != null) Text( + secondaryValue!, + style: TextStyle( + fontSize: 10, + color: color + ), + ) + ], + ) + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/home/fab.dart b/lib/screens/home/fab.dart index ce5f8a9..7e465be 100644 --- a/lib/screens/home/fab.dart +++ b/lib/screens/home/fab.dart @@ -1,27 +1,43 @@ import 'package:provider/provider.dart'; import 'package:flutter/material.dart'; -import 'package:adguard_home_manager/screens/home/management_modal.dart'; +import 'package:adguard_home_manager/screens/home/management_modal/management_modal.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; +import 'package:adguard_home_manager/providers/status_provider.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; class HomeFab extends StatelessWidget { - const HomeFab({Key? key}) : super(key: key); + const HomeFab({super.key}); @override Widget build(BuildContext context) { - final serversProvider = Provider.of(context); + final statusProvider = Provider.of(context); + + final width = MediaQuery.of(context).size.width; void openManagementBottomSheet() { - showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: (context) => const ManagementModal(), - backgroundColor: Colors.transparent, - ); + if (width > 700) { + showDialog( + context: context, + builder: (context) => const ManagementModal( + dialog: true, + ), + ); + } + else { + showModalBottomSheet( + context: context, + isScrollControlled: true, + useRootNavigator: true, + builder: (context) => const ManagementModal( + dialog: false, + ), + backgroundColor: Colors.transparent, + ); + } } - return serversProvider.serverStatus.loadStatus == 1 + return statusProvider.loadStatus == LoadStatus.loaded ? FloatingActionButton( onPressed: openManagementBottomSheet, child: const Icon(Icons.shield_rounded), diff --git a/lib/screens/home/home.dart b/lib/screens/home/home.dart index 932ddfe..7a60115 100644 --- a/lib/screens/home/home.dart +++ b/lib/screens/home/home.dart @@ -8,18 +8,22 @@ import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/screens/home/server_status.dart'; +import 'package:adguard_home_manager/screens/home/top_items/top_items_lists.dart'; +import 'package:adguard_home_manager/screens/home/combined_chart.dart'; import 'package:adguard_home_manager/screens/home/appbar.dart'; import 'package:adguard_home_manager/screens/home/fab.dart'; -import 'package:adguard_home_manager/screens/home/top_items.dart'; import 'package:adguard_home_manager/screens/home/chart.dart'; +import 'package:adguard_home_manager/providers/clients_provider.dart'; +import 'package:adguard_home_manager/providers/logs_provider.dart'; import 'package:adguard_home_manager/functions/number_format.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/providers/status_provider.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; +import 'package:adguard_home_manager/functions/snackbar.dart'; class Home extends StatefulWidget { - const Home({Key? key}) : super(key: key); + const Home({super.key}); @override State createState() => _HomeState(); @@ -30,7 +34,15 @@ class _HomeState extends State { late bool isVisible; @override - initState(){ + initState() { + final statusProvider = Provider.of(context, listen: false); + statusProvider.getServerStatus( + withLoadingIndicator: statusProvider.serverStatus != null ? false : true + ); + + final clientsProvider = Provider.of(context, listen: false); + clientsProvider.fetchClients(updateLoading: false); + super.initState(); isVisible = true; @@ -52,172 +64,218 @@ class _HomeState extends State { @override Widget build(BuildContext context) { - final serversProvider = Provider.of(context); + final statusProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final logsProvider = Provider.of(context); - Widget status() { - switch (serversProvider.serverStatus.loadStatus) { - case 0: - return SizedBox( - width: double.maxFinite, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const CircularProgressIndicator(), - const SizedBox(height: 30), - Text( - AppLocalizations.of(context)!.loadingStatus, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ) - ], - ), - ); - - case 1: - return ListView( - controller: scrollController, - children: [ - ServerStatus(serverStatus: serversProvider.serverStatus.data!), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Divider( - thickness: 1, - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.2), - ), - ), - const SizedBox(height: 20), - - HomeChart( - data: serversProvider.serverStatus.data!.stats.dnsQueries, - label: AppLocalizations.of(context)!.dnsQueries, - primaryValue: intFormat(serversProvider.serverStatus.data!.stats.numDnsQueries, Platform.localeName), - secondaryValue: "${doubleFormat(serversProvider.serverStatus.data!.stats.avgProcessingTime*1000, Platform.localeName)} ms", - color: Colors.blue, - ), - - HomeChart( - data: serversProvider.serverStatus.data!.stats.blockedFiltering, - label: AppLocalizations.of(context)!.blockedFilters, - primaryValue: intFormat(serversProvider.serverStatus.data!.stats.numBlockedFiltering, Platform.localeName), - secondaryValue: "${serversProvider.serverStatus.data!.stats.numDnsQueries > 0 ? doubleFormat((serversProvider.serverStatus.data!.stats.numBlockedFiltering/serversProvider.serverStatus.data!.stats.numDnsQueries)*100, Platform.localeName) : 0}%", - color: Colors.red, - ), - - HomeChart( - data: serversProvider.serverStatus.data!.stats.replacedSafebrowsing, - label: AppLocalizations.of(context)!.malwarePhisingBlocked, - primaryValue: intFormat(serversProvider.serverStatus.data!.stats.numReplacedSafebrowsing, Platform.localeName), - secondaryValue: "${serversProvider.serverStatus.data!.stats.numDnsQueries > 0 ? doubleFormat((serversProvider.serverStatus.data!.stats.numReplacedSafebrowsing/serversProvider.serverStatus.data!.stats.numDnsQueries)*100, Platform.localeName) : 0}%", - color: Colors.green, - ), - - HomeChart( - data: serversProvider.serverStatus.data!.stats.replacedParental, - label: AppLocalizations.of(context)!.blockedAdultWebsites, - primaryValue: intFormat(serversProvider.serverStatus.data!.stats.numReplacedParental, Platform.localeName), - secondaryValue: "${serversProvider.serverStatus.data!.stats.numDnsQueries > 0 ? doubleFormat((serversProvider.serverStatus.data!.stats.numReplacedParental/serversProvider.serverStatus.data!.stats.numDnsQueries)*100, Platform.localeName) : 0}%", - color: Colors.orange, - ), - - TopItems( - label: AppLocalizations.of(context)!.topQueriedDomains, - data: serversProvider.serverStatus.data!.stats.topQueriedDomains, - type: 'topQueriedDomains', - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Divider( - thickness: 1, - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.2), - ), - ), - const SizedBox(height: 20), - - TopItems( - label: AppLocalizations.of(context)!.topBlockedDomains, - data: serversProvider.serverStatus.data!.stats.topBlockedDomains, - type: 'topBlockedDomains', - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Divider( - thickness: 1, - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.2), - ), - ), - const SizedBox(height: 20), - - TopItems( - label: AppLocalizations.of(context)!.topClients, - data: serversProvider.serverStatus.data!.stats.topClients, - type: 'topClients', - clients: true, - ), - ], - ); - - case 2: - return SizedBox( - width: double.maxFinite, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon( - Icons.error, - color: Colors.red, - size: 50, - ), - const SizedBox(height: 30), - Text( - AppLocalizations.of(context)!.errorLoadServerStatus, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ) - ], - ), - ); - - default: - return const SizedBox(); - } - } + final width = MediaQuery.of(context).size.width; return Scaffold( - appBar: const HomeAppBar(), - body: RefreshIndicator( - color: Theme.of(context).colorScheme.primary, - onRefresh: () async { - final result = await getServerStatus(serversProvider.selectedServer!); - if (result['result'] == 'success') { - serversProvider.setServerStatusData(result['data']); - } - else { - appConfigProvider.addLog(result['log']); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.serverStatusNotRefreshed), - backgroundColor: Colors.red, + body: SafeArea( + top: false, + child: Stack( + children: [ + NestedScrollView( + controller: scrollController, + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: HomeAppBar(innerBoxScrolled: innerBoxIsScrolled,) + ) + ], + body: SafeArea( + top: false, + bottom: false, + child: Builder( + builder: (context) => RefreshIndicator( + color: Theme.of(context).colorScheme.primary, + displacement: 95, + onRefresh: () async { + final result = await statusProvider.getServerStatus(); + if (mounted && result == false) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.serverStatusNotRefreshed, + color: Colors.red + ); + } + }, + child: CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + ), + if (statusProvider.loadStatus == LoadStatus.loading) SliverFillRemaining( + child: SizedBox( + width: double.maxFinite, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.loadingStatus, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ) + ), + if (statusProvider.loadStatus == LoadStatus.loaded) SliverList.list( + children: [ + ServerStatusWidget(serverStatus: statusProvider.serverStatus!), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Divider( + thickness: 1, + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.2), + ), + ), + const SizedBox(height: 16), + + if (appConfigProvider.combinedChartHome == false) Wrap( + children: [ + FractionallySizedBox( + widthFactor: width > 700 ? 0.5 : 1, + child: HomeChart( + data: statusProvider.serverStatus!.stats.dnsQueries, + label: AppLocalizations.of(context)!.dnsQueries, + primaryValue: intFormat(statusProvider.serverStatus!.stats.numDnsQueries, Platform.localeName), + secondaryValue: "${doubleFormat(statusProvider.serverStatus!.stats.avgProcessingTime*1000, Platform.localeName)} ms", + color: Colors.blue, + hoursInterval: statusProvider.serverStatus!.stats.timeUnits == "days" ? 24 : 1, + onTapTitle: () { + logsProvider.setSelectedResultStatus( + value: "all", + refetch: true + ); + logsProvider.filterLogs(); + appConfigProvider.setSelectedScreen(2); + }, + isDesktop: width > 700, + ), + ), + FractionallySizedBox( + widthFactor: width > 700 ? 0.5 : 1, + child: HomeChart( + data: statusProvider.serverStatus!.stats.blockedFiltering, + label: AppLocalizations.of(context)!.blockedFilters, + primaryValue: intFormat(statusProvider.serverStatus!.stats.numBlockedFiltering, Platform.localeName), + secondaryValue: "${statusProvider.serverStatus!.stats.numDnsQueries > 0 ? doubleFormat((statusProvider.serverStatus!.stats.numBlockedFiltering/statusProvider.serverStatus!.stats.numDnsQueries)*100, Platform.localeName) : 0}%", + color: Colors.red, + hoursInterval: statusProvider.serverStatus!.stats.timeUnits == "days" ? 24 : 1, + onTapTitle: () { + logsProvider.setSelectedResultStatus( + value: "blocked", + refetch: true + ); + appConfigProvider.setSelectedScreen(2); + }, + isDesktop: width > 700, + ), + ), + FractionallySizedBox( + widthFactor: width > 700 ? 0.5 : 1, + child: HomeChart( + data: statusProvider.serverStatus!.stats.replacedSafebrowsing, + label: AppLocalizations.of(context)!.malwarePhishingBlocked, + primaryValue: intFormat(statusProvider.serverStatus!.stats.numReplacedSafebrowsing, Platform.localeName), + secondaryValue: "${statusProvider.serverStatus!.stats.numDnsQueries > 0 ? doubleFormat((statusProvider.serverStatus!.stats.numReplacedSafebrowsing/statusProvider.serverStatus!.stats.numDnsQueries)*100, Platform.localeName) : 0}%", + color: Colors.green, + hoursInterval: statusProvider.serverStatus!.stats.timeUnits == "days" ? 24 : 1, + onTapTitle: () { + logsProvider.setSelectedResultStatus( + value: "blocked_safebrowsing", + refetch: true + ); + appConfigProvider.setSelectedScreen(2); + }, + isDesktop: width > 700, + ), + ), + FractionallySizedBox( + widthFactor: width > 700 ? 0.5 : 1, + child: HomeChart( + data: statusProvider.serverStatus!.stats.replacedParental, + label: AppLocalizations.of(context)!.blockedAdultWebsites, + primaryValue: intFormat(statusProvider.serverStatus!.stats.numReplacedParental, Platform.localeName), + secondaryValue: "${statusProvider.serverStatus!.stats.numDnsQueries > 0 ? doubleFormat((statusProvider.serverStatus!.stats.numReplacedParental/statusProvider.serverStatus!.stats.numDnsQueries)*100, Platform.localeName) : 0}%", + color: Colors.orange, + hoursInterval: statusProvider.serverStatus!.stats.timeUnits == "days" ? 24 : 1, + onTapTitle: () { + logsProvider.setSelectedResultStatus( + value: "blocked_parental", + refetch: true + ); + logsProvider.filterLogs(); + appConfigProvider.setSelectedScreen(2); + }, + isDesktop: width > 700, + ), + ), + ], + ), + + if (appConfigProvider.combinedChartHome == true) const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: CombinedHomeChart(), + ), + + TopItemsLists(order: appConfigProvider.homeTopItemsOrder), + + const SizedBox(height: 16), + ], + ), + if (statusProvider.loadStatus == LoadStatus.error) SliverFillRemaining( + child: SizedBox( + width: double.maxFinite, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon( + Icons.error, + color: Colors.red, + size: 50, + ), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.errorLoadServerStatus, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ) + ), + ], + ) + ), + ) ) - ); - } - }, - child: status() + ), + AnimatedPositioned( + duration: const Duration(milliseconds: 100), + curve: Curves.easeInOut, + bottom: isVisible == true ? + appConfigProvider.showingSnackbar + ? 70 + : 20 + : -70, + right: 20, + child: const HomeFab() + ), + ], + ), ), - floatingActionButton: appConfigProvider.showingSnackbar - ? null - : isVisible - ? const HomeFab() - : null - ); } } \ No newline at end of file diff --git a/lib/screens/home/management_modal.dart b/lib/screens/home/management_modal.dart deleted file mode 100644 index 3852e0e..0000000 --- a/lib/screens/home/management_modal.dart +++ /dev/null @@ -1,210 +0,0 @@ -// ignore_for_file: use_build_context_synchronously - -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; - -class ManagementModal extends StatelessWidget { - const ManagementModal({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final serversProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - - void updateBlocking(bool value, String filter) async { - final result = await serversProvider.updateBlocking( - serversProvider.selectedServer!, - filter, - value - ); - if (result != null) { - if (result != false) { - appConfigProvider.addLog(result['log']); - } - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.invalidUsernamePassword), - backgroundColor: Colors.red, - ) - ); - } - } - - Widget mainSwitch() { - return 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: serversProvider.protectionsManagementProcess.contains('general') == false - ? () => updateBlocking(!serversProvider.serverStatus.data!.generalEnabled, 'general') - : null, - borderRadius: BorderRadius.circular(28), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 12 - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - AppLocalizations.of(context)!.allProtections, - style: const TextStyle( - fontSize: 18, - ), - ), - Switch( - value: serversProvider.serverStatus.data!.generalEnabled, - onChanged: serversProvider.protectionsManagementProcess.contains('general') == false - ? (value) => updateBlocking(value, 'general') - : null, - activeColor: Theme.of(context).colorScheme.primary, - ) - ], - ), - ), - ), - ), - ); - } - - Widget smallSwitch(String label, IconData icon, bool value, Function(bool) onChange, bool disabled) { - return Material( - color: Colors.transparent, - child: InkWell( - onTap: disabled == false - ? () => onChange(!value) - : null, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 44, - vertical: 8 - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Icon( - icon, - size: 24, - color: Theme.of(context).listTileTheme.iconColor, - ), - const SizedBox(width: 16), - Text( - label, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ], - ), - Switch( - value: value, - onChanged: disabled == false - ? onChange - : null, - activeColor: Theme.of(context).colorScheme.primary, - ) - ], - ), - ), - ), - ); - } - - return Container( - width: double.maxFinite, - height: 540, - decoration: BoxDecoration( - color: Theme.of(context).dialogBackgroundColor, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28) - ) - ), - child: Column( - children: [ - Expanded( - child: ListView( - physics: 540 < MediaQuery.of(context).size.height - ? const NeverScrollableScrollPhysics() - : null, - children: [ - Padding( - padding: const EdgeInsets.only(top: 24), - child: Icon( - Icons.shield_rounded, - size: 24, - color: Theme.of(context).listTileTheme.iconColor - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 16), - child: Text( - AppLocalizations.of(context)!.manageServer, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ), - mainSwitch(), - const SizedBox(height: 10), - smallSwitch( - AppLocalizations.of(context)!.ruleFiltering, - Icons.filter_list_rounded, - serversProvider.serverStatus.data!.filteringEnabled, - (value) => updateBlocking(value, 'filtering'), - serversProvider.protectionsManagementProcess.contains('filtering') - ), - smallSwitch( - AppLocalizations.of(context)!.safeBrowsing, - Icons.vpn_lock_rounded, - serversProvider.serverStatus.data!.safeBrowsingEnabled, - (value) => updateBlocking(value, 'safeBrowsing'), - serversProvider.protectionsManagementProcess.contains('safeBrowsing') - ), - smallSwitch( - AppLocalizations.of(context)!.parentalFiltering, - Icons.block, - serversProvider.serverStatus.data!.parentalControlEnabled, - (value) => updateBlocking(value, 'parentalControl'), - serversProvider.protectionsManagementProcess.contains('parentalControl') - ), - smallSwitch( - AppLocalizations.of(context)!.safeSearch, - Icons.search_rounded, - serversProvider.serverStatus.data!.safeSearchEnabled, - (value) => updateBlocking(value, 'safeSearch'), - serversProvider.protectionsManagementProcess.contains('safeSearch') - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.all(24), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(AppLocalizations.of(context)!.close), - ), - ], - ), - ) - ], - ), - ); - } -} \ No newline at end of file diff --git a/lib/screens/home/management_modal/main_switch.dart b/lib/screens/home/management_modal/main_switch.dart new file mode 100644 index 0000000..f746b0e --- /dev/null +++ b/lib/screens/home/management_modal/main_switch.dart @@ -0,0 +1,238 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:expandable/expandable.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/functions/format_time.dart'; +import 'package:adguard_home_manager/providers/status_provider.dart'; + +class MainSwitch extends StatelessWidget { + final ExpandableController expandableController; + final void Function({ required bool value, required String filter }) updateBlocking; + final void Function(int) disableWithCountdown; + final Animation animation; + + const MainSwitch({ + super.key, + required this.expandableController, + required this.updateBlocking, + required this.disableWithCountdown, + required this.animation, + }); + + @override + Widget build(BuildContext context) { + final statusProvider = Provider.of(context); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: ExpandableNotifier( + controller: expandableController, + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(28), + child: InkWell( + onTap: statusProvider.serverStatus!.generalEnabled == true && !statusProvider.protectionsManagementProcess.contains('general') + ? () => expandableController.toggle() + : null, + borderRadius: BorderRadius.circular(28), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 12 + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(28), + color: Theme.of(context).colorScheme.primary.withOpacity(0.1) + ), + child: Expandable( + theme: const ExpandableThemeData( + animationDuration: Duration(milliseconds: 200), + fadeCurve: Curves.ease + ), + collapsed: _TopRow( + legacyMode: false, + expandableController: expandableController, + updateBlocking: updateBlocking, + animation: animation, + ), + expanded: Column( + children: [ + _TopRow( + legacyMode: false, + expandableController: expandableController, + updateBlocking: updateBlocking, + animation: animation, + ), + _BottomRow( + disableWithCountdown: disableWithCountdown, + ), + const SizedBox(height: 8) + ], + ) + ), + ), + ), + ) + ) + ); + } +} + +class _TopRow extends StatelessWidget { + final bool legacyMode; + final ExpandableController expandableController; + final void Function({ required bool value, required String filter }) updateBlocking; + final Animation animation; + + const _TopRow({ + required this.legacyMode, + required this.expandableController, + required this.updateBlocking, + required this.animation, + }); + + @override + Widget build(BuildContext context) { + final statusProvider = Provider.of(context); + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + children: [ + RotationTransition( + turns: animation, + child: Icon( + Icons.keyboard_arrow_down_rounded, + size: 26, + color: statusProvider.serverStatus!.generalEnabled == true + ? Theme.of(context).colorScheme.onSurfaceVariant + : Colors.grey, + ), + ), + const SizedBox(width: 8), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppLocalizations.of(context)!.allProtections, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 18, + ), + ), + if (statusProvider.serverStatus!.timeGeneralDisabled > 0) ...[ + const SizedBox(height: 2), + if (statusProvider.currentDeadline != null) Text( + "${AppLocalizations.of(context)!.remainingTime}: ${formatRemainingSeconds(statusProvider.remainingTime)}" + ) + ] + ], + ), + ), + ], + ), + ), + const SizedBox(width: 8), + Switch( + value: statusProvider.serverStatus!.generalEnabled, + onChanged: statusProvider.protectionsManagementProcess.contains('general') == false + ? (value) { + if (value == false && expandableController.expanded == true && legacyMode == false) { + expandableController.toggle(); + } + updateBlocking( + value: value, + filter: 'general' + ); + } : null, + ) + ] + ); + } +} + +class _BottomRow extends StatefulWidget { + final void Function(int) disableWithCountdown; + + const _BottomRow({ + required this.disableWithCountdown, + }); + + @override + State<_BottomRow> createState() => _BottomRowState(); +} + +class _BottomRowState extends State<_BottomRow> { + final _chipsScrollController = ScrollController(); + + @override + Widget build(BuildContext context) { + final statusProvider = Provider.of(context); + + final textScale = MediaQuery.of(context).textScaler.scale(1); + + return Container( + height: Platform.isMacOS || Platform.isLinux || Platform.isWindows + ? 50 * textScale + : 40 * textScale, + margin: const EdgeInsets.only(top: 8), + child: Scrollbar( + controller: _chipsScrollController, + 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: _chipsScrollController, + scrollDirection: Axis.horizontal, + children: [ + ActionChip( + label: Text(AppLocalizations.of(context)!.seconds(30)), + onPressed: statusProvider.protectionsManagementProcess.contains('general') == false && statusProvider.serverStatus!.generalEnabled == true + ? () => widget.disableWithCountdown(29000) + : null, + ), + const SizedBox(width: 8), + ActionChip( + label: Text(AppLocalizations.of(context)!.minute(1)), + onPressed: statusProvider.protectionsManagementProcess.contains('general') == false && statusProvider.serverStatus!.generalEnabled == true + ? () => widget.disableWithCountdown(59000) + : null, + ), + const SizedBox(width: 8), + ActionChip( + label: Text(AppLocalizations.of(context)!.minutes(10)), + onPressed: statusProvider.protectionsManagementProcess.contains('general') == false && statusProvider.serverStatus!.generalEnabled == true + ? () => widget.disableWithCountdown(599000) + : null, + ), + const SizedBox(width: 8), + ActionChip( + label: Text(AppLocalizations.of(context)!.hour(1)), + onPressed: statusProvider.protectionsManagementProcess.contains('general') == false && statusProvider.serverStatus!.generalEnabled == true + ? () => widget.disableWithCountdown(3599000) + : null, + ), + const SizedBox(width: 8), + ActionChip( + label: Text(AppLocalizations.of(context)!.hours(24)), + onPressed: statusProvider.protectionsManagementProcess.contains('general') == false && statusProvider.serverStatus!.generalEnabled == true + ? () => widget.disableWithCountdown(86399000) + : null, + ), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/home/management_modal/management_modal.dart b/lib/screens/home/management_modal/management_modal.dart new file mode 100644 index 0000000..5a931ad --- /dev/null +++ b/lib/screens/home/management_modal/management_modal.dart @@ -0,0 +1,267 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:expandable/expandable.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/screens/home/management_modal/main_switch.dart'; +import 'package:adguard_home_manager/screens/home/management_modal/small_switch.dart'; + +import 'package:adguard_home_manager/functions/snackbar.dart'; +import 'package:adguard_home_manager/providers/status_provider.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; + +class ManagementModal extends StatefulWidget { + final bool dialog; + + const ManagementModal({ + super.key, + required this.dialog + }); + + @override + State createState() => _ManagementModalState(); +} + +class _ManagementModalState extends State with SingleTickerProviderStateMixin { + late AnimationController animationController; + late Animation animation; + final ExpandableController expandableController = ExpandableController(); + + @override + void initState() { + expandableController.addListener(() async { + await Future.delayed(const Duration(milliseconds: 200)); + if (expandableController.value == false) { + animationController.animateTo(0); + } + else { + animationController.animateBack(1); + } + }); + + animationController = AnimationController( + duration: const Duration(milliseconds: 200), + vsync: this, + ) + ..addListener(() => setState(() => {})); + animation = Tween( + begin: 0.0, + end: 0.5, + ).animate(CurvedAnimation( + parent: animationController, + curve: Curves.easeInOut + )); + + super.initState(); + } + + @override + void dispose() { + animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final statusProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + + void updateBlocking({ + required bool value, + required String filter, + int? time + }) async { + final result = await statusProvider.updateBlocking( + block: filter, + newStatus: value, + time: time + ); + if (mounted && result == false) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.invalidUsernamePassword, + color: Colors.red + ); + } + } + + void disableWithCountdown(int time) async { + updateBlocking(value: false, filter: 'general', time: time); + expandableController.toggle(); + } + + if (widget.dialog == true) { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 400 + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: SingleChildScrollView( + child: _Modal( + expandableController: expandableController, + updateBlocking: updateBlocking, + disableWithCountdown: disableWithCountdown, + animation: animation, + ) + ), + ), + Padding( + padding: const EdgeInsets.all(24), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.close), + ), + ], + ), + ), + if (Platform.isIOS) const SizedBox(height: 16) + ], + ), + ), + ); + } + else { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28) + ) + ), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: SingleChildScrollView( + child: _Modal( + expandableController: expandableController, + updateBlocking: updateBlocking, + disableWithCountdown: disableWithCountdown, + animation: animation, + ) + ), + ), + Padding( + padding: const EdgeInsets.all(24), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.close), + ), + ], + ), + ), + if (Platform.isIOS) const SizedBox(height: 16) + ], + ), + ), + ); + } + } +} + +class _Modal extends StatelessWidget { + final ExpandableController expandableController; + final void Function({ required bool value, required String filter }) updateBlocking; + final void Function(int) disableWithCountdown; + final Animation animation; + + const _Modal({ + required this.expandableController, + required this.updateBlocking, + required this.disableWithCountdown, + required this.animation, + }); + + @override + Widget build(BuildContext context) { + final statusProvider = Provider.of(context); + + return Wrap( + children: [ + // Header + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(top: 24), + child: Icon( + Icons.shield_rounded, + size: 24, + color: Theme.of(context).listTileTheme.iconColor + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Text( + AppLocalizations.of(context)!.manageServer, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ], + ), + ], + ), + + MainSwitch( + expandableController: expandableController, + updateBlocking: updateBlocking, + disableWithCountdown: disableWithCountdown, + animation: animation, + ), + Container(height: 10), + SmallSwitch( + label: AppLocalizations.of(context)!.ruleFiltering, + icon: Icons.filter_list_rounded, + value: statusProvider.serverStatus!.filteringEnabled, + onChange: (value) => updateBlocking(value: value, filter: 'filtering'), + disabled: statusProvider.protectionsManagementProcess.contains('filtering') + ), + SmallSwitch( + label: AppLocalizations.of(context)!.safeBrowsing, + icon: Icons.vpn_lock_rounded, + value: statusProvider.serverStatus!.safeBrowsingEnabled, + onChange: (value) => updateBlocking(value: value, filter: 'safeBrowsing'), + disabled: statusProvider.protectionsManagementProcess.contains('safeBrowsing') + ), + SmallSwitch( + label: AppLocalizations.of(context)!.parentalFiltering, + icon: Icons.block, + value: statusProvider.serverStatus!.parentalControlEnabled, + onChange: (value) => updateBlocking(value: value, filter: 'parentalControl'), + disabled: statusProvider.protectionsManagementProcess.contains('parentalControl') + ), + SmallSwitch( + label: AppLocalizations.of(context)!.safeSearch, + icon: Icons.search_rounded, + value: statusProvider.serverStatus!.safeSearchEnabled, + onChange: (value) => updateBlocking(value: value, filter: 'safeSearch'), + disabled: statusProvider.protectionsManagementProcess.contains('safeSearch') + ), + ], + ); + } +} diff --git a/lib/screens/home/management_modal/small_switch.dart b/lib/screens/home/management_modal/small_switch.dart new file mode 100644 index 0000000..192360c --- /dev/null +++ b/lib/screens/home/management_modal/small_switch.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; + +class SmallSwitch extends StatelessWidget { + final String label; + final IconData icon; + final bool value; + final void Function(bool) onChange; + final bool disabled; + + const SmallSwitch({ + super.key, + required this.label, + required this.icon, + required this.value, + required this.onChange, + required this.disabled, + }); + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: disabled == false + ? () => onChange(!value) + : null, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 44, + vertical: 8 + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon( + icon, + size: 24, + color: Theme.of(context).listTileTheme.iconColor, + ), + const SizedBox(width: 16), + Text( + label, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ], + ), + Switch( + value: value, + onChanged: disabled == false + ? onChange + : null, + ) + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/home/server_status.dart b/lib/screens/home/server_status.dart index c4d8d97..b716c82 100644 --- a/lib/screens/home/server_status.dart +++ b/lib/screens/home/server_status.dart @@ -5,19 +5,23 @@ import 'package:adguard_home_manager/screens/home/status_box.dart'; import 'package:adguard_home_manager/models/server_status.dart'; -class ServerStatus extends StatelessWidget { - final ServerStatusData serverStatus; +class ServerStatusWidget extends StatelessWidget { + final ServerStatus serverStatus; - const ServerStatus({ - Key? key, + const ServerStatusWidget({ + super.key, required this.serverStatus, - }) : super(key: key); + }); @override Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(20), + final width = MediaQuery.of(context).size.width; + final textScaleFactor = MediaQuery.of(context).textScaler.scale(1); + + return Padding( + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16), child: Column( + mainAxisSize: MainAxisSize.min, children: [ Text( AppLocalizations.of(context)!.serverStatus, @@ -27,40 +31,40 @@ class ServerStatus extends StatelessWidget { color: Theme.of(context).colorScheme.onSurface ), ), - const SizedBox(height: 20), - SizedBox( - height: 140, - child: GridView( - physics: const NeverScrollableScrollPhysics(), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - crossAxisSpacing: 10, - mainAxisSpacing: 10, - mainAxisExtent: 65 - ), - children: [ - StatusBox( - icon: Icons.filter_list_rounded, - label: AppLocalizations.of(context)!.ruleFilteringWidget, - isEnabled: serverStatus.filteringEnabled - ), - StatusBox( - icon: Icons.vpn_lock_rounded, - label: AppLocalizations.of(context)!.safeBrowsingWidget, - isEnabled: serverStatus.safeBrowsingEnabled - ), - StatusBox( - icon: Icons.block, - label: AppLocalizations.of(context)!.parentalFilteringWidget, - isEnabled: serverStatus.parentalControlEnabled - ), - StatusBox( - icon: Icons.search_rounded, - label: AppLocalizations.of(context)!.safeSearchWidget, - isEnabled: serverStatus.safeSearchEnabled - ), - ], + const SizedBox(height: 16), + GridView( + primary: false, + shrinkWrap: true, + padding: const EdgeInsets.all(0), + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: width > 700 ? 4 : 2, + crossAxisSpacing: 10, + mainAxisSpacing: 10, + mainAxisExtent: 70*textScaleFactor ), + children: [ + StatusBox( + icon: Icons.filter_list_rounded, + label: AppLocalizations.of(context)!.ruleFilteringWidget, + isEnabled: serverStatus.filteringEnabled + ), + StatusBox( + icon: Icons.vpn_lock_rounded, + label: AppLocalizations.of(context)!.safeBrowsingWidget, + isEnabled: serverStatus.safeBrowsingEnabled + ), + StatusBox( + icon: Icons.block, + label: AppLocalizations.of(context)!.parentalFilteringWidget, + isEnabled: serverStatus.parentalControlEnabled + ), + StatusBox( + icon: Icons.search_rounded, + label: AppLocalizations.of(context)!.safeSearchWidget, + isEnabled: serverStatus.safeSearchEnabled + ), + ], ) ], ), diff --git a/lib/screens/home/status_box.dart b/lib/screens/home/status_box.dart index 62d4279..38f6c95 100644 --- a/lib/screens/home/status_box.dart +++ b/lib/screens/home/status_box.dart @@ -9,11 +9,11 @@ class StatusBox extends StatelessWidget { final bool isEnabled; const StatusBox({ - Key? key, + super.key, required this.icon, required this.label, required this.isEnabled - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -44,13 +44,15 @@ class StatusBox extends StatelessWidget { : Colors.grey.computeLuminance() > 0.5 ? Colors.black : Colors.white, ), const SizedBox(width: 12), - Text( - label, - style: TextStyle( - color: appConfigProvider.useThemeColorForStatus == true - ? Theme.of(context).colorScheme.primary.computeLuminance() > 0.5 ? Colors.black : Colors.white - : Colors.grey.computeLuminance() > 0.5 ? Colors.black : Colors.white, - fontWeight: FontWeight.w500 + Flexible( + child: Text( + label, + style: TextStyle( + color: appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary.computeLuminance() > 0.5 ? Colors.black : Colors.white + : Colors.grey.computeLuminance() > 0.5 ? Colors.black : Colors.white, + fontWeight: FontWeight.w500 + ), ), ) ], diff --git a/lib/screens/home/top_items.dart b/lib/screens/home/top_items.dart deleted file mode 100644 index 52ffeaf..0000000 --- a/lib/screens/home/top_items.dart +++ /dev/null @@ -1,300 +0,0 @@ -// ignore_for_file: use_build_context_synchronously - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:provider/provider.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -import 'package:adguard_home_manager/screens/home/top_items_options_modal.dart'; -import 'package:adguard_home_manager/screens/top_items/top_items.dart'; - -import 'package:adguard_home_manager/models/applied_filters.dart'; -import 'package:adguard_home_manager/providers/logs_provider.dart'; -import 'package:adguard_home_manager/classes/process_modal.dart'; -import 'package:adguard_home_manager/models/filtering_status.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; -import 'package:adguard_home_manager/providers/app_config_provider.dart'; -class TopItems extends StatelessWidget { - final String type; - final String label; - final List> data; - final bool? clients; - - const TopItems({ - Key? key, - required this.type, - required this.label, - required this.data, - this.clients - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final serversProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - final logsProvider = Provider.of(context); - - bool? getIsBlocked() { - if (type == 'topBlockedDomains') { - return true; - } - else if (type == 'topQueriedDomains') { - return false; - } - else { - return null; - } - } - - void blockUnblock(String domain, String newStatus) async { - final ProcessModal processModal = ProcessModal(context: context); - processModal.open(AppLocalizations.of(context)!.savingUserFilters); - - final rules = await getFilteringRules(server: serversProvider.selectedServer!); - - if (rules['result'] == 'success') { - FilteringStatus oldStatus = serversProvider.serverStatus.data!.filteringStatus; - - List newRules = rules['data'].userRules.where((d) => !d.contains(domain)).toList(); - if (newStatus == 'block') { - newRules.add("||$domain^"); - } - else if (newStatus == 'unblock') { - newRules.add("@@||$domain^"); - } - FilteringStatus newObj = serversProvider.serverStatus.data!.filteringStatus; - newObj.userRules = newRules; - serversProvider.setFilteringStatus(newObj); - - final result = await postFilteringRules(server: serversProvider.selectedServer!, data: {'rules': newRules}); - - processModal.close(); - - if (result['result'] == 'success') { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.userFilteringRulesUpdated), - backgroundColor: Colors.green, - ) - ); - } - else { - appConfigProvider.addLog(result['log']); - serversProvider.setFilteringStatus(oldStatus); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.userFilteringRulesNotUpdated), - backgroundColor: Colors.red, - ) - ); - } - } - else { - appConfigProvider.addLog(rules['log']); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.userFilteringRulesNotUpdated), - backgroundColor: Colors.red, - ) - ); - } - } - - void copyDomainClipboard(String domain) async { - await Clipboard.setData( - ClipboardData(text: domain) - ); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.domainCopiedClipboard), - backgroundColor: Colors.green, - ) - ); - } - - void openOptionsModal(String domain, String type) { - showDialog( - context: context, - builder: (context) => TopItemsOptionsModal( - isBlocked: getIsBlocked(), - changeStatus: (String status) => blockUnblock(domain, status), - copyToClipboard: () => copyDomainClipboard(domain), - type: type, - ) - ); - } - - Widget rowItem(Map item) { - String? name; - if (clients != null && clients == true) { - try { - name = serversProvider.serverStatus.data!.clients.firstWhere((c) => c.ids.contains(item.keys.toList()[0])).name; - } catch (e) { - // ---- // - } - } - - return Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - if (type == 'topQueriedDomains' || type == 'topBlockedDomains') { - logsProvider.setDomainText(item.keys.toList()[0]); - logsProvider.setSelectedClients(null); - logsProvider.setAppliedFilters( - AppliedFiters( - selectedResultStatus: 'all', - domainText: item.keys.toList()[0], - clients: null - ) - ); - appConfigProvider.setSelectedScreen(2); - } - else if (type == 'topClients') { - logsProvider.setDomainText(null); - logsProvider.setSelectedClients([item.keys.toList()[0]]); - logsProvider.setAppliedFilters( - AppliedFiters( - selectedResultStatus: 'all', - domainText: null, - clients: [item.keys.toList()[0]] - ) - ); - appConfigProvider.setSelectedScreen(2); - } - }, - onLongPress: () => openOptionsModal(item.keys.toList()[0], type), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 8 - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - item.keys.toList()[0], - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 16, - color: Theme.of(context).colorScheme.onSurface - ), - ), - if (name != null) ...[ - const SizedBox(height: 5), - Text( - name, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.onSurfaceVariant - ), - ), - ] - ], - ), - ), - Text( - item.values.toList()[0].toString(), - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface - ), - ) - ], - ), - ), - ), - ); - } - - List> generateData() { - switch (type) { - case 'topQueriedDomains': - return serversProvider.serverStatus.data!.stats.topQueriedDomains; - - case 'topBlockedDomains': - return serversProvider.serverStatus.data!.stats.topBlockedDomains; - - case 'topClients': - return serversProvider.serverStatus.data!.stats.topClients; - - default: - return []; - } - } - - return SizedBox( - child: Column( - children: [ - Text( - label, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.onSurface - ), - ), - const SizedBox(height: 20), - if (data.isEmpty) Padding( - padding: const EdgeInsets.only( - bottom: 20, - top: 10 - ), - child: Text( - AppLocalizations.of(context)!.noItems, - style: TextStyle( - fontSize: 16, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ), - if (data.isNotEmpty) rowItem(data[0]), - if (data.length >= 2) rowItem(data[1]), - if (data.length >= 3) rowItem(data[2]), - if (data.length >= 4) rowItem(data[3]), - if (data.length >= 5) rowItem(data[4]), - if (data.length > 5) ...[ - const SizedBox(height: 20), - Padding( - padding: const EdgeInsets.only(right: 20), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () => Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => TopItemsScreen( - type: type, - title: label, - isClient: clients, - data: generateData(), - ) - ) - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(AppLocalizations.of(context)!.viewMore), - const SizedBox(width: 10), - const Icon( - Icons.arrow_forward, - size: 20, - ) - ], - ) - ), - ], - ), - ), - const SizedBox(height: 10), - ] - ], - ), - ); - } -} \ No newline at end of file diff --git a/lib/screens/home/top_items/row_item.dart b/lib/screens/home/top_items/row_item.dart new file mode 100644 index 0000000..bfa43e7 --- /dev/null +++ b/lib/screens/home/top_items/row_item.dart @@ -0,0 +1,185 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/widgets/options_menu.dart'; + +import 'package:adguard_home_manager/models/menu_option.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/providers/status_provider.dart'; + +class RowItem extends StatelessWidget { + final HomeTopItems type; + final Color chartColor; + final String domain; + final String number; + final bool clients; + final bool showColor; + final String? unit; + final List Function(dynamic) options; + final void Function(dynamic)? onTapEntry; + + const RowItem({ + super.key, + required this.type, + required this.chartColor, + required this.domain, + required this.number, + required this.clients, + required this.showColor, + required this.options, + this.onTapEntry, + this.unit, + }); + + @override + Widget build(BuildContext context) { + final statusProvider = Provider.of(context); + + String? name; + if (clients == true) { + try { + name = statusProvider.serverStatus!.clients.firstWhere((c) => c.ids.contains(domain)).name; + } catch (e) { + // ---- // + } + } + + return Material( + color: Colors.transparent, + child: OptionsMenu( + value: domain, + options: options, + onTap: onTapEntry, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 8 + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Row( + children: [ + if (showColor == true) Container( + margin: const EdgeInsets.only(right: 16), + width: 12, + height: 12, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + color: chartColor + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + domain, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface + ), + ), + if (name != null) ...[ + const SizedBox(height: 5), + Text( + name, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ), + ] + ], + ), + ), + ], + ), + ), + const SizedBox(width: 16), + Text( + number, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface + ), + ) + ], + ), + ), + ), + ); + } +} + +class OthersRowItem extends StatelessWidget { + final List> items; + final bool showColor; + + const OthersRowItem({ + super.key, + required this.items, + required this.showColor, + }); + + @override + Widget build(BuildContext context) { + if (items.length <= 5) { + return const SizedBox(); + } + + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 8 + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Row( + children: [ + if (showColor == true) Container( + margin: const EdgeInsets.only(right: 16), + width: 12, + height: 12, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + color: Colors.grey + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppLocalizations.of(context)!.others, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface + ), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(width: 16), + Text( + List.from( + items.sublist(5, items.length).map((e) => e.values.first.toInt()) + ).reduce((a, b) => a + b).toString(), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface + ), + ) + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/home/top_items/top_items_lists.dart b/lib/screens/home/top_items/top_items_lists.dart new file mode 100644 index 0000000..3ca6e24 --- /dev/null +++ b/lib/screens/home/top_items/top_items_lists.dart @@ -0,0 +1,293 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/screens/home/top_items/top_items_section.dart'; + +import 'package:adguard_home_manager/providers/clients_provider.dart'; +import 'package:adguard_home_manager/functions/number_format.dart'; +import 'package:adguard_home_manager/functions/snackbar.dart'; +import 'package:adguard_home_manager/classes/process_modal.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/logs_provider.dart'; +import 'package:adguard_home_manager/functions/copy_clipboard.dart'; +import 'package:adguard_home_manager/models/menu_option.dart'; +import 'package:adguard_home_manager/providers/status_provider.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; + +class TopItemsLists extends StatelessWidget { + final List order; + + const TopItemsLists({ + super.key, + required this.order, + }); + + @override + Widget build(BuildContext context) { + final statusProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + final logsProvider = Provider.of(context); + final clientsProvider = Provider.of(context); + + List bottom = [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Divider( + thickness: 1, + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.2), + ), + ), + const SizedBox(height: 16), + ]; + + void filterDomainLogs({required String value}) { + logsProvider.setSearchText(value); + logsProvider.setSelectedClients(null); + logsProvider.setAppliedFilters( + AppliedFiters( + selectedResultStatus: 'all', + searchText: value, + clients: [] + ) + ); + appConfigProvider.setSelectedScreen(2); + } + + void filterClientLogs({required String value}) { + logsProvider.setSearchText(null); + logsProvider.setSelectedClients([value]); + logsProvider.setAppliedFilters( + AppliedFiters( + selectedResultStatus: 'all', + searchText: null, + clients: [value] + ) + ); + appConfigProvider.setSelectedScreen(2); + } + + void blockUnblock({required String domain, required String newStatus}) async { + final ProcessModal processModal = ProcessModal(); + processModal.open(AppLocalizations.of(context)!.savingUserFilters); + + final rules = await statusProvider.blockUnblockDomain( + domain: domain, + newStatus: newStatus + ); + + processModal.close(); + + if (!context.mounted) return; + if (rules == true) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.userFilteringRulesUpdated, + color: Colors.green + ); + } + else { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.userFilteringRulesNotUpdated, + color: Colors.red + ); + } + } + + void copyValueClipboard(dynamic value) { + copyToClipboard(value: value, successMessage: AppLocalizations.of(context)!.copiedClipboard); + } + + void blockUnblockClient(dynamic client) async { + final currentList = clientsProvider.checkClientList(client); + final newList = currentList == AccessSettingsList.allowed || currentList == null + ? AccessSettingsList.disallowed + : AccessSettingsList.allowed; + + ProcessModal processModal = ProcessModal(); + processModal.open( + currentList == AccessSettingsList.allowed || currentList == null + ? AppLocalizations.of(context)!.blockingClient + : AppLocalizations.of(context)!.unblockingClient + ); + + final result = await clientsProvider.addClientList(client, newList); + if (!context.mounted) return; + + processModal.close(); + + if (result.successful == true) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.clientAddedSuccessfully, + color: Colors.green + ); + } + else if (result.successful == false && result.content == 'client_another_list') { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.clientAnotherList, + color: Colors.red + ); + } + else { + showSnackbar( + appConfigProvider: appConfigProvider, + label: newList == AccessSettingsList.allowed || newList == AccessSettingsList.disallowed + ? AppLocalizations.of(context)!.clientNotRemoved + : AppLocalizations.of(context)!.domainNotAdded, + color: Colors.red + ); + } + } + + return Column( + children: order.asMap().entries.map((item) { + switch (item.value) { + case HomeTopItems.queriedDomains: + return Column( + children: [ + TopItemsSection( + label: AppLocalizations.of(context)!.topQueriedDomains, + type: HomeTopItems.queriedDomains, + data: statusProvider.serverStatus?.stats.topQueriedDomains ?? [], + withChart: true, + withProgressBar: true, + buildValue: (v) => v.toString(), + menuOptions: (v) => [ + MenuOption( + title: AppLocalizations.of(context)!.blockDomain, + icon: Icons.block_rounded, + action: () => blockUnblock(domain: v.toString(), newStatus: 'block') + ), + MenuOption( + title: AppLocalizations.of(context)!.copyClipboard, + icon: Icons.copy_rounded, + action: () => copyValueClipboard(v) + ), + ], + onTapEntry: (v) => filterDomainLogs(value: v.toString()), + ), + if (item.key < order.length - 1) ...bottom + ], + ); + + case HomeTopItems.blockedDomains: + return Column( + children: [ + TopItemsSection( + label: AppLocalizations.of(context)!.topBlockedDomains, + type: HomeTopItems.blockedDomains, + data: statusProvider.serverStatus?.stats.topBlockedDomains ?? [], + withChart: true, + withProgressBar: true, + buildValue: (v) => v.toString(), + menuOptions: (v) => [ + MenuOption( + title: AppLocalizations.of(context)!.unblockDomain, + icon: Icons.check_rounded, + action: () => blockUnblock(domain: v, newStatus: 'unblock') + ), + MenuOption( + title: AppLocalizations.of(context)!.copyClipboard, + icon: Icons.copy_rounded, + action: () => copyValueClipboard(v) + ) + ], + onTapEntry: (v) => filterDomainLogs(value: v), + ), + if (item.key < order.length - 1) ...bottom + ], + ); + + case HomeTopItems.recurrentClients: + return Column( + children: [ + TopItemsSection( + label: AppLocalizations.of(context)!.topClients, + type: HomeTopItems.recurrentClients, + data: statusProvider.serverStatus?.stats.topClients ?? [], + withChart: true, + withProgressBar: true, + buildValue: (v) => v.toString(), + menuOptions: (v) => [ + if (clientsProvider.clients?.clientsAllowedBlocked != null) MenuOption( + title: clientsProvider.checkClientList(v) == AccessSettingsList.allowed || clientsProvider.checkClientList(v) == null + ? AppLocalizations.of(context)!.blockClient + : AppLocalizations.of(context)!.unblockClient, + icon: clientsProvider.checkClientList(v) == AccessSettingsList.allowed || clientsProvider.checkClientList(v) == null + ? Icons.block_rounded + : Icons.check_rounded, + action: () => blockUnblockClient(v) + ), + MenuOption( + title: AppLocalizations.of(context)!.copyClipboard, + icon: Icons.copy_rounded, + action: () => copyValueClipboard(v) + ), + ], + onTapEntry: (v) => filterClientLogs(value: v), + ), + if (item.key < order.length - 1) ...bottom + ], + ); + + case HomeTopItems.topUpstreams: + return statusProvider.serverStatus!.stats.topUpstreamResponses != null + ? Column( + children: [ + TopItemsSection( + label: AppLocalizations.of(context)!.topUpstreams, + type: HomeTopItems.topUpstreams, + data: statusProvider.serverStatus?.stats.topUpstreamResponses ?? [], + withChart: true, + withProgressBar: true, + buildValue: (v) => v.toString(), + menuOptions: (v) => [ + MenuOption( + title: AppLocalizations.of(context)!.copyClipboard, + icon: Icons.copy_rounded, + action: () => copyValueClipboard(v) + ) + ], + ), + if (item.key < order.length - 1) ...bottom + ], + ) + : const SizedBox(); + + case HomeTopItems.avgUpstreamResponseTime: + return statusProvider.serverStatus!.stats.topUpstreamsAvgTime != null + ? Column( + children: [ + TopItemsSection( + label: AppLocalizations.of(context)!.averageUpstreamResponseTime, + type: HomeTopItems.avgUpstreamResponseTime, + data: statusProvider.serverStatus?.stats.topUpstreamsAvgTime ?? [], + withChart: false, + withProgressBar: false, + buildValue: (v) => "${doubleFormat(v*1000, Platform.localeName)} ms", + menuOptions: (v) => [ + MenuOption( + title: AppLocalizations.of(context)!.copyClipboard, + icon: Icons.copy_rounded, + action: () => copyValueClipboard(v) + ) + ], + ), + if (item.key < order.length - 1) ...bottom + ], + ) + : const SizedBox(); + + default: + return const SizedBox(); + } + }).toList(), + ); + } +} \ No newline at end of file diff --git a/lib/screens/home/top_items/top_items_screen.dart b/lib/screens/home/top_items/top_items_screen.dart new file mode 100644 index 0000000..bf0a0f5 --- /dev/null +++ b/lib/screens/home/top_items/top_items_screen.dart @@ -0,0 +1,391 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:percent_indicator/percent_indicator.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/widgets/options_menu.dart'; +import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; +import 'package:adguard_home_manager/widgets/floating_search_bar.dart'; + +import 'package:adguard_home_manager/models/menu_option.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/functions/number_format.dart'; +import 'package:adguard_home_manager/providers/status_provider.dart'; + +enum _SortingOptions { highestToLowest, lowestToHighest } +final GlobalKey _searchButtonKey = GlobalKey(); + +class TopItemsScreen extends StatefulWidget { + final HomeTopItems type; + final String title; + final bool? isClient; + final List> data; + final bool withProgressBar; + final String Function(dynamic) buildValue; + final List Function(dynamic) options; + final void Function(dynamic)? onTapEntry; + final bool isFullscreen; + + const TopItemsScreen({ + super.key, + required this.type, + required this.title, + this.isClient, + required this.data, + required this.withProgressBar, + required this.buildValue, + required this.options, + this.onTapEntry, + required this.isFullscreen, + }); + + @override + State createState() => _TopItemsScreenState(); +} + +class _TopItemsScreenState extends State { + _SortingOptions _sortingOptions = _SortingOptions.highestToLowest; + final TextEditingController searchController = TextEditingController(); + String? _currentSearchValue = ""; + + List> data = []; + List> screenData = []; + + void search(String value) { + List> newValues = widget.data.where((item) => item.keys.toList()[0].contains(value)).toList(); + setState(() { + screenData = newValues; + _currentSearchValue = searchController.text; + }); + } + + @override + void initState() { + data = widget.data; + screenData = widget.data; + super.initState(); + } + + @override + Widget build(BuildContext context) { + double total = 0; + for (var element in data) { + total = total + double.parse(element.values.toList()[0].toString()); + } + + final sortedValues = _sortingOptions == _SortingOptions.lowestToHighest + ? screenData.reversed.toList() + : screenData.toList(); + + void showSearchDialog() { + showDialog( + context: context, + builder: (context) => FloatingSearchBar( + existingSearchValue: _currentSearchValue, + searchButtonRenderBox: _searchButtonKey.currentContext?.findRenderObject() as RenderBox?, + onSearchCompleted: (v) { + List> newValues = widget.data.where((item) => item.keys.toList()[0].contains(v)).toList(); + setState(() { + screenData = newValues; + _currentSearchValue = v; + }); + }, + ), + ); + } + + if (widget.isFullscreen == true) { + return Scaffold( + body: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverAppBar.large( + title: Text(widget.title), + actions: [ + IconButton( + key: _searchButtonKey, + onPressed: showSearchDialog, + icon: const Icon(Icons.search_rounded), + tooltip: AppLocalizations.of(context)!.search, + ), + PopupMenuButton( + icon: const Icon(Icons.sort_rounded), + itemBuilder: (context) => [ + PopupMenuItem( + onTap: () => setState(() => _sortingOptions = _SortingOptions.highestToLowest), + child: Row( + children: [ + const Icon(Icons.arrow_downward_rounded), + const SizedBox(width: 8), + Expanded( + child: Text(AppLocalizations.of(context)!.fromHighestToLowest) + ), + const SizedBox(width: 16), + Icon( + _sortingOptions == _SortingOptions.highestToLowest + ? Icons.radio_button_checked_rounded + : Icons.radio_button_unchecked_rounded, + color: _sortingOptions == _SortingOptions.highestToLowest + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onSurfaceVariant, + ) + ], + ) + ), + PopupMenuItem( + onTap: () => setState(() => _sortingOptions = _SortingOptions.lowestToHighest), + child: Row( + children: [ + const Icon(Icons.arrow_upward_rounded), + const SizedBox(width: 8), + Expanded( + child: Text(AppLocalizations.of(context)!.fromLowestToHighest) + ), + const SizedBox(width: 16), + Icon( + _sortingOptions == _SortingOptions.lowestToHighest + ? Icons.radio_button_checked_rounded + : Icons.radio_button_unchecked_rounded, + color: _sortingOptions == _SortingOptions.lowestToHighest + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onSurfaceVariant, + ) + ], + ) + ), + ], + ), + const SizedBox(width: 8) + ], + ) + ) + ], + body: SafeArea( + top: false, + bottom: false, + child: Builder( + builder: (context) => CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + ), + if (sortedValues.isEmpty) Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + AppLocalizations.of(context)!.noItemsSearch, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + ), + if (sortedValues.isNotEmpty) SliverPadding( + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewPadding.bottom), + sliver: SliverList.builder( + itemCount: sortedValues.length, + itemBuilder: (context, index) => _Item( + data: sortedValues[index], + isClient: widget.isClient, + options: widget.options, + total: total, + withProgressBar: widget.withProgressBar, + onTapEntry: widget.onTapEntry, + buildValue: widget.buildValue, + ), + ), + ), + ], + ), + ) + ) + ), + ); + } + else { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500 + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + flex: 1, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.clear_rounded), + tooltip: AppLocalizations.of(context)!.close, + ), + ], + ), + ), + Expanded( + flex: 1, + child: TextField( + controller: searchController, + onChanged: search, + decoration: InputDecoration( + filled: true, + fillColor: Theme.of(context).colorScheme.primary.withOpacity(0.1), + hintText: AppLocalizations.of(context)!.search, + prefixIcon: const Icon(Icons.search_rounded), + contentPadding: const EdgeInsets.only(left: 14, bottom: 9, top: 11), + focusedBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.transparent), + borderRadius: BorderRadius.circular(25.7), + ), + enabledBorder: UnderlineInputBorder( + borderSide: const BorderSide(color: Colors.transparent), + borderRadius: BorderRadius.circular(25.7), + ), + ), + ), + ), + ], + ), + ), + Expanded( + child: sortedValues.isNotEmpty ? ListView.builder( + itemCount: sortedValues.length, + itemBuilder: (context, index) => _Item( + data: sortedValues[index], + isClient: widget.isClient, + options: widget.options, + withProgressBar: widget.withProgressBar, + onTapEntry: widget.onTapEntry, + buildValue: widget.buildValue, + total: total, + ), + ) : Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + AppLocalizations.of(context)!.noItemsSearch, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + ), + ), + ], + ), + ), + ); + } + } +} + +class _Item extends StatelessWidget { + final dynamic data; + final bool? isClient; + final List Function(dynamic) options; + final bool withProgressBar; + final void Function(dynamic)? onTapEntry; + final String Function(dynamic) buildValue; + final double total; + + const _Item({ + required this.data, + required this.isClient, + required this.options, + required this.withProgressBar, + required this.onTapEntry, + required this.buildValue, + required this.total, + }); + + @override + Widget build(BuildContext context) { + final statusProvider = Provider.of(context); + + String? name; + if (isClient != null && isClient == true) { + try { + name = statusProvider.serverStatus!.clients.firstWhere((c) => c.ids.contains(data.keys.toList()[0])).name; + } catch (e) { + // ---- // + } + } + + return OptionsMenu( + options: options, + value: data.keys.toList()[0], + onTap: onTapEntry != null + ? (v) { + onTapEntry!(v); + Navigator.pop(context); + } + : null, + child: CustomListTile( + title: data.keys.toList()[0], + trailing: Text( + buildValue(data.values.toList()[0]), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ), + subtitleWidget: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (name != null) ...[ + Text( + name, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurface + ), + ), + const SizedBox(height: 5), + ], + if (withProgressBar == true) Row( + children: [ + SizedBox( + width: 50, + child: Text( + "${doubleFormat((data.values.toList()[0]/total*100), Platform.localeName)}%", + style: TextStyle( + color: Theme.of(context).listTileTheme.textColor + ), + ), + ), + const SizedBox(width: 10), + Flexible( + child: LinearPercentIndicator( + animation: true, + lineHeight: 4, + animationDuration: 500, + curve: Curves.easeOut, + percent: data.values.toList()[0]/total, + barRadius: const Radius.circular(5), + progressColor: Theme.of(context).colorScheme.primary, + backgroundColor: Theme.of(context).colorScheme.surfaceTint.withOpacity(0.2), + ), + ), + const SizedBox(width: 10), + ], + ), + ], + ) + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/home/top_items/top_items_section.dart b/lib/screens/home/top_items/top_items_section.dart new file mode 100644 index 0000000..708a3d0 --- /dev/null +++ b/lib/screens/home/top_items/top_items_section.dart @@ -0,0 +1,347 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/screens/home/top_items/row_item.dart'; +import 'package:adguard_home_manager/screens/home/top_items/top_items_screen.dart'; +import 'package:adguard_home_manager/widgets/custom_pie_chart.dart'; + +import 'package:adguard_home_manager/functions/number_format.dart'; +import 'package:adguard_home_manager/models/menu_option.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; + +class TopItemsSection extends StatelessWidget { + final HomeTopItems type; + final String label; + final List> data; + final bool withChart; + final bool withProgressBar; + final String Function(dynamic) buildValue; + final List Function(dynamic) menuOptions; + final void Function(dynamic)? onTapEntry; + + const TopItemsSection({ + super.key, + required this.type, + required this.label, + required this.data, + required this.withChart, + required this.withProgressBar, + required this.buildValue, + required this.menuOptions, + this.onTapEntry, + }); + + @override + Widget build(BuildContext context) { + final colors = [ + Colors.red, + Colors.green, + Colors.blue, + Colors.orange, + Colors.teal, + Colors.grey + ]; + + final width = MediaQuery.of(context).size.width; + + final withChart = type != HomeTopItems.avgUpstreamResponseTime; + + Map ringData() { + Map values = {}; + data.sublist(0, data.length > 5 ? 5 : data.length).forEach((element) { + values = { + ...values, + element.keys.first: element.values.first.toDouble() + }; + }); + if (data.length > 5) { + final int rest = List.from( + data.sublist(5, data.length).map((e) => e.values.first.toInt()) + ).reduce((a, b) => a + b); + values = { + ...values, + AppLocalizations.of(context)!.others: rest.toDouble() + }; + } + return values; + } + + List> lineData() { + List> values = []; + data.sublist(0, data.length > 5 ? 5 : data.length).forEach((element) { + values.add({ + "label": element.keys.first, + "value": element.values.first.toDouble() + }); + }); + if (data.length > 5) { + final int rest = List.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> lineChartData = lineData(); + final mapData = lineChartData.map((e) => e["value"]); + final double total = mapData.isNotEmpty + ? mapData.reduce((a, b) => a + b) + : 0; + + return SizedBox( + child: Column( + children: [ + if (data.isEmpty) _NoData(label: label), + if (data.isNotEmpty && width > 700) Padding( + padding: EdgeInsets.only(bottom: withChart == false ? 16 : 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + if (withChart == true) Expanded( + flex: 1, + child: ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 250 + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: CustomPieChart( + data: ringData(), + colors: colors + ) + ), + ) + ), + Expanded( + flex: 2, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only( + top: 8, + bottom: 16 + ), + child: Text( + label, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500 + ), + ), + ), + _ItemsList( + colors: colors, + data: data, + clients: type == HomeTopItems.recurrentClients, + type: type, + showChart: withChart, + buildValue: buildValue, + menuOptions: menuOptions, + onTapEntry: onTapEntry, + ), + if (withChart == true) OthersRowItem( + items: data, + showColor: true, + ) + ] + ), + ) + ], + ), + ), + if (data.isNotEmpty && width <= 700) ...[ + Text( + label, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface + ), + ), + const SizedBox(height: 8), + if (withChart == true) Padding( + padding: const EdgeInsets.all(16), + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: SizedBox( + height: 20, + child: LayoutBuilder( + builder: (context, constraints) => Row( + children: lineChartData.asMap().entries.map((e) => Tooltip( + message:'${e.value["label"]} (${doubleFormat((e.value["value"]/total)*100, Platform.localeName)}%)', + child: Container( + width: constraints.maxWidth*(e.value["value"]/total), + decoration: BoxDecoration( + color: colors[e.key] + ), + ), + )).toList() + ) + ) + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 8), + child: _ItemsList( + colors: colors, + data: data, + clients: type == HomeTopItems.recurrentClients, + type: type, + showChart: withChart, + buildValue: buildValue, + menuOptions: menuOptions, + onTapEntry: onTapEntry, + ), + ), + OthersRowItem( + items: data, + showColor: withChart, + ), + ], + + if (data.length > 5) ...[ + Padding( + padding: const EdgeInsets.only(right: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => { + if (width > 700) { + showDialog( + context: context, + builder: (context) => TopItemsScreen( + type: type, + title: label, + isClient: type == HomeTopItems.recurrentClients, + data: data, + withProgressBar: withProgressBar, + buildValue: buildValue, + options: menuOptions, + onTapEntry: onTapEntry, + isFullscreen: !(width > 700 || !(Platform.isAndroid | Platform.isIOS)), + ), + ) + } + else { + Navigator.push(context, MaterialPageRoute( + builder: (context) => TopItemsScreen( + type: type, + title: label, + isClient: type == HomeTopItems.recurrentClients, + data: data, + withProgressBar: withProgressBar, + buildValue: buildValue, + options: menuOptions, + onTapEntry: onTapEntry, + isFullscreen: !(width > 700 || !(Platform.isAndroid | Platform.isIOS)), + ), + )) + } + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(AppLocalizations.of(context)!.viewMore), + const SizedBox(width: 10), + const Icon( + Icons.arrow_forward, + size: 20, + ) + ], + ) + ), + ], + ), + ), + const SizedBox(height: 10), + ] + ], + ), + ); + } +} + +class _ItemsList extends StatelessWidget { + final List colors; + final List> data; + final bool? clients; + final HomeTopItems type; + final bool showChart; + final String Function(dynamic) buildValue; + final List Function(dynamic) menuOptions; + final void Function(dynamic)? onTapEntry; + + const _ItemsList({ + required this.colors, + required this.data, + required this.clients, + required this.type, + required this.showChart, + required this.buildValue, + required this.menuOptions, + this.onTapEntry, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: data.sublist( + 0, data.length > 5 ? 5 : data.length + ).asMap().entries.map((e) => RowItem( + clients: clients ?? false, + domain: e.value.keys.toList()[0], + number: buildValue(e.value.values.toList()[0]), + type: type, + chartColor: colors[e.key], + showColor: showChart, + options: menuOptions, + onTapEntry: onTapEntry, + )).toList() + ); + } +} + +class _NoData extends StatelessWidget { + final String label; + + const _NoData({ + required this.label + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text( + label, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface + ), + ), + const SizedBox(height: 24), + Text( + AppLocalizations.of(context)!.noDataThisSection, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 16), + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/home/top_items_options_modal.dart b/lib/screens/home/top_items_options_modal.dart deleted file mode 100644 index 18c8f9e..0000000 --- a/lib/screens/home/top_items_options_modal.dart +++ /dev/null @@ -1,84 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -import 'package:adguard_home_manager/widgets/custom_list_tile_dialog.dart'; - -class TopItemsOptionsModal extends StatelessWidget { - final bool? isBlocked; - final void Function(String status)? changeStatus; - final void Function() copyToClipboard; - final String type; - - const TopItemsOptionsModal({ - Key? key, - this.isBlocked, - this.changeStatus, - required this.copyToClipboard, - required this.type - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return AlertDialog( - contentPadding: const EdgeInsets.symmetric(vertical: 16), - title: Column( - children: [ - Icon( - Icons.more_horiz, - size: 24, - color: Theme.of(context).listTileTheme.iconColor - ), - const SizedBox(height: 16), - Text( - AppLocalizations.of(context)!.options, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface - ), - ) - ], - ), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (type == 'topQueriedDomains' || type == 'topBlockedDomains') ...[ - if (isBlocked == true && changeStatus != null) CustomListTileDialog( - title: AppLocalizations.of(context)!.unblock, - icon: Icons.check, - onTap: () { - Navigator.pop(context); - changeStatus!('unblock'); - }, - ), - if (isBlocked == false && changeStatus != null) CustomListTileDialog( - title: AppLocalizations.of(context)!.block, - icon: Icons.block, - onTap: () { - Navigator.pop(context); - changeStatus!('block'); - }, - ), - ], - CustomListTileDialog( - title: AppLocalizations.of(context)!.copyClipboard, - icon: Icons.copy, - onTap: () { - Navigator.pop(context); - copyToClipboard(); - } - ), - ], - ), - actions: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(AppLocalizations.of(context)!.cancel) - ) - ], - ) - ], - ); - } -} \ No newline at end of file diff --git a/lib/screens/logs/clients_modal.dart b/lib/screens/logs/clients_modal.dart deleted file mode 100644 index c8c157d..0000000 --- a/lib/screens/logs/clients_modal.dart +++ /dev/null @@ -1,172 +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/providers/logs_provider.dart'; - -class ClientsModal extends StatefulWidget { - final List? value; - - const ClientsModal({ - Key? key, - required this.value - }) : super(key: key); - - @override - State createState() => _ClientsModalState(); -} - -class _ClientsModalState extends State { - List selectedClients = []; - - @override - void initState() { - setState(() => selectedClients = widget.value ?? []); - super.initState(); - } - - @override - Widget build(BuildContext context) { - final logsProvider = Provider.of(context); - - final height = MediaQuery.of(context).size.height; - - void apply() async { - logsProvider.setSelectedClients( - selectedClients.isNotEmpty ? selectedClients : null - ); - - Navigator.pop(context); - } - - Widget listItem({ - required String label, - required void Function() onChanged - }) { - return Material( - color: Colors.transparent, - child: InkWell( - onTap: () => onChanged(), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - label, - style: TextStyle( - fontSize: 16, - color: Theme.of(context).colorScheme.onSurface - ), - ), - Checkbox( - value: selectedClients.contains(label), - onChanged: (_) => onChanged(), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5) - ), - ) - ], - ), - ), - ), - ); - } - - void selectAll() { - setState(() { - selectedClients = logsProvider.clients!.map((item) => item.ip).toList(); - }); - } - - void unselectAll() { - setState(() { - selectedClients = []; - }); - } - - return Container( - height: height >= (logsProvider.clients!.length*64) == true - ? logsProvider.clients!.length*64 - : height-25, - decoration: BoxDecoration( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28) - ), - color: Theme.of(context).dialogBackgroundColor - ), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only( - top: 24, - bottom: 16, - ), - child: Icon( - Icons.smartphone_rounded, - size: 24, - color: Theme.of(context).listTileTheme.iconColor - ), - ), - Text( - AppLocalizations.of(context)!.clients, - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurface - ), - ), - const SizedBox(height: 16), - Expanded( - child: ListView.builder( - physics: height >= (logsProvider.clients!.length*64) == true - ? const NeverScrollableScrollPhysics() - : null, - itemCount: logsProvider.clients!.length, - itemBuilder: (context, index) => listItem( - label: logsProvider.clients![index].ip, - onChanged: () { - if (selectedClients.contains(logsProvider.clients![index].ip)) { - setState(() { - selectedClients = selectedClients.where( - (item) => item != logsProvider.clients![index].ip - ).toList(); - }); - } - else { - setState(() { - selectedClients.add(logsProvider.clients![index].ip); - }); - } - } - ) - ), - ), - Padding( - padding: const EdgeInsets.all(24), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - TextButton( - onPressed: selectedClients.length == logsProvider.clients!.length - ? () => unselectAll() - : () => selectAll(), - child: Text( - selectedClients.length == logsProvider.clients!.length - ? AppLocalizations.of(context)!.unselectAll - : AppLocalizations.of(context)!.selectAll - ) - ), - TextButton( - onPressed: apply, - child: Text(AppLocalizations.of(context)!.apply) - ) - ], - ), - ) - ], - ), - ); - } -} \ No newline at end of file diff --git a/lib/screens/logs/details/log_details_screen.dart b/lib/screens/logs/details/log_details_screen.dart new file mode 100644 index 0000000..5ffe18e --- /dev/null +++ b/lib/screens/logs/details/log_details_screen.dart @@ -0,0 +1,373 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/widgets/section_label.dart'; + +import 'package:adguard_home_manager/screens/logs/details/log_list_tile.dart'; + +import 'package:adguard_home_manager/functions/desktop_mode.dart'; +import 'package:adguard_home_manager/functions/open_url.dart'; +import 'package:adguard_home_manager/functions/copy_clipboard.dart'; +import 'package:adguard_home_manager/constants/urls.dart'; +import 'package:adguard_home_manager/classes/process_modal.dart'; +import 'package:adguard_home_manager/functions/get_filtered_status.dart'; +import 'package:adguard_home_manager/functions/snackbar.dart'; +import 'package:adguard_home_manager/providers/status_provider.dart'; +import 'package:adguard_home_manager/models/logs.dart'; +import 'package:adguard_home_manager/functions/format_time.dart'; +import 'package:adguard_home_manager/models/filtering_status.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; + +class LogDetailsScreen extends StatelessWidget { + final Log log; + final bool dialog; + final bool twoColumns; + + const LogDetailsScreen({ + super.key, + required this.log, + required this.dialog, + required this.twoColumns, + }); + + @override + Widget build(BuildContext context) { + final appConfigProvider = Provider.of(context); + final statusProvider = Provider.of(context); + + final width = MediaQuery.of(context).size.width; + + void blockUnblock(String domain, String newStatus) async { + final ProcessModal processModal = ProcessModal(); + processModal.open(AppLocalizations.of(context)!.savingUserFilters); + + final rules = await statusProvider.blockUnblockDomain( + domain: domain, + newStatus: newStatus + ); + + processModal.close(); + + if (rules == true) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.userFilteringRulesUpdated, + color: Colors.green + ); + } + else { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.userFilteringRulesNotUpdated, + color: Colors.red + ); + } + } + + if (dialog) { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500 + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.clear_rounded) + ), + const SizedBox(width: 16), + Text( + AppLocalizations.of(context)!.logDetails, + style: const TextStyle( + fontSize: 22 + ), + ), + ], + ), + Row( + children: [ + IconButton( + onPressed: () => openUrl("${Urls.googleSearchUrl}?q=${log.question.name}"), + icon: const Icon(Icons.travel_explore_rounded), + tooltip: AppLocalizations.of(context)!.searchDomainInternet + ), + IconButton( + onPressed: log.question.name != null + ? () => blockUnblock( + log.question.name!, + getFilteredStatus(context, appConfigProvider, log.reason, true)['filtered'] == true ? 'unblock' : 'block' + ) + : null, + icon: Icon( + getFilteredStatus(context, appConfigProvider, log.reason, true)['filtered'] == true + ? Icons.check_circle_rounded + : Icons.block + ), + tooltip: getFilteredStatus(context, appConfigProvider, log.reason, true)['filtered'] == true + ? AppLocalizations.of(context)!.unblockDomain + : AppLocalizations.of(context)!.blockDomain, + ), + ], + ) + ], + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: ListView( + children: [ + _Content(log: log) + ] + ) + ), + ) + ], + ), + ), + ); + } + else { + return Scaffold( + body: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverAppBar.large( + pinned: true, + floating: true, + centerTitle: false, + forceElevated: innerBoxIsScrolled, + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, + automaticallyImplyLeading: twoColumns != true, + title: Text(AppLocalizations.of(context)!.logDetails), + actions: [ + IconButton( + onPressed: () => openUrl("${Urls.googleSearchUrl}?q=${log.question.name}"), + icon: const Icon(Icons.travel_explore_rounded), + tooltip: AppLocalizations.of(context)!.searchDomainInternet + ), + if (statusProvider.filteringStatus != null) IconButton( + onPressed: log.question.name != null + ? () => blockUnblock( + log.question.name!, + getFilteredStatus(context, appConfigProvider, log.reason, true)['filtered'] == true ? 'unblock' : 'block' + ) + : null, + icon: Icon( + getFilteredStatus(context, appConfigProvider, log.reason, true)['filtered'] == true + ? Icons.check_circle_rounded + : Icons.block + ), + tooltip: getFilteredStatus(context, appConfigProvider, log.reason, true)['filtered'] == true + ? AppLocalizations.of(context)!.unblockDomain + : AppLocalizations.of(context)!.blockDomain, + ), + const SizedBox(width: 10) + ], + ), + ) + ], + body: SafeArea( + top: false, + child: Builder( + builder: (context) => CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + ), + SliverList.list( + children: [ + _Content(log: log) + ] + ) + ], + ), + ) + ) + ), + ); + } + } +} + +class _Content extends StatelessWidget { + final Log log; + + const _Content({ + required this.log, + }); + + @override + Widget build(BuildContext context) { + final appConfigProvider = Provider.of(context); + final statusProvider = Provider.of(context); + + Widget getResult() { + final filter = getFilteredStatus(context, appConfigProvider, log.reason, true); + return Text( + filter['label'], + style: TextStyle( + color: filter['color'], + fontWeight: FontWeight.w500 + ), + ); + } + + Filter? getList(int id) { + try { + return statusProvider.filteringStatus!.filters.firstWhere((filter) => filter.id == id, orElse: () { + return statusProvider.filteringStatus!.whitelistFilters.firstWhere((filter) => filter.id == id); + }); + } catch (_) { + return null; + } + } + + return Column( + children: [ + SectionLabel(label: AppLocalizations.of(context)!.status), + LogListTile( + icon: Icons.shield_rounded, + title: AppLocalizations.of(context)!.result, + subtitleWidget: getResult(), + trailing: log.cached == true + ? Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 5 + ), + decoration: BoxDecoration( + color: Theme.of(context).floatingActionButtonTheme.backgroundColor, + borderRadius: BorderRadius.circular(30) + ), + child: Text( + "CACHE", + style: TextStyle( + fontSize: 12, + color: Theme.of(context).floatingActionButtonTheme.foregroundColor, + fontWeight: FontWeight.w500 + ), + ), + ) + : null, + ), + if (log.rule != null) LogListTile( + icon: Icons.block, + title: AppLocalizations.of(context)!.blockingRule, + subtitle: log.rule + ), + LogListTile( + icon: Icons.calendar_month_rounded, + title: AppLocalizations.of(context)!.date, + subtitle: convertTimestampLocalTimezone(log.time, 'dd-MM-yyyy') + ), + LogListTile( + icon: Icons.schedule, + title: AppLocalizations.of(context)!.time, + subtitle: convertTimestampLocalTimezone(log.time, 'HH:mm:ss') + ), + SectionLabel(label: AppLocalizations.of(context)!.request), + if (log.question.name != null) LogListTile( + icon: Icons.domain_rounded, + title: AppLocalizations.of(context)!.domain, + subtitle: log.question.name, + onTap: () => copyToClipboard(value: log.question.name!, successMessage: AppLocalizations.of(context)!.domainCopiedClipboard), + ), + LogListTile( + icon: Icons.category_rounded, + title: AppLocalizations.of(context)!.type, + subtitle: log.question.type + ), + LogListTile( + icon: Icons.class_rounded, + title: AppLocalizations.of(context)!.clas, + subtitle: log.question.questionClass + ), + SectionLabel(label: AppLocalizations.of(context)!.response), + if (log.upstream != null && log.upstream != '') LogListTile( + icon: Icons.dns_rounded, + title: AppLocalizations.of(context)!.dnsServer, + subtitle: log.upstream, + onTap: () => copyToClipboard(value: log.upstream!, successMessage: AppLocalizations.of(context)!.dnsServerAddressCopied) + ), + LogListTile( + icon: Icons.timer_rounded, + title: AppLocalizations.of(context)!.elapsedTime, + subtitle: "${double.parse(log.elapsedMs).toStringAsFixed(2)} ms" + ), + if (log.status != null) LogListTile( + icon: Icons.system_update_alt_rounded, + title: AppLocalizations.of(context)!.responseCode, + subtitle: log.status + ), + SectionLabel(label: AppLocalizations.of(context)!.client), + LogListTile( + icon: Icons.smartphone_rounded, + title: AppLocalizations.of(context)!.deviceIp, + subtitle: log.client, + onTap: () => copyToClipboard(value: log.client, successMessage: AppLocalizations.of(context)!.clientIpCopied), + ), + if (log.clientInfo != null && log.clientInfo!.name != '') LogListTile( + icon: Icons.abc_rounded, + title: AppLocalizations.of(context)!.deviceName, + subtitle: log.clientInfo!.name, + onTap: () => copyToClipboard(value: log.clientInfo!.name, successMessage: AppLocalizations.of(context)!.clientNameCopied), + ), + if (log.rules.isNotEmpty) ...[ + SectionLabel(label: AppLocalizations.of(context)!.rules), + ...log.rules.map((rule) { + final Filter? list = getList(rule.filterListId); + if (list != null) { + return LogListTile( + icon: Icons.rule_rounded, + title: rule.text, + subtitle: list.name + ); + } + else { + return const SizedBox(); + } + }) + ], + if (log.answer.isNotEmpty) ...[ + SectionLabel(label: AppLocalizations.of(context)!.answers), + ...log.answer.map((a) => LogListTile( + icon: Icons.download_rounded, + title: a.value, + subtitle: "TTL: ${a.ttl.toString()}", + trailing: Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 5 + ), + decoration: BoxDecoration( + color: Theme.of(context).floatingActionButtonTheme.backgroundColor, + borderRadius: BorderRadius.circular(30) + ), + child: Text( + a.type, + style: TextStyle( + fontSize: 12, + color: Theme.of(context).floatingActionButtonTheme.foregroundColor, + fontWeight: FontWeight.w500 + ), + ), + ) + )) + ] + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/logs/details/log_list_tile.dart b/lib/screens/logs/details/log_list_tile.dart new file mode 100644 index 0000000..15edd6b --- /dev/null +++ b/lib/screens/logs/details/log_list_tile.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; + +class LogListTile extends StatelessWidget { + final IconData icon; + final String title; + final String? subtitle; + final Widget? subtitleWidget; + final Widget? trailing; + final void Function()? onTap; + + const LogListTile({ + super.key, + required this.icon, + required this.title, + this.subtitle, + this.subtitleWidget, + this.trailing, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Row( + children: [ + Icon( + icon, + size: 24, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 16), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface + ), + ), + const SizedBox(height: 3), + subtitleWidget ?? Text( + subtitle!, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).listTileTheme.textColor, + ), + ) + ], + ), + ), + ], + ), + ), + if (trailing != null) ...[ + const SizedBox(width: 16), + trailing! + ] + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/logs/filter_status_modal.dart b/lib/screens/logs/filter_status_modal.dart deleted file mode 100644 index 371f3ab..0000000 --- a/lib/screens/logs/filter_status_modal.dart +++ /dev/null @@ -1,191 +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/providers/logs_provider.dart'; - -class FilterStatusModal extends StatefulWidget { - final String value; - - const FilterStatusModal({ - Key? key, - required this.value - }) : super(key: key); - - @override - State createState() => _FilterStatusModalState(); -} - -class _FilterStatusModalState extends State { - String selectedResultStatus = 'all'; - - @override - void initState() { - setState(() => selectedResultStatus = widget.value); - super.initState(); - } - - @override - Widget build(BuildContext context) { - final logsProvider = Provider.of(context); - - final height = MediaQuery.of(context).size.height; - - void apply() async { - logsProvider.setSelectedResultStatus(selectedResultStatus); - - Navigator.pop(context); - } - - Widget filterStatusListItem({ - required String id, - required IconData icon, - required String label, - required void Function(String?) onChanged - }) { - return Material( - color: Colors.transparent, - child: InkWell( - onTap: () => onChanged(id), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Icon( - icon, - size: 24, - color: Theme.of(context).listTileTheme.iconColor - ), - const SizedBox(width: 16), - Text( - label, - style: TextStyle( - fontSize: 16, - color: Theme.of(context).colorScheme.onSurface - ), - ) - ], - ), - Radio( - value: id, - groupValue: selectedResultStatus, - onChanged: onChanged - ) - ], - ), - ), - ), - ); - } - - return Container( - height: height >= 720 == true - ? 720 - : height-25, - decoration: BoxDecoration( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28) - ), - color: Theme.of(context).dialogBackgroundColor - ), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only( - top: 24, - bottom: 16, - ), - child: Icon( - Icons.shield_rounded, - size: 24, - color: Theme.of(context).listTileTheme.iconColor - ), - ), - Text( - AppLocalizations.of(context)!.responseStatus, - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurface - ), - ), - const SizedBox(height: 16), - Expanded( - child: ListView( - physics: height >= 720 == true - ? const NeverScrollableScrollPhysics() - : null, - children: [ - filterStatusListItem( - id: "all", - icon: Icons.shield_rounded, - label: AppLocalizations.of(context)!.all, - onChanged: (value) => setState(() => selectedResultStatus = value!) - ), - filterStatusListItem( - id: "filtered", - icon: Icons.shield_rounded, - label: AppLocalizations.of(context)!.filtered, - onChanged: (value) => setState(() => selectedResultStatus = value!) - ), - filterStatusListItem( - id: "processed", - icon: Icons.verified_user_rounded, - label: AppLocalizations.of(context)!.processedRow, - onChanged: (value) => setState(() => selectedResultStatus = value!) - ), - filterStatusListItem( - id: "whitelisted", - icon: Icons.verified_user_rounded, - label: AppLocalizations.of(context)!.processedWhitelistRow, - onChanged: (value) => setState(() => selectedResultStatus = value!) - ), - filterStatusListItem( - id: "blocked", - icon: Icons.gpp_bad_rounded, - label: AppLocalizations.of(context)!.blocked, - onChanged: (value) => setState(() => selectedResultStatus = value!) - ), - filterStatusListItem( - id: "blocked_safebrowsing", - icon: Icons.gpp_bad_rounded, - label: AppLocalizations.of(context)!.blockedSafeBrowsingRow, - onChanged: (value) => setState(() => selectedResultStatus = value!) - ), - filterStatusListItem( - id: "blocked_parental", - icon: Icons.gpp_bad_rounded, - label: AppLocalizations.of(context)!.blockedParentalRow, - onChanged: (value) => setState(() => selectedResultStatus = value!) - ), - filterStatusListItem( - id: "safe_search", - icon: Icons.gpp_bad_rounded, - label: AppLocalizations.of(context)!.blockedSafeSearchRow, - onChanged: (value) => setState(() => selectedResultStatus = value!) - ), - - ], - ), - ), - Padding( - padding: const EdgeInsets.all(24), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: apply, - child: Text(AppLocalizations.of(context)!.apply) - ) - ], - ), - ) - ], - ), - ); - } -} \ No newline at end of file diff --git a/lib/screens/logs/filters/clients_modal.dart b/lib/screens/logs/filters/clients_modal.dart new file mode 100644 index 0000000..05b7213 --- /dev/null +++ b/lib/screens/logs/filters/clients_modal.dart @@ -0,0 +1,419 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:segmented_button_slide/segmented_button_slide.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/functions/is_ip.dart'; +import 'package:adguard_home_manager/widgets/list_bottom_sheet.dart'; +import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; + +import 'package:adguard_home_manager/providers/status_provider.dart'; +import 'package:adguard_home_manager/providers/clients_provider.dart'; +import 'package:adguard_home_manager/providers/logs_provider.dart'; + +class _ClientLog { + final String ip; + final String? name; + final List? ids; + + const _ClientLog({ + required this.ip, + required this.name, + this.ids, + }); +} + +class ClientsModal extends StatefulWidget { + final List? value; + final bool dialog; + + const ClientsModal({ + super.key, + required this.value, + required this.dialog + }); + + @override + State createState() => _ClientsModalState(); +} + +class _ClientsModalState extends State { + List<_ClientLog> _filteredClients = []; + final _searchController = TextEditingController(); + int _selectedList = 0; + + @override + void initState() { + final clientsProvider = Provider.of(context, listen: false); + final statusProvider = Provider.of(context, listen: false); + _filteredClients = clientsProvider.clients!.autoClients.map((e) { + String? name; + try { + name = statusProvider.serverStatus!.clients.firstWhere((c) => c.ids.contains(e.ip)).name; + } catch (e) { + // ---- // + } + return _ClientLog( + ip: e.ip, + name: name + ); + }).toList(); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + final clientsProvider = Provider.of(context); + final logsProvider = Provider.of(context); + final statusProvider = Provider.of(context); + + void onSearch({required String value, int? selectedList}) { + if ((selectedList ?? _selectedList) == 1) { + final filtered = clientsProvider.clients!.clients.map((e) { + String? name; + try { + name = statusProvider.serverStatus!.clients.firstWhere((c) => c.ids.contains(e.ids[0])).name; + } catch (e) { + // ---- // + } + return _ClientLog( + ip: e.ids[0], + name: name, + ids: e.ids + ); + }).where( + (c) => c.ip.contains(value.toLowerCase()) || (c.name != null && c.name!.toLowerCase().contains(value.toLowerCase())) + ).toList(); + setState(() => _filteredClients = filtered); + } + else { + final filtered = clientsProvider.clients!.autoClients.map((e) { + String? name; + try { + name = statusProvider.serverStatus!.clients.firstWhere((c) => c.ids.contains(e.ip)).name; + } catch (e) { + // ---- // + } + return _ClientLog( + ip: e.ip, + name: name + ); + }).where( + (c) => c.ip.contains(value.toLowerCase()) || (c.name != null && c.name!.toLowerCase().contains(value.toLowerCase())) + ).toList(); + setState(() => _filteredClients = filtered); + } + } + + void onListChange(int list) { + onSearch(value: _searchController.text, selectedList: list); + } + + void searchAddedClient(_ClientLog client) { + final ips = client.ids?.where((e) => isIpAddress(e) == true).toList(); + if (ips == null || ips.isEmpty) return; + logsProvider.setSearchText(ips[0]); + logsProvider.filterLogs(); + Navigator.of(context).pop(); + Navigator.pop(context); + } + + if (widget.dialog == true) { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500 + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + CloseButton( + onPressed: () => Navigator.pop(context), + ), + const SizedBox(width: 12), + Text( + AppLocalizations.of(context)!.clients, + style: const TextStyle( + fontSize: 22 + ), + ) + ], + ), + ), + Flexible( + child: ListView( + children: [ + _SearchField( + controller: _searchController, + onClear: () => setState(() => _searchController.text = ""), + onSearch: (v) => onSearch(value: v) + ), + 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) + ) + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(16), + child: SegmentedButtonSlide( + entries: [ + SegmentedButtonSlideEntry(icon: Icons.devices, label: AppLocalizations.of(context)!.activeClients), + SegmentedButtonSlideEntry(icon: Icons.add_rounded, label: AppLocalizations.of(context)!.added), + ], + selectedEntry: _selectedList, + onChange: (v) { + onListChange(v); + setState(() => _selectedList = v); + }, + colors: SegmentedButtonSlideColors( + barColor: Theme.of(context).colorScheme.primary.withOpacity(0.2), + backgroundSelectedColor: Theme.of(context).colorScheme.primary, + ), + selectedTextStyle: TextStyle( + color: Theme.of(context).colorScheme.onPrimary, + ), + unselectedTextStyle: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + hoverTextStyle: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + ListView.builder( + primary: false, + shrinkWrap: true, + itemCount: _filteredClients.length, + itemBuilder: (context, index) => _ListItem( + title: _filteredClients[index].ip, + subtitle: _selectedList == 0 ? _filteredClients[index].name : _filteredClients[index].ids?.join(", "), + checkboxActive: logsProvider.selectedClients.contains(_filteredClients[index].ip), + isAddedClient: _selectedList == 0, + onSearchAddedClient: () => searchAddedClient(_filteredClients[index]), + onChanged: (isSelected) { + if (isSelected == true) { + logsProvider.setSelectedClients([ + ...logsProvider.selectedClients, + _filteredClients[index].ip + ]); + } + else { + logsProvider.setSelectedClients( + logsProvider.selectedClients.where( + (item) => item != _filteredClients[index].ip + ).toList() + ); + } + } + ) + ), + ], + ) + ), + if (Platform.isIOS) const SizedBox(height: 16) + ], + ) + ), + ); + } + else { + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: ListBottomSheet( + icon: Icons.smartphone_rounded, + title: AppLocalizations.of(context)!.clients, + children: [ + _SearchField( + controller: _searchController, + onClear: () => setState(() => _searchController.text = ""), + onSearch: (v) => onSearch(value: v) + ), + 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) + ) + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(16), + child: SegmentedButtonSlide( + entries: [ + SegmentedButtonSlideEntry(icon: Icons.devices, label: AppLocalizations.of(context)!.activeClients), + SegmentedButtonSlideEntry(icon: Icons.add_rounded, label: AppLocalizations.of(context)!.added), + ], + selectedEntry: _selectedList, + onChange: (v) { + onListChange(v); + setState(() => _selectedList = v); + }, + colors: SegmentedButtonSlideColors( + barColor: Theme.of(context).colorScheme.primary.withOpacity(0.2), + backgroundSelectedColor: Theme.of(context).colorScheme.primary, + ), + selectedTextStyle: TextStyle( + color: Theme.of(context).colorScheme.onPrimary, + fontWeight: FontWeight.w700 + ), + unselectedTextStyle: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + hoverTextStyle: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + ListView.builder( + shrinkWrap: true, + primary: false, + itemCount: _filteredClients.length, + itemBuilder: (context, index) => _ListItem( + title: _selectedList == 0 ? _filteredClients[index].ip : _filteredClients[index].name ?? "", + subtitle: _selectedList == 0 ? _filteredClients[index].name : _filteredClients[index].ids?.join(", "), + checkboxActive: logsProvider.selectedClients.contains(_filteredClients[index].ip), + isAddedClient: _selectedList == 1, + onSearchAddedClient: _filteredClients[index].ids != null && _filteredClients[index].ids!.where((e) => isIpAddress(e) == true).isNotEmpty ? () => searchAddedClient(_filteredClients[index]) : null, + onChanged: (isSelected) { + if (isSelected == true) { + logsProvider.setSelectedClients([ + ...logsProvider.selectedClients, + _filteredClients[index].ip + ]); + } + else { + logsProvider.setSelectedClients( + logsProvider.selectedClients.where( + (item) => item != _filteredClients[index].ip + ).toList() + ); + } + } + ) + ), + ] + ), + ); + } + } +} + +class _SearchField extends StatelessWidget { + final TextEditingController controller; + final void Function(String) onSearch; + final void Function() onClear; + + const _SearchField({ + required this.controller, + required this.onClear, + required this.onSearch, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: TextFormField( + controller: controller, + onChanged: onSearch, + decoration: InputDecoration( + hintText: AppLocalizations.of(context)!.search, + prefixIcon: const Icon(Icons.search_rounded), + border: InputBorder.none, + filled: true, + fillColor: Colors.grey.withOpacity(0.2), + suffixIcon: controller.text != "" + ? IconButton( + onPressed: onClear, + icon: const Icon( + Icons.close_rounded, + size: 20, + ), + tooltip: AppLocalizations.of(context)!.clearSearch, + ) + : null + ), + ), + ), + ); + } +} + +class _ListItem extends StatelessWidget { + final String title; + final String? subtitle; + final bool checkboxActive; + final void Function(bool) onChanged; + final bool isAddedClient; + final void Function()? onSearchAddedClient; + + const _ListItem({ + required this.title, + this.subtitle, + required this.checkboxActive, + required this.onChanged, + required this.isAddedClient, + required this.onSearchAddedClient, + }); + + @override + Widget build(BuildContext context) { + if (isAddedClient == true) { + return CustomListTile( + title: title, + subtitle: subtitle, + trailing: TextButton( + onPressed: onSearchAddedClient, + child: Text(AppLocalizations.of(context)!.select) + ), + ); + } + else { + return CustomCheckboxListTile( + value: checkboxActive, + onChanged: (v) => onChanged(v), + title: title, + subtitle: subtitle, + padding: const EdgeInsets.only( + left: 24, + top: 8, + right: 12, + bottom: 8 + ), + ); + } + } +} \ No newline at end of file diff --git a/lib/screens/logs/filters/filter_status_modal.dart b/lib/screens/logs/filters/filter_status_modal.dart new file mode 100644 index 0000000..d4e4155 --- /dev/null +++ b/lib/screens/logs/filters/filter_status_modal.dart @@ -0,0 +1,236 @@ + + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/widgets/list_bottom_sheet.dart'; + +import 'package:adguard_home_manager/providers/logs_provider.dart'; + +class FilterStatusModal extends StatefulWidget { + final String value; + final bool dialog; + + const FilterStatusModal({ + super.key, + required this.value, + required this.dialog + }); + + @override + State createState() => _FilterStatusModalState(); +} + +class _FilterStatusModalState extends State { + String selectedResultStatus = 'all'; + + @override + void initState() { + setState(() => selectedResultStatus = widget.value); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final logsProvider = Provider.of(context); + + if (widget.dialog == true) { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 400 + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: SingleChildScrollView( + child: Wrap( + children: [ + Padding( + padding: const EdgeInsets.only(top: 16, left: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + CloseButton( + onPressed: () => Navigator.pop(context), + ), + const SizedBox(width: 12), + Text( + AppLocalizations.of(context)!.responseStatus, + style: const TextStyle( + fontSize: 22 + ), + ) + ], + ), + ), + Container(height: 16), + _ItemsList( + selectedResultStatus: logsProvider.selectedResultStatus, + updateSelectedResultStatus: (v) => logsProvider.setSelectedResultStatus(value: v), + ), + Container(height: 16) + ], + ), + ), + ), + ], + ) + ), + ); + } + else { + return SizedBox( + height: 700, + child: ListBottomSheet( + icon: Icons.shield_rounded, + title: AppLocalizations.of(context)!.responseStatus, + initialChildSize: 1, + minChildSize: 0.5, + children: [ + _ItemsList( + selectedResultStatus: logsProvider.selectedResultStatus, + updateSelectedResultStatus: (v) => logsProvider.setSelectedResultStatus(value: v), + ) + ] + ), + ); + } + } +} + +class _ItemsList extends StatelessWidget { + final String selectedResultStatus; + final void Function(String) updateSelectedResultStatus; + + const _ItemsList({ + required this.selectedResultStatus, + required this.updateSelectedResultStatus + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + _Item( + selectedResultStatus: selectedResultStatus, + id: "all", + icon: Icons.shield_rounded, + label: AppLocalizations.of(context)!.all, + onChanged: updateSelectedResultStatus + ), + _Item( + selectedResultStatus: selectedResultStatus, + id: "filtered", + icon: Icons.shield_rounded, + label: AppLocalizations.of(context)!.filtered, + onChanged: updateSelectedResultStatus + ), + _Item( + selectedResultStatus: selectedResultStatus, + id: "processed", + icon: Icons.verified_user_rounded, + label: AppLocalizations.of(context)!.processedRow, + onChanged: updateSelectedResultStatus + ), + _Item( + selectedResultStatus: selectedResultStatus, + id: "whitelisted", + icon: Icons.verified_user_rounded, + label: AppLocalizations.of(context)!.processedWhitelistRow, + onChanged: updateSelectedResultStatus + ), + _Item( + selectedResultStatus: selectedResultStatus, + id: "blocked", + icon: Icons.gpp_bad_rounded, + label: AppLocalizations.of(context)!.blocked, + onChanged: updateSelectedResultStatus + ), + _Item( + selectedResultStatus: selectedResultStatus, + id: "blocked_safebrowsing", + icon: Icons.gpp_bad_rounded, + label: AppLocalizations.of(context)!.blockedSafeBrowsingRow, + onChanged: updateSelectedResultStatus + ), + _Item( + selectedResultStatus: selectedResultStatus, + id: "blocked_parental", + icon: Icons.gpp_bad_rounded, + label: AppLocalizations.of(context)!.blockedParentalRow, + onChanged: updateSelectedResultStatus + ), + _Item( + selectedResultStatus: selectedResultStatus, + id: "safe_search", + icon: Icons.gpp_bad_rounded, + label: AppLocalizations.of(context)!.blockedSafeSearchRow, + onChanged: updateSelectedResultStatus + ), + ], + ); + } +} + +class _Item extends StatelessWidget { + final String selectedResultStatus; + final String id; + final IconData icon; + final String label; + final void Function(String) onChanged; + + const _Item({ + required this.selectedResultStatus, + required this.id, + required this.icon, + required this.label, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () => onChanged(id), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Row( + children: [ + Icon( + icon, + size: 24, + color: Theme.of(context).listTileTheme.iconColor + ), + const SizedBox(width: 16), + Flexible( + child: Text( + label, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface + ), + ), + ) + ], + ), + ), + Radio( + value: id, + groupValue: selectedResultStatus, + onChanged: (v) => onChanged(v!) + ) + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/logs/filters/logs_filters_modal.dart b/lib/screens/logs/filters/logs_filters_modal.dart new file mode 100644 index 0000000..b506cff --- /dev/null +++ b/lib/screens/logs/filters/logs_filters_modal.dart @@ -0,0 +1,267 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/screens/logs/filters/clients_modal.dart'; +import 'package:adguard_home_manager/screens/logs/filters/filter_status_modal.dart'; +import 'package:adguard_home_manager/widgets/section_label.dart'; +import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; + +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/providers/clients_provider.dart'; +import 'package:adguard_home_manager/providers/logs_provider.dart'; + +class LogsFiltersModal extends StatelessWidget { + final bool dialog; + + const LogsFiltersModal({ + super.key, + required this.dialog + }); + + @override + Widget build(BuildContext context) { + if (dialog == true) { + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500 + ), + child: const _FiltersList() + ) + ), + ); + } + else { + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).dialogBackgroundColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28) + ) + ), + child: const SafeArea( + child: _FiltersList(), + ) + ), + ); + } + } +} + +class _FiltersList extends StatelessWidget { + const _FiltersList(); + + @override + Widget build(BuildContext context) { + final logsProvider = Provider.of(context); + final clientsProvider = Provider.of(context); + + final width = MediaQuery.of(context).size.width; + + final Map translatedString = { + "all": AppLocalizations.of(context)!.all, + "filtered": AppLocalizations.of(context)!.filtered, + "processed": AppLocalizations.of(context)!.processedRow, + "whitelisted": AppLocalizations.of(context)!.processedWhitelistRow, + "blocked": AppLocalizations.of(context)!.blocked, + "blocked_safebrowsing": AppLocalizations.of(context)!.blockedSafeBrowsingRow, + "blocked_parental": AppLocalizations.of(context)!.blockedParentalRow, + "safe_search": AppLocalizations.of(context)!.blockedSafeSearchRow, + }; + + void openSelectFilterStatus() { + if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + barrierDismissible: false, + context: context, + builder: (context) => FilterStatusModal( + value: logsProvider.selectedResultStatus, + dialog: true, + ), + ); + } + else { + showModalBottomSheet( + context: context, + useRootNavigator: true, + builder: (context) => FilterStatusModal( + value: logsProvider.selectedResultStatus, + dialog: false, + ), + isScrollControlled: true, + backgroundColor: Colors.transparent, + useSafeArea: true + ); + } + } + + void openSelectClients() { + if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + context: context, + builder: (context) => ClientsModal( + value: logsProvider.selectedClients, + dialog: true, + ), + barrierDismissible: false + ); + } + else { + showModalBottomSheet( + context: context, + useRootNavigator: true, + builder: (context) => ClientsModal( + value: logsProvider.selectedClients, + dialog: false, + ), + isScrollControlled: true, + useSafeArea: true, + backgroundColor: Colors.transparent + ); + } + } + + 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, + bottom: 16, + ), + child: Icon( + Icons.filter_list_rounded, + size: 24, + color: Theme.of(context).listTileTheme.iconColor + ), + ), + Text( + AppLocalizations.of(context)!.filters, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w400, + height: 1.3, + color: Theme.of(context).colorScheme.onSurface + ), + ), + const SizedBox(height: 16), + ], + ), + ], + ), + Container(height: 16), + CustomListTile( + title: AppLocalizations.of(context)!.client, + subtitle: logsProvider.selectedClients.isNotEmpty + ? "${logsProvider.selectedClients.length} ${AppLocalizations.of(context)!.clientsSelected}" + : AppLocalizations.of(context)!.all, + onTap: clientsProvider.loadStatus == LoadStatus.loaded + ? openSelectClients + : null, + disabled: clientsProvider.loadStatus != LoadStatus.loaded, + icon: Icons.smartphone_rounded, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + trailing: clientsProvider.loadStatus == LoadStatus.loading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ) + : clientsProvider.loadStatus == LoadStatus.error + ? const Icon( + Icons.error_rounded, + color: Colors.red, + ) + : null, + ), + SectionLabel( + label: AppLocalizations.of(context)!.quickFilters, + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 16 + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + FilterChip( + selected: logsProvider.selectedResultStatus == "all", + label: Text(AppLocalizations.of(context)!.all), + onSelected: (_) => logsProvider.setSelectedResultStatus(value: "all") + ), + FilterChip( + selected: logsProvider.selectedResultStatus == "processed" || + logsProvider.selectedResultStatus == "whitelisted", + label: Text(AppLocalizations.of(context)!.allowed), + onSelected: (_) => logsProvider.setSelectedResultStatus(value: "processed") + ), + FilterChip( + selected: logsProvider.selectedResultStatus == "blocked" || + logsProvider.selectedResultStatus == "blocked_safebrowsing" || + logsProvider.selectedResultStatus == "blocked_parental" || + logsProvider.selectedResultStatus == "safe_search", + label: Text(AppLocalizations.of(context)!.blocked), + onSelected: (_) => logsProvider.setSelectedResultStatus(value: "blocked") + ), + ], + ), + const Padding(padding: EdgeInsets.all(8)), + CustomListTile( + title: AppLocalizations.of(context)!.responseStatus, + subtitle: "${translatedString[logsProvider.selectedResultStatus]}", + onTap: openSelectFilterStatus, + icon: Icons.shield_rounded, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(24), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + onPressed: () { + logsProvider.requestResetFilters(); + }, + child: Text(AppLocalizations.of(context)!.resetFilters) + ), + TextButton( + onPressed: () { + Navigator.pop(context); + logsProvider.filterLogs(); + }, + child: Text(AppLocalizations.of(context)!.apply) + ), + ], + ), + ), + if (Platform.isIOS) const SizedBox(height: 16) + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/logs/live/live_logs_list.dart b/lib/screens/logs/live/live_logs_list.dart new file mode 100644 index 0000000..4bdf02f --- /dev/null +++ b/lib/screens/logs/live/live_logs_list.dart @@ -0,0 +1,93 @@ +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/log_tile.dart'; +import 'package:adguard_home_manager/screens/logs/details/log_details_screen.dart'; + +import 'package:adguard_home_manager/models/logs.dart'; +import 'package:adguard_home_manager/providers/live_logs_provider.dart'; + +class LiveLogsList extends StatelessWidget { + final Log? selectedLog; + final void Function(Log) onLogSelected; + final bool twoColumns; + + const LiveLogsList({ + super.key, + required this.selectedLog, + required this.onLogSelected, + required this.twoColumns, + }); + + @override + Widget build(BuildContext context) { + final liveLogsProvider = Provider.of(context); + + return NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverAppBar.large( + title: Text(AppLocalizations.of(context)!.liveLogs), + ) + ) + ], + body: SafeArea( + top: false, + bottom: false, + child: Builder( + builder: (context) => CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + ), + if (liveLogsProvider.logs.isEmpty) SliverFillRemaining( + child: Center( + child: Padding( + padding: const EdgeInsets.all(16), + child: Text( + AppLocalizations.of(context)!.hereWillAppearRealtimeLogs, + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 22 + ), + ), + ), + ), + ), + if (liveLogsProvider.logs.isNotEmpty) SliverPadding( + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewPadding.bottom), + sliver: SliverList.builder( + itemCount: liveLogsProvider.logs.length, + itemBuilder: (context, index) => LogTile( + log: liveLogsProvider.logs[index], + length: liveLogsProvider.logs.length, + index: index, + onLogTap: (log) { + if (!twoColumns) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => LogDetailsScreen( + log: log, + dialog: false, + twoColumns: twoColumns, + ) + ) + ); + } + onLogSelected(log); + }, + isLogSelected: selectedLog == liveLogsProvider.logs[index], + twoColumns: twoColumns + ), + ), + ) + ], + ), + ) + ) + ); + } +} \ No newline at end of file diff --git a/lib/screens/logs/live/live_logs_screen.dart b/lib/screens/logs/live/live_logs_screen.dart new file mode 100644 index 0000000..5c35e1a --- /dev/null +++ b/lib/screens/logs/live/live_logs_screen.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'package:adguard_home_manager/screens/logs/details/log_details_screen.dart'; +import 'package:adguard_home_manager/screens/logs/live/live_logs_list.dart'; + +import 'package:adguard_home_manager/models/logs.dart'; +import 'package:adguard_home_manager/providers/live_logs_provider.dart'; + +class LiveLogsScreen extends StatefulWidget { + const LiveLogsScreen({super.key}); + + @override + State createState() => _LiveLogsScreenState(); +} + +class _LiveLogsScreenState extends State { + Log? _selectedLog; + + @override + void initState() { + Provider.of(context, listen: false).startFetchLogs(); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth > 800) { + return Material( + color: Colors.transparent, + child: Row( + children: [ + Expanded( + flex: 2, + child: LiveLogsList( + twoColumns: true, + selectedLog: _selectedLog, + onLogSelected: (log) => setState(() => _selectedLog = log), + ) + ), + Expanded( + flex: 3, + child: _selectedLog != null + ? LogDetailsScreen( + log: _selectedLog!, + dialog: false, + twoColumns: true, + ) + : const SizedBox() + ) + ], + ), + ); + } + else { + return LiveLogsList( + twoColumns: false, + selectedLog: _selectedLog, + onLogSelected: (log) => setState(() => _selectedLog = log), + ); + } + }, + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/logs/log_details_screen.dart b/lib/screens/logs/log_details_screen.dart deleted file mode 100644 index 5386d0c..0000000 --- a/lib/screens/logs/log_details_screen.dart +++ /dev/null @@ -1,253 +0,0 @@ -// ignore_for_file: use_build_context_synchronously - -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -import 'package:adguard_home_manager/widgets/section_label.dart'; - -import 'package:adguard_home_manager/screens/logs/log_list_tile.dart'; - -import 'package:adguard_home_manager/classes/process_modal.dart'; -import 'package:adguard_home_manager/functions/get_filtered_status.dart'; -import 'package:adguard_home_manager/models/logs.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; -import 'package:adguard_home_manager/functions/format_time.dart'; -import 'package:adguard_home_manager/models/filtering_status.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; -import 'package:adguard_home_manager/providers/app_config_provider.dart'; - -class LogDetailsScreen extends StatelessWidget { - final Log log; - - const LogDetailsScreen({ - Key? key, - required this.log - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final serversProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - - Filter? getList(int id) { - try { - return serversProvider.filteringStatus!.filters.firstWhere((filter) => filter.id == id, orElse: () { - return serversProvider.filteringStatus!.whitelistFilters.firstWhere((filter) => filter.id == id); - }); - } catch (_) { - return null; - } - } - - Widget getResult() { - final filter = getFilteredStatus(context, appConfigProvider, log.reason, true); - return Text( - filter['label'], - style: TextStyle( - color: filter['color'], - fontWeight: FontWeight.w500 - ), - ); - } - - void blockUnblock(Log log, String newStatus) async { - final ProcessModal processModal = ProcessModal(context: context); - processModal.open(AppLocalizations.of(context)!.savingUserFilters); - - final rules = await getFilteringRules(server: serversProvider.selectedServer!); - - if (rules['result'] == 'success') { - FilteringStatus oldStatus = serversProvider.filteringStatus!; - - List newRules = rules['data'].userRules.where((domain) => !domain.contains(log.question.name)).toList(); - if (newStatus == 'block') { - newRules.add("||${log.question.name}^"); - } - else if (newStatus == 'unblock') { - newRules.add("@@||${log.question.name}^"); - } - FilteringStatus newObj = serversProvider.filteringStatus!; - newObj.userRules = newRules; - serversProvider.setFilteringStatus(newObj); - - final result = await postFilteringRules(server: serversProvider.selectedServer!, data: {'rules': newRules}); - - processModal.close(); - - if (result['result'] == 'success') { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.userFilteringRulesUpdated), - backgroundColor: Colors.green, - ) - ); - } - else { - appConfigProvider.addLog(result['log']); - serversProvider.setFilteringStatus(oldStatus); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.userFilteringRulesNotUpdated), - backgroundColor: Colors.red, - ) - ); - } - } - else { - appConfigProvider.addLog(rules['log']); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.userFilteringRulesNotUpdated), - backgroundColor: Colors.red, - ) - ); - } - } - - return Scaffold( - appBar: AppBar( - title: Text(AppLocalizations.of(context)!.logDetails), - actions: [ - IconButton( - onPressed: () => blockUnblock(log, getFilteredStatus(context, appConfigProvider, log.reason, true)['filtered'] == true ? 'unblock' : 'block'), - icon: Icon( - getFilteredStatus(context, appConfigProvider, log.reason, true)['filtered'] == true - ? Icons.check_circle_rounded - : Icons.block - ), - tooltip: getFilteredStatus(context, appConfigProvider, log.reason, true)['filtered'] == true - ? AppLocalizations.of(context)!.unblockDomain - : AppLocalizations.of(context)!.blockDomain, - ), - const SizedBox(width: 10) - ], - ), - body: ListView( - children: [ - SectionLabel(label: AppLocalizations.of(context)!.status), - LogListTile( - icon: Icons.shield_rounded, - title: AppLocalizations.of(context)!.result, - subtitleWidget: getResult(), - trailing: log.cached == true - ? Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 5 - ), - decoration: BoxDecoration( - color: Theme.of(context).floatingActionButtonTheme.backgroundColor, - borderRadius: BorderRadius.circular(30) - ), - child: Text( - "CACHE", - style: TextStyle( - fontSize: 12, - color: Theme.of(context).floatingActionButtonTheme.foregroundColor, - fontWeight: FontWeight.w500 - ), - ), - ) - : null, - ), - if (log.rule != null) LogListTile( - icon: Icons.block, - title: AppLocalizations.of(context)!.blockingRule, - subtitle: log.rule - ), - LogListTile( - icon: Icons.schedule, - title: AppLocalizations.of(context)!.time, - subtitle: convertTimestampLocalTimezone(log.time, 'HH:mm:ss') - ), - SectionLabel(label: AppLocalizations.of(context)!.request), - LogListTile( - icon: Icons.domain_rounded, - title: AppLocalizations.of(context)!.domain, - subtitle: log.question.name - ), - LogListTile( - icon: Icons.category_rounded, - title: AppLocalizations.of(context)!.type, - subtitle: log.question.type - ), - LogListTile( - icon: Icons.class_rounded, - title: AppLocalizations.of(context)!.clas, - subtitle: log.question.questionClass - ), - SectionLabel(label: AppLocalizations.of(context)!.response), - if (log.upstream != '') LogListTile( - icon: Icons.dns_rounded, - title: AppLocalizations.of(context)!.dnsServer, - subtitle: log.upstream - ), - LogListTile( - icon: Icons.timer_rounded, - title: AppLocalizations.of(context)!.elapsedTime, - subtitle: "${double.parse(log.elapsedMs).toStringAsFixed(2)} ms" - ), - LogListTile( - icon: Icons.system_update_alt_rounded, - title: AppLocalizations.of(context)!.responseCode, - subtitle: log.status - ), - SectionLabel(label: AppLocalizations.of(context)!.client), - LogListTile( - icon: Icons.smartphone_rounded, - title: AppLocalizations.of(context)!.deviceIp, - subtitle: log.client - ), - if (log.clientInfo != null && log.clientInfo!.name != '') LogListTile( - icon: Icons.abc_rounded, - title: AppLocalizations.of(context)!.deviceName, - subtitle: log.clientInfo!.name - ), - if (log.rules.isNotEmpty) ...[ - SectionLabel(label: AppLocalizations.of(context)!.rules), - ...log.rules.map((rule) { - final Filter? list = getList(rule.filterListId); - if (list != null) { - return LogListTile( - icon: Icons.rule_rounded, - title: rule.text, - subtitle: list.name - ); - } - else { - return const SizedBox(); - } - }).toList() - ], - if (log.answer.isNotEmpty) ...[ - SectionLabel(label: AppLocalizations.of(context)!.answers), - ...log.answer.map((a) => LogListTile( - icon: Icons.download_rounded, - title: a.value, - subtitle: "TTL: ${a.ttl.toString()}", - trailing: Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 5 - ), - decoration: BoxDecoration( - color: Theme.of(context).floatingActionButtonTheme.backgroundColor, - borderRadius: BorderRadius.circular(30) - ), - child: Text( - a.type, - style: TextStyle( - fontSize: 12, - color: Theme.of(context).floatingActionButtonTheme.foregroundColor, - fontWeight: FontWeight.w500 - ), - ), - ) - )).toList() - ] - ], - ), - ); - } -} \ No newline at end of file diff --git a/lib/screens/logs/log_list_tile.dart b/lib/screens/logs/log_list_tile.dart deleted file mode 100644 index c45f0f8..0000000 --- a/lib/screens/logs/log_list_tile.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:flutter/material.dart'; - -class LogListTile extends StatelessWidget { - final IconData icon; - final String title; - final String? subtitle; - final Widget? subtitleWidget; - final Widget? trailing; - - const LogListTile({ - Key? key, - required this.icon, - required this.title, - this.subtitle, - this.subtitleWidget, - this.trailing, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Row( - children: [ - Icon( - icon, - size: 24, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - const SizedBox(width: 16), - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurface - ), - ), - const SizedBox(height: 3), - subtitleWidget ?? Text( - subtitle!, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).listTileTheme.textColor, - ), - ) - ], - ), - ), - ], - ), - ), - if (trailing != null) ...[ - const SizedBox(width: 16), - trailing! - ] - ], - ), - ); - } -} \ No newline at end of file diff --git a/lib/screens/logs/log_tile.dart b/lib/screens/logs/log_tile.dart index ff7b058..dc33048 100644 --- a/lib/screens/logs/log_tile.dart +++ b/lib/screens/logs/log_tile.dart @@ -1,16 +1,21 @@ -// ignore_for_file: use_build_context_synchronously import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:adguard_home_manager/screens/home/top_items_options_modal.dart'; -import 'package:adguard_home_manager/screens/logs/log_details_screen.dart'; +import 'package:adguard_home_manager/screens/clients/client/client_screen_functions.dart'; +import 'package:adguard_home_manager/screens/clients/client/client_screen.dart'; +import 'package:adguard_home_manager/widgets/options_menu.dart'; +import 'package:adguard_home_manager/providers/status_provider.dart'; +import 'package:adguard_home_manager/providers/filtering_provider.dart'; +import 'package:adguard_home_manager/classes/process_modal.dart'; +import 'package:adguard_home_manager/functions/snackbar.dart'; import 'package:adguard_home_manager/functions/copy_clipboard.dart'; -import 'package:adguard_home_manager/functions/block_unblock_domain.dart'; +import 'package:adguard_home_manager/models/clients.dart'; +import 'package:adguard_home_manager/providers/clients_provider.dart'; +import 'package:adguard_home_manager/models/menu_option.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; import 'package:adguard_home_manager/functions/get_filtered_status.dart'; -import 'package:adguard_home_manager/functions/snackbar.dart'; import 'package:adguard_home_manager/models/logs.dart'; import 'package:adguard_home_manager/functions/format_time.dart'; @@ -18,19 +23,28 @@ class LogTile extends StatelessWidget { final Log log; final int length; final int index; + final bool? isLogSelected; + final void Function(Log) onLogTap; + final bool? useAlwaysNormalTile; + final bool twoColumns; const LogTile({ - Key? key, + super.key, required this.log, required this.length, - required this.index - }) : super(key: key); + required this.index, + this.isLogSelected, + required this.onLogTap, + this.useAlwaysNormalTile, + required this.twoColumns, + }); @override Widget build(BuildContext context) { final appConfigProvider = Provider.of(context); - - final width = MediaQuery.of(context).size.width; + final statusProvider = Provider.of(context); + final clientsProvider = Provider.of(context); + final filteringProvider = Provider.of(context); Widget logStatusWidget({ required IconData icon, @@ -38,7 +52,7 @@ class LogTile extends StatelessWidget { required String text }) { return SizedBox( - width: 70, + width: 80, child: Column( children: [ Icon( @@ -69,159 +83,472 @@ class LogTile extends StatelessWidget { ); } - void changeBlockStatus(String status) async { - final result = await blockUnblock(context, log.question.name, status); - showSnacbkar( - context: context, - appConfigProvider: appConfigProvider, - label: result['message'], - color: result['success'] == true ? Colors.green : Colors.red + String logClient() { + if (appConfigProvider.showIpLogs == true) { + return log.client; + } + else if (log.clientInfo != null && log.clientInfo!.name != "") { + return log.clientInfo!.name; + } + else { + return log.client; + } + } + + void blockUnblock({required String domain, required String newStatus}) async { + if (!context.mounted) return; + + final ProcessModal processModal = ProcessModal(); + processModal.open(AppLocalizations.of(context)!.savingUserFilters); + + final rules = await statusProvider.blockUnblockDomain( + domain: domain, + newStatus: newStatus + ); + + processModal.close(); + + if (!context.mounted) return; + if (rules == true) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.userFilteringRulesUpdated, + color: Colors.green + ); + } + else { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.userFilteringRulesNotUpdated, + color: Colors.red + ); + } + } + + void confirmAddClient(Client client) async { + if (!context.mounted) return; + + ProcessModal processModal = ProcessModal(); + processModal.open(AppLocalizations.of(context)!.addingClient); + + final result = await clientsProvider.addClient(client); + + processModal.close(); + + if (!context.mounted) return; + + if (result == true) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.clientAddedSuccessfully, + color: Colors.green + ); + } + else { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.clientNotAdded, + color: Colors.red + ); + } + } + + void blockUnblockRuleClient() async { + if (!context.mounted) return; + + ProcessModal processModal = ProcessModal(); + processModal.open(AppLocalizations.of(context)!.addingRule); + + final rule = isDomainBlocked(log.reason) == true + ? "@@||${log.question.name}^\$client='${log.client}'" + : "||${log.question.name}^\$client='${log.client}'"; + + final result = await filteringProvider.addCustomRule(rule); + + processModal.close(); + + if (!context.mounted) return; + if (result == true) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: isDomainBlocked(log.reason) == true + ? AppLocalizations.of(context)!.domainUnblockedThisClient(log.question.name!) + : AppLocalizations.of(context)!.domainBlockedThisClient(log.question.name!), + color: Colors.green + ); + } + else { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.ruleNotAdded, + color: Colors.red + ); + } + } + + void allowDisallowClient() async { + ProcessModal processModal = ProcessModal(); + processModal.open( + log.clientInfo!.disallowed == true + ? AppLocalizations.of(context)!.allowingClient + : AppLocalizations.of(context)!.disallowingClient + ); + + final result = await clientsProvider.addClientList( + log.client, + log.clientInfo!.disallowed == true + ? AccessSettingsList.allowed + : AccessSettingsList.disallowed + ); + + processModal.close(); + + if (!context.mounted) return; + if (result.successful == true) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.clientAddedSuccessfully, + color: Colors.green + ); + } + else if (result.successful == false && result.content == 'client_another_list') { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.clientAnotherList, + color: Colors.red + ); + } + else { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.changesNotSaved, + color: Colors.red + ); + } + } + + void openAddClient() { + Future.delayed( + const Duration(milliseconds: 0), + () { + if (!context.mounted) return; + openClientFormModal( + context: context, + width: MediaQuery.of(context).size.width, + onConfirm: confirmAddClient, + initialData: ClientInitialData(name: "Client ${log.client}", ip: log.client) + ); + } ); } - void openOptionsModal(Log log) { - showDialog( - context: context, - builder: (context) => TopItemsOptionsModal( - isBlocked: getFilteredStatus(context, appConfigProvider, log.reason, false)['color'] == Colors.red - ? true : false, - changeStatus: changeBlockStatus, - copyToClipboard: () => copyToClipboard( - context: context, - value: log.question.name, - successMessage: AppLocalizations.of(context)!.domainCopiedClipboard - ), - type: 'topQueriedDomains', // topQueriedDomains can also be used here. It's the same - ) - ); - } - - return Material( - color: Colors.transparent, - child: InkWell( - onTap: () => Navigator.push(context, MaterialPageRoute( - builder: (context) => LogDetailsScreen(log: log) - )), - onLongPress: () => openOptionsModal(log), - child: Container( - width: double.maxFinite, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SizedBox( - width: width-130, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - log.question.name, - style: TextStyle( - fontSize: 16, - height: 1.5, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurface - ), - ), - const SizedBox(height: 5), - if (log.client.length <= 15 && appConfigProvider.showNameTimeLogs == false) Row( + final domainBlocked = isDomainBlocked(log.reason); + + if (twoColumns && !(useAlwaysNormalTile == true)) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: InkWell( + borderRadius: BorderRadius.circular(28), + child: OptionsMenu( + onTap: (_) => onLogTap(log), + borderRadius: BorderRadius.circular(28), + options: (_) => [ + if (log.question.name != null) MenuOption( + title: domainBlocked == true + ? AppLocalizations.of(context)!.unblockDomain + : AppLocalizations.of(context)!.blockDomain, + icon: domainBlocked == true + ? Icons.check_rounded + : Icons.block_rounded, + action: () => blockUnblock( + domain: log.question.name!, + newStatus: domainBlocked == true ? 'unblock' : 'block' + ) + ), + if (filteringProvider.filtering != null) MenuOption( + title: domainBlocked == true + ? AppLocalizations.of(context)!.unblockThisClientOnly + : AppLocalizations.of(context)!.blockThisClientOnly, + icon: domainBlocked == true + ? Icons.check_rounded + : Icons.block_rounded, + action: blockUnblockRuleClient + ), + if (log.clientInfo?.name == "") MenuOption( + title: AppLocalizations.of(context)!.addPersistentClient, + icon: Icons.add_rounded, + action: openAddClient + ), + MenuOption( + title: log.clientInfo!.disallowed == true + ? AppLocalizations.of(context)!.allowThisClient + : AppLocalizations.of(context)!.disallowThisClient, + icon: log.clientInfo!.disallowed == true + ? Icons.check_rounded + : Icons.block_rounded, + action: allowDisallowClient + ), + if (log.question.name != null) MenuOption( + title: AppLocalizations.of(context)!.copyClipboard, + icon: Icons.copy_rounded, + action: () => copyToClipboard(value: log.question.name!, successMessage: AppLocalizations.of(context)!.copiedClipboard) + ) + ], + child: Container( + width: double.maxFinite, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(28), + color: isLogSelected == true + ? Theme.of(context).colorScheme.primaryContainer + : null + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Row( + mainAxisSize: MainAxisSize.min, children: [ - ...[ - Icon( - Icons.smartphone_rounded, - size: 16, - color: Theme.of(context).listTileTheme.textColor, - ), - const SizedBox(width: 5), - Flexible( - child: Text( - log.client, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: Theme.of(context).listTileTheme.textColor, - fontSize: 14, - height: 1.4, - fontWeight: FontWeight.w400, + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + log.question.name ?? "N/A", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface, + ), ), - ), - ) - ], - const SizedBox(width: 15), - ...[ - Icon( - Icons.schedule_rounded, - size: 16, - color: Theme.of(context).listTileTheme.textColor, - ), - const SizedBox(width: 5), - Flexible( - child: Text( - convertTimestampLocalTimezone(log.time, 'HH:mm:ss'), - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: Theme.of(context).listTileTheme.textColor, - fontSize: 13 + const SizedBox(height: 5), + if (log.client.length <= 15 && appConfigProvider.showTimeLogs == false) Row( + children: [ + ...[ + Icon( + Icons.smartphone_rounded, + size: 16, + color: Theme.of(context).listTileTheme.textColor, + ), + const SizedBox(width: 8), + Flexible( + child: Text( + logClient(), + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Theme.of(context).listTileTheme.textColor, + fontSize: 14, + height: 1.4, + fontWeight: FontWeight.w400, + ), + ), + ) + ], + const SizedBox(width: 16), + ...[ + Icon( + Icons.schedule_rounded, + size: 16, + color: Theme.of(context).listTileTheme.textColor, + ), + const SizedBox(width: 8), + Flexible( + child: Text( + convertTimestampLocalTimezone(log.time, 'HH:mm:ss'), + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Theme.of(context).listTileTheme.textColor, + fontSize: 13 + ), + ), + ), + ], + ], ), - ), + if (log.client.length > 15 || appConfigProvider.showTimeLogs == true) Column( + children: [ + Row( + children: [ + Icon( + Icons.smartphone_rounded, + size: 16, + color: Theme.of(context).listTileTheme.textColor, + ), + const SizedBox(width: 8), + Flexible( + child: Text( + logClient(), + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Theme.of(context).listTileTheme.textColor, + fontSize: 13 + ), + ), + ) + ], + ), + const SizedBox(height: 10), + Row( + children: [ + Icon( + Icons.schedule_rounded, + size: 16, + color: Theme.of(context).listTileTheme.textColor, + ), + const SizedBox(width: 8), + SizedBox( + child: Text( + convertTimestampLocalTimezone(log.time, 'HH:mm:ss'), + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Theme.of(context).listTileTheme.textColor, + fontSize: 13 + ), + ), + ) + ], + ), + if (appConfigProvider.showTimeLogs == true && log.elapsedMs != '') ...[ + const SizedBox(height: 10), + Row( + children: [ + Icon( + Icons.timer, + size: 16, + color: Theme.of(context).listTileTheme.textColor, + ), + const SizedBox(width: 8), + SizedBox( + child: Text( + "${double.parse(log.elapsedMs).toStringAsFixed(2)} ms", + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Theme.of(context).listTileTheme.textColor, + fontSize: 13 + ), + ), + ) + ], + ), + ], + ], + ), + ], ), - ], + ) ], ), - if (log.client.length > 15 || appConfigProvider.showNameTimeLogs == true) Column( - children: [ - Row( - children: [ + ), + generateLogStatus() + ], + ) + ), + ), + ), + ); + } + else { + return Material( + color: Colors.transparent, + child: OptionsMenu( + onTap: (_) => onLogTap(log), + options: (_) => [ + if (log.question.name != null) MenuOption( + title: domainBlocked == true + ? AppLocalizations.of(context)!.unblockDomain + : AppLocalizations.of(context)!.blockDomain, + icon: domainBlocked == true + ? Icons.check_rounded + : Icons.block_rounded, + action: () => blockUnblock( + domain: log.question.name!, + newStatus: domainBlocked == true ? 'unblock' : 'block' + ) + ), + if (filteringProvider.filtering != null) MenuOption( + title: domainBlocked == true + ? AppLocalizations.of(context)!.unblockThisClientOnly + : AppLocalizations.of(context)!.blockThisClientOnly, + icon: domainBlocked == true + ? Icons.check_rounded + : Icons.block_rounded, + action: blockUnblockRuleClient + ), + if (log.clientInfo?.name == "") MenuOption( + title: AppLocalizations.of(context)!.addPersistentClient, + icon: Icons.add_rounded, + action: openAddClient + ), + MenuOption( + title: log.clientInfo!.disallowed == true + ? AppLocalizations.of(context)!.allowThisClient + : AppLocalizations.of(context)!.disallowThisClient, + icon: log.clientInfo!.disallowed == true + ? Icons.check_rounded + : Icons.block_rounded, + action: allowDisallowClient + ), + if (log.question.name != null) MenuOption( + title: AppLocalizations.of(context)!.copyClipboard, + icon: Icons.copy_rounded, + action: () => copyToClipboard( + value: log.question.name!, + successMessage: AppLocalizations.of(context)!.copiedClipboard + ) + ) + ], + child: Container( + width: double.maxFinite, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + log.question.name ?? "N/A", + style: TextStyle( + fontSize: 16, + height: 1.5, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface + ), + ), + const SizedBox(height: 5), + if (log.client.length <= 15 && appConfigProvider.showTimeLogs == false) Row( + children: [ + ...[ Icon( Icons.smartphone_rounded, size: 16, color: Theme.of(context).listTileTheme.textColor, ), - const SizedBox(width: 15), + const SizedBox(width: 8), Flexible( child: Text( - log.client, + logClient(), overflow: TextOverflow.ellipsis, style: TextStyle( color: Theme.of(context).listTileTheme.textColor, - fontSize: 13 + fontSize: 14, + height: 1.4, + fontWeight: FontWeight.w400, ), ), ) ], - ), - if (appConfigProvider.showNameTimeLogs == true && log.clientInfo!.name != '') ...[ - const SizedBox(height: 10), - Row( - children: [ - Icon( - Icons.badge_rounded, - size: 16, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - const SizedBox(width: 15), - Flexible( - child: Text( - log.clientInfo!.name, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: Theme.of(context).listTileTheme.textColor, - fontSize: 13 - ), - ), - ) - ], - ), - ], - const SizedBox(height: 10), - Row( - children: [ + const SizedBox(width: 16), + ...[ Icon( Icons.schedule_rounded, size: 16, color: Theme.of(context).listTileTheme.textColor, ), - const SizedBox(width: 15), - SizedBox( + const SizedBox(width: 8), + Flexible( child: Text( convertTimestampLocalTimezone(log.time, 'HH:mm:ss'), overflow: TextOverflow.ellipsis, @@ -230,22 +557,23 @@ class LogTile extends StatelessWidget { fontSize: 13 ), ), - ) + ), ], - ), - if (appConfigProvider.showNameTimeLogs == true && log.elapsedMs != '') ...[ - const SizedBox(height: 10), + ], + ), + if (log.client.length > 15 || appConfigProvider.showTimeLogs == true) Column( + children: [ Row( children: [ Icon( - Icons.timer, + Icons.smartphone_rounded, size: 16, color: Theme.of(context).listTileTheme.textColor, ), - const SizedBox(width: 15), - SizedBox( + const SizedBox(width: 8), + Flexible( child: Text( - "${double.parse(log.elapsedMs).toStringAsFixed(2)} ms", + logClient(), overflow: TextOverflow.ellipsis, style: TextStyle( color: Theme.of(context).listTileTheme.textColor, @@ -255,18 +583,66 @@ class LogTile extends StatelessWidget { ) ], ), + const SizedBox(height: 10), + Row( + children: [ + Row( + children: [ + Icon( + Icons.schedule_rounded, + size: 16, + color: Theme.of(context).listTileTheme.textColor, + ), + const SizedBox(width: 8), + SizedBox( + child: Text( + convertTimestampLocalTimezone(log.time, 'HH:mm:ss'), + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Theme.of(context).listTileTheme.textColor, + fontSize: 13 + ), + ), + ) + ], + ), + if (appConfigProvider.showTimeLogs == true && log.elapsedMs != '') ...[ + const SizedBox(width: 16), + Row( + children: [ + Icon( + Icons.timer, + size: 16, + color: Theme.of(context).listTileTheme.textColor, + ), + const SizedBox(width: 8), + SizedBox( + child: Text( + "${double.parse(log.elapsedMs).toStringAsFixed(2)} ms", + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Theme.of(context).listTileTheme.textColor, + fontSize: 13 + ), + ), + ) + ], + ), + ], + ], + ), ], - ], - ), - ], + ), + ], + ), ), - ), - const SizedBox(width: 10), - generateLogStatus() - ], + const SizedBox(width: 10), + generateLogStatus() + ], + ), ), ), - ), - ); + ); + } } } \ No newline at end of file diff --git a/lib/screens/logs/logs.dart b/lib/screens/logs/logs.dart index 3165010..d008297 100644 --- a/lib/screens/logs/logs.dart +++ b/lib/screens/logs/logs.dart @@ -2,534 +2,68 @@ 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/logs_filters_modal.dart'; -import 'package:adguard_home_manager/screens/logs/logs_config_modal.dart'; -import 'package:adguard_home_manager/screens/logs/log_tile.dart'; +import 'package:adguard_home_manager/screens/logs/logs_list.dart'; +import 'package:adguard_home_manager/screens/logs/details/log_details_screen.dart'; -import 'package:adguard_home_manager/functions/snackbar.dart'; -import 'package:adguard_home_manager/classes/process_modal.dart'; -import 'package:adguard_home_manager/models/applied_filters.dart'; -import 'package:adguard_home_manager/providers/logs_provider.dart'; -import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; import 'package:adguard_home_manager/models/logs.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; +import 'package:adguard_home_manager/providers/filtering_provider.dart'; -class Logs extends StatelessWidget { - const Logs({Key? key}) : super(key: key); +class Logs extends StatefulWidget { + const Logs({super.key}); @override - Widget build(BuildContext context) { - final serversProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - final logsProvider = Provider.of(context); - - return LogsWidget( - serversProvider: serversProvider, - appConfigProvider: appConfigProvider, - logsProvider: logsProvider, - selectedResultStatus: logsProvider.appliedFilters.selectedResultStatus, - domainText: logsProvider.appliedFilters.domainText, - ); - } + State createState() => _LogsState(); } -class LogsWidget extends StatefulWidget { - final ServersProvider serversProvider; - final AppConfigProvider appConfigProvider; - final LogsProvider logsProvider; - final String selectedResultStatus; - final String? domainText; - - const LogsWidget({ - Key? key, - required this.serversProvider, - required this.appConfigProvider, - required this.logsProvider, - required this.selectedResultStatus, - required this.domainText, - }) : super(key: key); - - @override - State createState() => _LogsWidgetState(); -} - -class _LogsWidgetState extends State { - late ScrollController scrollController; - - bool isLoadingMore = false; - - bool showDivider = true; - - Future fetchLogs({ - int? inOffset, - bool? loadingMore, - String? responseStatus, - String? domainText, - }) async { - int offst = inOffset ?? widget.logsProvider.offset; - - String resStatus = responseStatus ?? widget.selectedResultStatus; - String? search = domainText ?? widget.domainText; - - if (loadingMore != null && loadingMore == true) { - setState(() => isLoadingMore = true); - } - - final result = await getLogs( - server: widget.serversProvider.selectedServer!, - count: widget.logsProvider.logsQuantity, - offset: offst, - olderThan: widget.logsProvider.logsOlderThan, - responseStatus: resStatus, - search: search - ); - - if (loadingMore != null && loadingMore == true) { - setState(() => isLoadingMore = false); - } - - if (mounted) { - if (result['result'] == 'success') { - widget.logsProvider.setOffset(inOffset != null ? inOffset+widget.logsProvider.logsQuantity : widget.logsProvider.offset+widget.logsProvider.logsQuantity); - if (loadingMore != null && loadingMore == true && widget.logsProvider.logsData != null) { - LogsData newLogsData = result['data']; - newLogsData.data = [...widget.logsProvider.logsData!.data, ...result['data'].data]; - if (widget.logsProvider.appliedFilters.clients != null) { - newLogsData.data = newLogsData.data.where( - (item) => widget.logsProvider.appliedFilters.clients!.contains(item.client) - ).toList(); - } - widget.logsProvider.setLogsData(newLogsData); - } - else { - LogsData newLogsData = result['data']; - if (widget.logsProvider.appliedFilters.clients != null) { - newLogsData.data = newLogsData.data.where( - (item) => widget.logsProvider.appliedFilters.clients!.contains(item.client) - ).toList(); - } - widget.logsProvider.setLogsData(newLogsData); - } - widget.logsProvider.setLoadStatus(1); - } - else { - widget.logsProvider.setLoadStatus(2); - widget.appConfigProvider.addLog(result['log']); - } - } - } - - void fetchFilteringRules() async { - final result = await getFilteringRules(server: widget.serversProvider.selectedServer!); - if (mounted) { - if (result['result'] == 'success') { - widget.serversProvider.setFilteringStatus(result['data']); - } - else { - widget.appConfigProvider.addLog(result['log']); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.couldntGetFilteringStatus), - backgroundColor: Colors.red, - ) - ); - } - } - } - - Future fetchClients() async { - final result = await getClients(widget.serversProvider.selectedServer!); - if (mounted) { - if (result['result'] == 'success') { - widget.logsProvider.setClientsLoadStatus(1); - widget.logsProvider.setClients(result['data'].autoClientsData); - } - else { - widget.logsProvider.setClientsLoadStatus(2); - widget.appConfigProvider.addLog(result['log']); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.couldntGetFilteringStatus), - backgroundColor: Colors.red, - ) - ); - } - } - } - - void scrollListener() { - if (scrollController.position.extentAfter < 500 && isLoadingMore == false) { - fetchLogs(loadingMore: true); - } - if (scrollController.position.pixels > 0) { - setState(() => showDivider = false); - } - else { - setState(() => showDivider = true); - } - } +class _LogsState extends State { + Log? _selectedLog; @override void initState() { - scrollController = ScrollController()..addListener(scrollListener); - fetchLogs(inOffset: 0); - fetchFilteringRules(); - fetchClients(); + Provider.of(context, listen: false).fetchFilters(); super.initState(); } @override Widget build(BuildContext context) { - final serversProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - final logsProvider = Provider.of(context); - - void updateConfig(Map data) async { - ProcessModal processModal = ProcessModal(context: context); - processModal.open(AppLocalizations.of(context)!.updatingSettings); - - final result = await updateQueryLogParameters(server: serversProvider.selectedServer!, data: data); - - processModal.close(); - - if (result['result'] == 'success') { - showSnacbkar( - context: context, - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.logsConfigUpdated, - color: Colors.green - ); - } - else { - appConfigProvider.addLog(result['log']); - - showSnacbkar( - context: context, - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.logsConfigNotUpdated, - color: Colors.red - ); - } - } - - void clearQueries() async { - ProcessModal processModal = ProcessModal(context: context); - processModal.open(AppLocalizations.of(context)!.updatingSettings); - - final result = await clearLogs(server: serversProvider.selectedServer!); - - processModal.close(); - - if (result['result'] == 'success') { - showSnacbkar( - context: context, - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.logsCleared, - color: Colors.green - ); - } - else { - appConfigProvider.addLog(result['log']); - - showSnacbkar( - context: context, - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.logsNotCleared, - color: Colors.red - ); - } - } - - - void openFilersModal() { - showModalBottomSheet( - context: context, - builder: (context) => const LogsFiltersModal(), - backgroundColor: Colors.transparent, - isScrollControlled: true - ); - } - - final Map translatedString = { - "all": AppLocalizations.of(context)!.all, - "filtered": AppLocalizations.of(context)!.filtered, - "processed": AppLocalizations.of(context)!.processedRow, - "whitelisted": AppLocalizations.of(context)!.processedWhitelistRow, - "blocked": AppLocalizations.of(context)!.blocked, - "blocked_safebrowsing": AppLocalizations.of(context)!.blockedSafeBrowsingRow, - "blocked_parental": AppLocalizations.of(context)!.blockedParentalRow, - "safe_search": AppLocalizations.of(context)!.safeSearch, - }; - - Widget generateBody() { - switch (logsProvider.loadStatus) { - case 0: - return SizedBox( - width: double.maxFinite, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, + return LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth > 800) { + return Material( + color: Colors.transparent, + child: Row( children: [ - const CircularProgressIndicator(), - const SizedBox(height: 30), - Text( - AppLocalizations.of(context)!.loadingLogs, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ) - ], - ), - ); - - case 1: - return RefreshIndicator( - onRefresh: () async { - await fetchLogs(inOffset: 0); - }, - child: logsProvider.logsData!.data.isNotEmpty - ? ListView.builder( - controller: scrollController, - padding: const EdgeInsets.only(top: 0), - itemCount: isLoadingMore == true - ? logsProvider.logsData!.data.length+1 - : logsProvider.logsData!.data.length, - itemBuilder: (context, index) { - if (isLoadingMore == true && index == logsProvider.logsData!.data.length) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 20), - child: Center( - child: CircularProgressIndicator(), - ), - ); - } - else { - return LogTile( - log: logsProvider.logsData!.data[index], - index: index, - length: logsProvider.logsData!.data.length, - ); - } - } - ) - : Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - AppLocalizations.of(context)!.noLogsDisplay, - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - if (logsProvider.logsOlderThan != null) Padding( - padding: const EdgeInsets.only( - top: 30, - left: 20, - right: 20 - ), - child: Text( - AppLocalizations.of(context)!.noLogsThatOld, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ), - ] - ), - ) - ); - - case 2: - return SizedBox( - width: double.maxFinite, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon( - Icons.error, - color: Colors.red, - size: 50, - ), - const SizedBox(height: 30), - Text( - AppLocalizations.of(context)!.logsNotLoaded, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ) - ], - ), - ); - - default: - return const SizedBox(); - } - } - - return Scaffold( - appBar: AppBar( - title: Text(AppLocalizations.of(context)!.logs), - actions: [ - logsProvider.loadStatus == 1 - ? IconButton( - onPressed: openFilersModal, - icon: const Icon(Icons.filter_list_rounded) - ) - : const SizedBox(), - IconButton( - onPressed: () => { - showModalBottomSheet( - context: context, - builder: (context) => LogsConfigModal( - onConfirm: updateConfig, - onClear: clearQueries, - ), - backgroundColor: Colors.transparent, - isScrollControlled: true - ) - }, - icon: const Icon(Icons.settings) - ), - const SizedBox(width: 5), - ], - bottom: logsProvider.appliedFilters.domainText != null || logsProvider.appliedFilters.selectedResultStatus != 'all' || logsProvider.appliedFilters.clients != null - ? PreferredSize( - preferredSize: const Size(double.maxFinite, 50), - child: Container( - height: 50, - width: double.maxFinite, - padding: const EdgeInsets.only(bottom: 10), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: showDivider == true - ? Theme.of(context).colorScheme.onSurface.withOpacity(0.1) - : Colors.transparent, - ) + Expanded( + flex: 2, + child: LogsListWidget( + twoColumns: true, + selectedLog: _selectedLog, + onLogSelected: (log) => setState(() => _selectedLog = log), ) ), - child: ListView( - scrollDirection: Axis.horizontal, - children: [ - if (logsProvider.appliedFilters.domainText != null) ...[ - const SizedBox(width: 15), - Chip( - avatar: const Icon( - Icons.link_rounded, - ), - label: Row( - children: [ - Text( - logsProvider.appliedFilters.domainText!, - ), - ], - ), - deleteIcon: const Icon( - Icons.clear, - size: 18, - ), - onDeleted: () { - logsProvider.setAppliedFilters( - AppliedFiters( - selectedResultStatus: logsProvider.appliedFilters.selectedResultStatus, - domainText: null, - clients: logsProvider.appliedFilters.clients - ) - ); - logsProvider.setDomainText(null); - fetchLogs( - inOffset: 0, - domainText: '' - ); - }, - ), - ], - if (logsProvider.appliedFilters.selectedResultStatus != 'all') ...[ - const SizedBox(width: 15), - Chip( - avatar: const Icon( - Icons.shield_rounded, - ), - label: Row( - children: [ - Text( - translatedString[logsProvider.appliedFilters.selectedResultStatus]!, - ), - ], - ), - deleteIcon: const Icon( - Icons.clear, - size: 18, - ), - onDeleted: () { - logsProvider.setAppliedFilters( - AppliedFiters( - selectedResultStatus: 'all', - domainText: logsProvider.appliedFilters.domainText, - clients: logsProvider.appliedFilters.clients - ) - ); - logsProvider.setSelectedResultStatus('all'); - fetchLogs( - inOffset: 0, - responseStatus: 'all' - ); - }, - ), - ], - if (logsProvider.appliedFilters.clients != null) ...[ - const SizedBox(width: 15), - Chip( - avatar: const Icon( - Icons.smartphone_rounded, - ), - label: Row( - children: [ - Text( - logsProvider.appliedFilters.clients!.length == 1 - ? logsProvider.appliedFilters.clients![0] - : "${logsProvider.appliedFilters.clients!.length} ${AppLocalizations.of(context)!.clients}", - ), - ], - ), - deleteIcon: const Icon( - Icons.clear, - size: 18, - ), - onDeleted: () { - logsProvider.setAppliedFilters( - AppliedFiters( - selectedResultStatus: logsProvider.appliedFilters.selectedResultStatus, - domainText: logsProvider.appliedFilters.domainText, - clients: null - ) - ); - logsProvider.setSelectedClients(null); - fetchLogs( - inOffset: 0, - responseStatus: logsProvider.appliedFilters.selectedResultStatus - ); - }, - ), - ], - const SizedBox(width: 15), - ], - ), - ) - ) - : null, - ), - body: generateBody() + Expanded( + flex: 3, + child: _selectedLog != null + ? LogDetailsScreen( + log: _selectedLog!, + dialog: false, + twoColumns: true, + ) + : const SizedBox() + ) + ], + ), + ); + } + else { + return LogsListWidget( + twoColumns: false, + selectedLog: _selectedLog, + onLogSelected: (log) => setState(() => _selectedLog = log), + ); + } + }, ); } } \ No newline at end of file diff --git a/lib/screens/logs/logs_config_modal.dart b/lib/screens/logs/logs_config_modal.dart deleted file mode 100644 index 7fd4a67..0000000 --- a/lib/screens/logs/logs_config_modal.dart +++ /dev/null @@ -1,324 +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/services/http_requests.dart'; -import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; - -class LogsConfigModal extends StatelessWidget { - final void Function(Map) onConfirm; - final void Function() onClear; - - const LogsConfigModal({ - Key? key, - required this.onConfirm, - required this.onClear, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final serversProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - - return LogsConfigModalWidget( - serversProvider: serversProvider, - appConfigProvider: appConfigProvider, - context: context, - onConfirm: onConfirm, - onClear: onClear, - ); - } -} - -class LogsConfigModalWidget extends StatefulWidget { - final ServersProvider serversProvider; - final AppConfigProvider appConfigProvider; - final BuildContext context; - final void Function(Map) onConfirm; - final void Function() onClear; - - const LogsConfigModalWidget({ - Key? key, - required this.serversProvider, - required this.appConfigProvider, - required this.context, - required this.onConfirm, - required this.onClear, - }) : super(key: key); - - @override - State createState() => _LogsConfigModalWidgetState(); -} - -class _LogsConfigModalWidgetState extends State { - bool generalSwitch = false; - bool anonymizeClientIp = false; - String? retentionTime = ""; - - List> retentionItems = []; - - int loadStatus = 0; - - void loadData() async { - final result = await getQueryLogInfo(server: widget.serversProvider.selectedServer!); - - if (mounted) { - if (result['result'] == 'success') { - setState(() { - generalSwitch = result['data']['enabled']; - anonymizeClientIp = result['data']['anonymize_client_ip']; - retentionTime = result['data']['interval'].toString(); - loadStatus = 1; - }); - } - else { - setState(() => loadStatus = 2); - } - } - } - - @override - void initState() { - retentionItems = [ - { - 'label': AppLocalizations.of(widget.context)!.hours6, - 'value': 0.25 - }, - { - 'label': AppLocalizations.of(widget.context)!.hours24, - 'value': 1 - }, - { - 'label': AppLocalizations.of(widget.context)!.days7, - 'value': 7 - }, - { - 'label': AppLocalizations.of(widget.context)!.days30, - 'value': 30 - }, - { - 'label': AppLocalizations.of(widget.context)!.days90, - 'value': 90 - }, - ]; - - loadData(); - super.initState(); - } - - @override - Widget build(BuildContext context) { - - Widget generateBody() { - switch (loadStatus) { - case 0: - return const Center( - child: CircularProgressIndicator(), - ); - - case 1: - return Column( - children: [ - Expanded( - child: ListView( - physics: 420 < MediaQuery.of(context).size.height - ? const NeverScrollableScrollPhysics() - : null, - 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: () => setState(() => generalSwitch = !generalSwitch), - borderRadius: BorderRadius.circular(28), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 8 - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - AppLocalizations.of(context)!.enableLog, - style: const TextStyle( - fontSize: 18, - ), - ), - Switch( - value: generalSwitch, - onChanged: (value) => setState(() => generalSwitch = value), - activeColor: Theme.of(context).colorScheme.primary, - ) - ], - ), - ), - ), - ), - ), - const SizedBox(height: 16), - Material( - color: Colors.transparent, - child: InkWell( - onTap: () => setState(() => anonymizeClientIp = !anonymizeClientIp), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 30), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - AppLocalizations.of(context)!.anonymizeClientIp, - style: const TextStyle( - fontSize: 16 - ), - ), - Switch( - value: anonymizeClientIp, - onChanged: (value) => setState(() => anonymizeClientIp = value), - activeColor: Theme.of(context).colorScheme.primary, - ) - ], - ), - ), - ), - ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: DropdownButtonFormField( - items: retentionItems.map>((Map item) { - return DropdownMenuItem( - value: item['value'].toString(), - child: Text(item['label']), - ); - }).toList(), - value: retentionTime, - onChanged: (value) => setState(() => retentionTime = value), - decoration: InputDecoration( - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - label: Text(AppLocalizations.of(context)!.retentionTime) - ), - borderRadius: BorderRadius.circular(20), - ), - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.all(24), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - TextButton( - onPressed: () { - Navigator.pop(context); - widget.onClear(); - }, - child: Text(AppLocalizations.of(context)!.clearLogs) - ), - Row( - children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(AppLocalizations.of(context)!.cancel) - ), - const SizedBox(width: 20), - TextButton( - onPressed: retentionTime != '' - ? () { - Navigator.pop(context); - widget.onConfirm({ - "enabled": generalSwitch, - "interval": double.parse(retentionTime!), - "anonymize_client_ip": anonymizeClientIp - }); - } - : null, - child: Text( - AppLocalizations.of(context)!.confirm, - style: TextStyle( - color: retentionTime != '' - ? Theme.of(context).colorScheme.primary - : Colors.grey - ), - ) - ), - ], - ) - ], - ), - ) - ], - ); - - case 2: - return SizedBox( - width: double.maxFinite, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon( - Icons.error, - color: Colors.red, - size: 50, - ), - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Text( - AppLocalizations.of(context)!.logSettingsNotLoaded, - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 22, - color: Colors.grey, - ), - ), - ) - ], - ), - ); - - default: - return const SizedBox(); - } - } - - return Container( - height: 420, - decoration: BoxDecoration( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28) - ), - color: Theme.of(context).dialogBackgroundColor - ), - child: generateBody() - ); - } -} \ No newline at end of file diff --git a/lib/screens/logs/logs_filters_modal.dart b/lib/screens/logs/logs_filters_modal.dart deleted file mode 100644 index 9f6dc7f..0000000 --- a/lib/screens/logs/logs_filters_modal.dart +++ /dev/null @@ -1,310 +0,0 @@ -// ignore_for_file: use_build_context_synchronously - -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -import 'package:adguard_home_manager/screens/logs/clients_modal.dart'; -import 'package:adguard_home_manager/screens/logs/filter_status_modal.dart'; -import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; - -import 'package:adguard_home_manager/models/logs.dart'; -import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; -import 'package:adguard_home_manager/models/applied_filters.dart'; -import 'package:adguard_home_manager/providers/logs_provider.dart'; - -class LogsFiltersModal extends StatelessWidget { - const LogsFiltersModal({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final logsProvider = Provider.of(context); - - return LogsFiltersModalWidget( - logsProvider: logsProvider - ); - } -} - -class LogsFiltersModalWidget extends StatefulWidget { - final LogsProvider logsProvider; - - const LogsFiltersModalWidget({ - Key? key, - required this.logsProvider - }) : super(key: key); - - @override - State createState() => _LogsFiltersModalWidgetState(); -} - -class _LogsFiltersModalWidgetState extends State { - TextEditingController domainController = TextEditingController(); - String? domainError; - - @override - void initState() { - domainController.text = widget.logsProvider.domainText ?? ''; - domainError = null; - super.initState(); - } - - @override - Widget build(BuildContext context) { - final logsProvider = Provider.of(context); - final serversProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - - final Map translatedString = { - "all": AppLocalizations.of(context)!.all, - "filtered": AppLocalizations.of(context)!.filtered, - "processed": AppLocalizations.of(context)!.processedRow, - "whitelisted": AppLocalizations.of(context)!.processedWhitelistRow, - "blocked": AppLocalizations.of(context)!.blocked, - "blocked_safebrowsing": AppLocalizations.of(context)!.blockedSafeBrowsingRow, - "blocked_parental": AppLocalizations.of(context)!.blockedParentalRow, - "safe_search": AppLocalizations.of(context)!.blockedSafeSearchRow, - }; - - void resetFilters() async { - setState(() { - domainController.text = ''; - }); - - logsProvider.setLoadStatus(0); - - logsProvider.resetFilters(); - - final result = await getLogs( - server: serversProvider.selectedServer!, - count: logsProvider.logsQuantity - ); - - logsProvider.setAppliedFilters( - AppliedFiters( - selectedResultStatus: 'all', - domainText: null, - clients: null - ) - ); - - if (result['result'] == 'success') { - logsProvider.setLogsData(result['data']); - logsProvider.setLoadStatus(1); - } - else { - appConfigProvider.addLog(result['log']); - logsProvider.setLoadStatus(2); - } - } - - void openSelectFilterStatus() { - showModalBottomSheet( - context: context, - builder: (context) => FilterStatusModal( - value: logsProvider.selectedResultStatus, - ), - isScrollControlled: true, - backgroundColor: Colors.transparent - ); - } - - void openSelectClients() { - showModalBottomSheet( - context: context, - builder: (context) => ClientsModal( - value: logsProvider.selectedClients, - ), - isScrollControlled: true, - backgroundColor: Colors.transparent - ); - } - - void filterLogs() async { - Navigator.pop(context); - - logsProvider.setLoadStatus(0); - - logsProvider.setOffset(0); - - final result = await getLogs( - server: serversProvider.selectedServer!, - count: logsProvider.logsQuantity, - olderThan: logsProvider.logsOlderThan, - responseStatus: logsProvider.selectedResultStatus, - search: logsProvider.domainText, - ); - - logsProvider.setAppliedFilters( - AppliedFiters( - selectedResultStatus: logsProvider.selectedResultStatus, - domainText: logsProvider.domainText, - clients: logsProvider.selectedClients - ) - ); - - if (result['result'] == 'success') { - LogsData newLogsData = result['data']; - if (widget.logsProvider.appliedFilters.clients != null) { - newLogsData.data = newLogsData.data.where( - (item) => widget.logsProvider.appliedFilters.clients!.contains(item.client) - ).toList(); - } - logsProvider.setLogsData(newLogsData); - logsProvider.setLoadStatus(1); - } - else { - appConfigProvider.addLog(result['log']); - logsProvider.setLoadStatus(2); - } - } - - return Padding( - padding: MediaQuery.of(context).viewInsets, - child: Container( - height: 455, - decoration: BoxDecoration( - color: Theme.of(context).dialogBackgroundColor, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28) - ) - ), - child: Column( - children: [ - Expanded( - child: ListView( - physics: 400 < MediaQuery.of(context).size.height - ? const NeverScrollableScrollPhysics() - : null, - children: [ - Padding( - padding: const EdgeInsets.only( - top: 24, - bottom: 16, - ), - child: Icon( - Icons.filter_list_rounded, - size: 24, - color: Theme.of(context).listTileTheme.iconColor - ), - ), - Text( - AppLocalizations.of(context)!.filters, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.w400, - height: 1.3, - color: Theme.of(context).colorScheme.onSurface - ), - ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Row( - children: [ - Expanded( - child: TextFormField( - controller: domainController, - onChanged: (value) { - logsProvider.setDomainText(value); - RegExp domain = RegExp(r'^([a-z0-9|-]+\.)*[a-z0-9|-]+\.[a-z]+$'); - if (value == '' || domain.hasMatch(value) == true) { - setState(() { - domainError = null; - }); - } - else { - setState(() { - domainError = AppLocalizations.of(context)!.invalidDomain; - }); - } - }, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.link_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - labelText: AppLocalizations.of(context)!.domain, - suffixIcon: IconButton( - onPressed: () { - setState(() { - domainController.text = ''; - }); - logsProvider.setDomainText(null); - }, - icon: const Icon(Icons.clear) - ), - errorText: domainError - ), - ), - ) - ], - ), - ), - const SizedBox(height: 16), - CustomListTile( - title: AppLocalizations.of(context)!.client, - subtitle: logsProvider.selectedClients != null - ? "${logsProvider.selectedClients!.length} ${AppLocalizations.of(context)!.clientsSelected}" - : AppLocalizations.of(context)!.all, - onTap: logsProvider.clientsLoadStatus == 1 - ? openSelectClients - : null, - disabled: logsProvider.clientsLoadStatus != 1 , - icon: Icons.smartphone_rounded, - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - trailing: logsProvider.clientsLoadStatus == 0 - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - ), - ) - : logsProvider.clientsLoadStatus == 2 - ? const Icon( - Icons.error_rounded, - color: Colors.red, - ) - : null, - ), - CustomListTile( - title: AppLocalizations.of(context)!.responseStatus, - subtitle: "${translatedString[logsProvider.selectedResultStatus]}", - onTap: openSelectFilterStatus, - icon: Icons.shield_rounded, - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.all(24), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - TextButton( - onPressed: resetFilters, - child: Text(AppLocalizations.of(context)!.resetFilters) - ), - TextButton( - onPressed: domainError == null - ? () => filterLogs() - : null, - child: Text(AppLocalizations.of(context)!.apply) - ), - ], - ), - ) - ], - ), - ), - ); - } -} \ No newline at end of file diff --git a/lib/screens/logs/logs_list.dart b/lib/screens/logs/logs_list.dart new file mode 100644 index 0000000..4281af0 --- /dev/null +++ b/lib/screens/logs/logs_list.dart @@ -0,0 +1,292 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/screens/logs/details/log_details_screen.dart'; +import 'package:adguard_home_manager/screens/logs/log_tile.dart'; +import 'package:adguard_home_manager/screens/logs/logs_list_appbar.dart'; + +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/functions/snackbar.dart'; +import 'package:adguard_home_manager/models/logs.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; +import 'package:adguard_home_manager/providers/clients_provider.dart'; +import 'package:adguard_home_manager/providers/logs_provider.dart'; +import 'package:adguard_home_manager/providers/status_provider.dart'; + +class LogsListWidget extends StatefulWidget { + final Log? selectedLog; + final bool twoColumns; + final void Function(Log) onLogSelected; + + const LogsListWidget({ + super.key, + required this.twoColumns, + required this.selectedLog, + required this.onLogSelected, + }); + + @override + State createState() => _LogsListWidgetState(); +} + +class _LogsListWidgetState extends State { + final _scaffoldMessengerKey = GlobalKey(); + bool showDivider = true; + + void fetchFilteringRules() async { + final appConfigProvider = Provider.of(context, listen: false); + final statusProvider = Provider.of(context, listen: false); + + final result = await statusProvider.getFilteringRules(); + if (mounted && result == false) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.couldntGetFilteringStatus, + color: Colors.red + ); + } + } + + Future fetchClients() async { + final clientsProvider = Provider.of(context, listen: false); + final appConfigProvider = Provider.of(context, listen: false); + + final result = await clientsProvider.fetchClients(); + if (mounted && result == false) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.couldntGetFilteringStatus, + color: Colors.red + ); + } + } + + bool scrollListener(ScrollUpdateNotification scrollNotification) { + final logsProvider = Provider.of(context, listen: false); + + if (scrollNotification.metrics.extentAfter < 500 && logsProvider.isLoadingMore == false) { + logsProvider.fetchLogs(loadingMore: true); + } + if (scrollNotification.metrics.pixels > 0) { + setState(() => showDivider = false); + } + else { + setState(() => showDivider = true); + } + + return false; + } + + @override + void initState() { + final logsProvider = Provider.of(context, listen: false); + + logsProvider.fetchLogs(inOffset: 0); + fetchFilteringRules(); + fetchClients(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final logsProvider = Provider.of(context); + + return ScaffoldMessenger( + key: widget.twoColumns ? _scaffoldMessengerKey : null, + child: Scaffold( + body: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: LogsListAppBar( + innerBoxIsScrolled: innerBoxIsScrolled, + showDivider: showDivider, + ) + ) + ], + body: Builder( + builder: (context) { + switch (logsProvider.loadStatus) { + case LoadStatus.loading: + return SafeArea( + top: false, + bottom: false, + child: Builder( + builder: (context) => CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + ), + SliverFillRemaining( + child: SizedBox( + width: double.maxFinite, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.loadingLogs, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ) + ) + ], + ), + ) + ); + + case LoadStatus.loaded: + return SafeArea( + top: false, + bottom: false, + child: Builder( + builder: (context) => RefreshIndicator( + onRefresh: () async { + await logsProvider.fetchLogs(inOffset: 0); + }, + displacement: 95, + child: NotificationListener( + onNotification: scrollListener, + child: CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + ), + if (logsProvider.logsData!.data.isNotEmpty) SliverList.builder( + itemCount: logsProvider.isLoadingMore + ? logsProvider.logsData!.data.length + 1 + : logsProvider.logsData!.data.length, + itemBuilder: (context, index) { + if (logsProvider.isLoadingMore == true && index == logsProvider.logsData!.data.length) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 20), + child: Center( + child: CircularProgressIndicator(), + ), + ); + } + else if (logsProvider.logsData!.data[index].question.name != null) { + return LogTile( + log: logsProvider.logsData!.data[index], + index: index, + length: logsProvider.logsData!.data.length, + isLogSelected: widget.selectedLog != null && widget.selectedLog == logsProvider.logsData!.data[index], + onLogTap: (log) { + if (!widget.twoColumns) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => LogDetailsScreen( + log: log, + dialog: false, + twoColumns: widget.twoColumns, + ) + ) + ); + } + widget.onLogSelected(log); + }, + twoColumns: widget.twoColumns, + ); + } + else { + return null; + } + } + ), + if (logsProvider.logsData!.data.isEmpty) SliverFillRemaining( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + AppLocalizations.of(context)!.noLogsDisplay, + style: TextStyle( + fontSize: 24, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + if (logsProvider.logsOlderThan != null) Padding( + padding: const EdgeInsets.only( + top: 30, + left: 20, + right: 20 + ), + child: Text( + AppLocalizations.of(context)!.noLogsThatOld, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + ] + ), + ), + ) + ], + ), + ), + ), + ) + ); + + case LoadStatus.error: + return SafeArea( + top: false, + bottom: false, + child: Builder( + builder: (context) => CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + ), + SliverFillRemaining( + child: SizedBox( + width: double.maxFinite, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon( + Icons.error, + color: Colors.red, + size: 50, + ), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.logsNotLoaded, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ) + ) + ], + ), + ) + ); + + default: + return const SizedBox(); + } + }, + ) + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/logs/logs_list_appbar.dart b/lib/screens/logs/logs_list_appbar.dart new file mode 100644 index 0000000..21e03b1 --- /dev/null +++ b/lib/screens/logs/logs_list_appbar.dart @@ -0,0 +1,293 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/screens/logs/live/live_logs_screen.dart'; +import 'package:adguard_home_manager/screens/logs/filters/logs_filters_modal.dart'; +import 'package:adguard_home_manager/widgets/floating_search_bar.dart'; + +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/functions/desktop_mode.dart'; +import 'package:adguard_home_manager/providers/live_logs_provider.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; +import 'package:adguard_home_manager/models/applied_filters.dart'; +import 'package:adguard_home_manager/providers/logs_provider.dart'; + +final GlobalKey _searchButtonKey = GlobalKey(); + +class LogsListAppBar extends StatelessWidget { + final bool innerBoxIsScrolled; + final bool showDivider; + + const LogsListAppBar({ + super.key, + required this.innerBoxIsScrolled, + required this.showDivider, + }); + + @override + Widget build(BuildContext context) { + final logsProvider = Provider.of(context); + + final width = MediaQuery.of(context).size.width; + + void openFilersModal() { + if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + context: context, + builder: (context) => const LogsFiltersModal( + dialog: true, + ), + barrierDismissible: false + ); + } + else { + showModalBottomSheet( + context: context, + useRootNavigator: true, + builder: (context) => const LogsFiltersModal( + dialog: false, + ), + backgroundColor: Colors.transparent, + isScrollControlled: true + ); + } + } + + void showSearchDialog() { + showDialog( + context: context, + builder: (context) => FloatingSearchBar( + existingSearchValue: logsProvider.appliedFilters.searchText ?? "", + searchButtonRenderBox: _searchButtonKey.currentContext?.findRenderObject() as RenderBox?, + onSearchCompleted: (v) { + logsProvider.setAppliedFilters( + AppliedFiters( + selectedResultStatus: logsProvider.appliedFilters.selectedResultStatus, + searchText: v != "" ? v : null, + clients: logsProvider.appliedFilters.clients + ) + ); + logsProvider.filterLogs(); + }, + onSearchFieldUpdated: (v) { + if (v == "") { + logsProvider.setSearchText(null); + return; + } + logsProvider.setSearchText(v); + }, + onSearchFieldCleared: () { + logsProvider.setSearchText(null); + }, + ), + ); + } + + void openLiveLogsScreen() { + if (!context.mounted) return; + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => MultiProvider( + providers: [ + ChangeNotifierProxyProvider( + create: (context) => LiveLogsProvider(), + update: (context, servers, logs) => logs!..update(servers), + ), + ], + child: const LiveLogsScreen() + ) + ) + ); + } + + final Map translatedString = { + "all": AppLocalizations.of(context)!.all, + "filtered": AppLocalizations.of(context)!.filtered, + "processed": AppLocalizations.of(context)!.processedRow, + "whitelisted": AppLocalizations.of(context)!.processedWhitelistRow, + "blocked": AppLocalizations.of(context)!.blocked, + "blocked_safebrowsing": AppLocalizations.of(context)!.blockedSafeBrowsingRow, + "blocked_parental": AppLocalizations.of(context)!.blockedParentalRow, + "safe_search": AppLocalizations.of(context)!.safeSearch, + }; + + return SliverAppBar.large( + pinned: true, + floating: true, + centerTitle: false, + forceElevated: innerBoxIsScrolled, + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, + title: Text(AppLocalizations.of(context)!.logs), + actions: [ + if (!(Platform.isAndroid || Platform.isIOS)) IconButton( + onPressed: () => logsProvider.fetchLogs(inOffset: 0), + icon: const Icon(Icons.refresh_rounded), + tooltip: AppLocalizations.of(context)!.refresh, + ), + if (logsProvider.loadStatus == LoadStatus.loaded) IconButton( + key: _searchButtonKey, + onPressed: showSearchDialog, + icon: const Icon(Icons.search_rounded), + tooltip: AppLocalizations.of(context)!.search, + ), + if (logsProvider.loadStatus == LoadStatus.loaded) PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + onTap: openFilersModal, + child: Row( + children: [ + const Icon(Icons.filter_list_rounded), + const SizedBox(width: 10), + Text(AppLocalizations.of(context)!.filters) + ], + ) + ), + PopupMenuItem( + onTap: openLiveLogsScreen, + child: Row( + children: [ + const Icon(Icons.stream_rounded), + const SizedBox(width: 10), + Text(AppLocalizations.of(context)!.liveLogs) + ], + ) + ), + ], + ), + const SizedBox(width: 8), + ], + bottom: logsProvider.appliedFilters.searchText != null || logsProvider.appliedFilters.selectedResultStatus != 'all' || logsProvider.appliedFilters.clients.isNotEmpty + ? PreferredSize( + preferredSize: const Size(double.maxFinite, 70), + child: Container( + height: 50, + width: double.maxFinite, + padding: const EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: showDivider == true + ? Theme.of(context).colorScheme.onSurface.withOpacity(0.1) + : Colors.transparent, + ) + ) + ), + child: ListView( + scrollDirection: Axis.horizontal, + children: [ + if (logsProvider.appliedFilters.searchText != null) ...[ + const SizedBox(width: 15), + Chip( + avatar: const Icon( + Icons.search_rounded, + ), + label: Row( + children: [ + Text( + logsProvider.appliedFilters.searchText!, + ), + ], + ), + deleteIcon: const Icon( + Icons.clear, + size: 18, + ), + onDeleted: () { + logsProvider.setAppliedFilters( + AppliedFiters( + selectedResultStatus: logsProvider.appliedFilters.selectedResultStatus, + searchText: null, + clients: logsProvider.appliedFilters.clients + ) + ); + logsProvider.setSearchText(null); + logsProvider.fetchLogs( + inOffset: 0, + searchText: '' + ); + }, + ), + ], + if (logsProvider.appliedFilters.selectedResultStatus != 'all') ...[ + const SizedBox(width: 15), + Chip( + avatar: const Icon( + Icons.shield_rounded, + ), + label: Row( + children: [ + Text( + translatedString[logsProvider.appliedFilters.selectedResultStatus]!, + ), + ], + ), + deleteIcon: const Icon( + Icons.clear, + size: 18, + ), + onDeleted: () { + logsProvider.setAppliedFilters( + AppliedFiters( + selectedResultStatus: 'all', + searchText: logsProvider.appliedFilters.searchText, + clients: logsProvider.appliedFilters.clients + ) + ); + logsProvider.setSelectedResultStatus(value: 'all'); + logsProvider.fetchLogs( + inOffset: 0, + responseStatus: 'all' + ); + }, + ), + ], + if (logsProvider.appliedFilters.clients.isNotEmpty) ...[ + const SizedBox(width: 15), + Chip( + avatar: const Icon( + Icons.smartphone_rounded, + ), + label: Row( + children: [ + Text( + logsProvider.appliedFilters.clients.length == 1 + ? logsProvider.appliedFilters.clients[0] + : "${logsProvider.appliedFilters.clients.length} ${AppLocalizations.of(context)!.clients}", + ), + ], + ), + deleteIcon: const Icon( + Icons.clear, + size: 18, + ), + onDeleted: () { + logsProvider.setAppliedFilters( + AppliedFiters( + selectedResultStatus: logsProvider.appliedFilters.selectedResultStatus, + searchText: logsProvider.appliedFilters.searchText, + clients: [] + ) + ); + logsProvider.setSelectedClients(null); + logsProvider.fetchLogs( + inOffset: 0, + responseStatus: logsProvider.appliedFilters.selectedResultStatus + ); + }, + ), + ], + const SizedBox(width: 15), + ], + ), + ) + ) + : null, + ); + } +} + diff --git a/lib/screens/servers/servers.dart b/lib/screens/servers/servers.dart index a8423b3..25bca06 100644 --- a/lib/screens/servers/servers.dart +++ b/lib/screens/servers/servers.dart @@ -1,17 +1,24 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:expandable/expandable.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; +import 'package:adguard_home_manager/widgets/add_server/add_server_functions.dart'; import 'package:adguard_home_manager/widgets/servers_list/servers_list.dart'; -import 'package:adguard_home_manager/widgets/add_server_modal.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; class Servers extends StatefulWidget { - const Servers({Key? key}) : super(key: key); + final double? breakingWidth; + + const Servers({ + super.key, + this.breakingWidth + }); @override State createState() => _ServersState(); @@ -53,45 +60,48 @@ class _ServersState extends State { final serversProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + for (var i = 0; i < serversProvider.serversList.length; i++) { expandableControllerList.add(ExpandableController()); } void openAddServerModal() async { await Future.delayed(const Duration(seconds: 0), (() => { - Navigator.push(context, MaterialPageRoute( - fullscreenDialog: true, - builder: (BuildContext context) => const AddServerModal() - )) + openServerFormModal(context: context, width: width) })); } return Scaffold( appBar: AppBar( title: Text(AppLocalizations.of(context)!.servers), + centerTitle: false, ), - body: Stack( - children: [ - ServersList( - context: context, - controllers: expandableControllerList, - onChange: expandOrContract, - scrollController: scrollController - ), - AnimatedPositioned( - duration: const Duration(milliseconds: 100), - curve: Curves.easeInOut, - bottom: isVisible ? - appConfigProvider.showingSnackbar - ? 70 : 20 - : -70, - right: 20, - child: FloatingActionButton( - onPressed: openAddServerModal, - child: const Icon(Icons.add), + body: SafeArea( + child: Stack( + children: [ + ServersList( + context: context, + controllers: expandableControllerList, + onChange: expandOrContract, + scrollController: scrollController, + breakingWidth: widget.breakingWidth ?? 700, ), - ), - ], + AnimatedPositioned( + duration: const Duration(milliseconds: 100), + curve: Curves.easeInOut, + bottom: isVisible ? + appConfigProvider.showingSnackbar + ? 70 : (Platform.isIOS ? 40 : 20) + : -70, + right: 20, + child: FloatingActionButton( + onPressed: openAddServerModal, + child: const Icon(Icons.add), + ), + ), + ], + ), ), ); } diff --git a/lib/screens/settings/access_settings/access_settings.dart b/lib/screens/settings/access_settings/access_settings.dart index f6db465..9c1e986 100644 --- a/lib/screens/settings/access_settings/access_settings.dart +++ b/lib/screens/settings/access_settings/access_settings.dart @@ -1,158 +1,183 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/screens/settings/access_settings/clients_list.dart'; -import 'package:adguard_home_manager/services/http_requests.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/functions/desktop_mode.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/providers/clients_provider.dart'; -class AccessSettings extends StatelessWidget { - const AccessSettings({Key? key}) : super(key: key); +class AccessSettings extends StatefulWidget { + const AccessSettings({super.key}); @override - Widget build(BuildContext context) { - final serversProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - - return AccessSettingsWidget( - serversProvider: serversProvider, - appConfigProvider: appConfigProvider, - ); - } + State createState() => _AccessSettingsState(); } -class AccessSettingsWidget extends StatefulWidget { - final ServersProvider serversProvider; - final AppConfigProvider appConfigProvider; - - const AccessSettingsWidget({ - Key? key, - required this.serversProvider, - required this.appConfigProvider, - }) : super(key: key); - - @override - State createState() => _AccessSettingsWidgetState(); -} - -class _AccessSettingsWidgetState extends State with TickerProviderStateMixin { - final ScrollController scrollController = ScrollController(); - late TabController tabController; - - Future fetchClients() async { - widget.serversProvider.setClientsLoadStatus(0, false); - final result = await getClients(widget.serversProvider.selectedServer!); - if (mounted) { - if (result['result'] == 'success') { - widget.serversProvider.setClientsData(result['data']); - widget.serversProvider.setClientsLoadStatus(1, true); - } - else { - widget.appConfigProvider.addLog(result['log']); - widget.serversProvider.setClientsLoadStatus(2, true); - } - } - } - +class _AccessSettingsState extends State with TickerProviderStateMixin { + late ScrollController _scrollController; + late TabController _tabController; @override void initState() { - fetchClients(); + Provider.of(context, listen: false).fetchClients(updateLoading: true); super.initState(); - tabController = TabController( + _tabController = TabController( initialIndex: 0, length: 3, vsync: this, ); + _scrollController = ScrollController(); } @override Widget build(BuildContext context) { - final serversProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; - return Scaffold( - body: DefaultTabController( - length: 3, - child: NestedScrollView( - controller: scrollController, - headerSliverBuilder: ((context, innerBoxIsScrolled) { - return [ - SliverOverlapAbsorber( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - sliver: SliverSafeArea( - top: false, - sliver: SliverAppBar( - title: Text(AppLocalizations.of(context)!.accessSettings), - pinned: true, - floating: true, - forceElevated: innerBoxIsScrolled, - bottom: TabBar( - controller: tabController, - isScrollable: true, - unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant, - tabs: [ - Tab( - icon: const Icon(Icons.check), - text: AppLocalizations.of(context)!.allowedClients, - ), - Tab( - icon: const Icon(Icons.block), - text: AppLocalizations.of(context)!.disallowedClients, - ), - Tab( - icon: const Icon(Icons.link_rounded), - text: AppLocalizations.of(context)!.disallowedDomains, - ), - ] - ) + if (Platform.isAndroid || Platform.isIOS) { + return Scaffold( + body: DefaultTabController( + length: 3, + child: NestedScrollView( + controller: _scrollController, + headerSliverBuilder: ((context, innerBoxIsScrolled) { + return [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverSafeArea( + top: false, + bottom: false, + sliver: SliverAppBar( + title: Text(AppLocalizations.of(context)!.accessSettings), + pinned: true, + floating: true, + centerTitle: false, + forceElevated: innerBoxIsScrolled, + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, + bottom: _Tabs(tabController: _tabController) + ), ), - ), - ) - ]; - }), - body: Container( - decoration: BoxDecoration( - color: Theme.of(context).scaffoldBackgroundColor, - border: Border( - top: BorderSide( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.1) ) - ) - ), - child: TabBarView( - controller: tabController, - children: [ - ClientsList( - type: 'allowed', - scrollController: scrollController, - loadStatus: serversProvider.clients.loadStatus, - data: serversProvider.clients.loadStatus == 1 - ? serversProvider.clients.data!.clientsAllowedBlocked!.allowedClients : [], - fetchClients: fetchClients - ), - ClientsList( - type: 'disallowed', - scrollController: scrollController, - loadStatus: serversProvider.clients.loadStatus, - data: serversProvider.clients.loadStatus == 1 - ? serversProvider.clients.data!.clientsAllowedBlocked!.disallowedClients : [], - fetchClients: fetchClients - ), - ClientsList( - type: 'domains', - scrollController: scrollController, - loadStatus: serversProvider.clients.loadStatus, - data: serversProvider.clients.loadStatus == 1 - ? serversProvider.clients.data!.clientsAllowedBlocked!.blockedHosts : [], - fetchClients: fetchClients - ), - ] + ]; + }), + body: _TabsView( + tabController: _tabController, + scrollController: _scrollController ) - ), + ) + ), + ); + } + else { + return Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context)!.accessSettings), + centerTitle: false, + bottom: PreferredSize( + preferredSize: const Size(double.maxFinite, 50), + child: _Tabs(tabController: _tabController) + ) + ), + body: _TabsView( + tabController: _tabController, + scrollController: _scrollController ) - ), + ); + } + } +} + +class _Tabs extends StatelessWidget implements PreferredSizeWidget { + final TabController tabController; + + const _Tabs({ + required this.tabController, + }); + + @override + Widget build(BuildContext context) { + return TabBar( + controller: tabController, + isScrollable: true, + unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant, + tabAlignment: TabAlignment.start, + tabs: [ + Tab( + child: Row( + children: [ + const Icon(Icons.check), + const SizedBox(width: 8), + Text(AppLocalizations.of(context)!.allowedClients) + ], + ), + ), + Tab( + child: Row( + children: [ + const Icon(Icons.block), + const SizedBox(width: 8), + Text(AppLocalizations.of(context)!.disallowedClients) + ], + ), + ), + Tab( + child: Row( + children: [ + const Icon(Icons.link_rounded), + const SizedBox(width: 8), + Text(AppLocalizations.of(context)!.disallowedDomains) + ], + ), + ), + ] + ); + } + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); +} + +class _TabsView extends StatelessWidget { + final TabController tabController; + final ScrollController scrollController; + + const _TabsView({ + required this.tabController, + required this.scrollController, + }); + + @override + Widget build(BuildContext context) { + final clientsProvider = Provider.of(context); + + return TabBarView( + controller: tabController, + children: [ + ClientsList( + type: AccessSettingsList.allowed, + scrollController: scrollController, + loadStatus: clientsProvider.loadStatus, + data: clientsProvider.loadStatus == LoadStatus.loaded + ? clientsProvider.clients!.clientsAllowedBlocked!.allowedClients : [], + ), + ClientsList( + type: AccessSettingsList.disallowed, + scrollController: scrollController, + loadStatus: clientsProvider.loadStatus, + data: clientsProvider.loadStatus == LoadStatus.loaded + ? clientsProvider.clients!.clientsAllowedBlocked!.disallowedClients : [], + ), + ClientsList( + type: AccessSettingsList.domains, + scrollController: scrollController, + loadStatus: clientsProvider.loadStatus, + data: clientsProvider.loadStatus == LoadStatus.loaded + ? clientsProvider.clients!.clientsAllowedBlocked!.blockedHosts : [], + ), + ] ); } } \ No newline at end of file diff --git a/lib/screens/settings/access_settings/add_client_modal.dart b/lib/screens/settings/access_settings/add_client_modal.dart index 2aa91e6..bb0385a 100644 --- a/lib/screens/settings/access_settings/add_client_modal.dart +++ b/lib/screens/settings/access_settings/add_client_modal.dart @@ -1,23 +1,76 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -class AddClientModal extends StatefulWidget { - final String type; - final void Function(String, String) onConfirm; +import 'package:adguard_home_manager/providers/clients_provider.dart'; + +class AddClientModal extends StatelessWidget { + final AccessSettingsList type; + final void Function(String, AccessSettingsList) onConfirm; + final bool dialog; const AddClientModal({ - Key? key, + super.key, required this.type, - required this.onConfirm - }) : super(key: key); + required this.onConfirm, + required this.dialog, + }); @override - State createState() => _AddClientModalState(); + Widget build(BuildContext context) { + if (dialog == true) { + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 400 + ), + child: _Content( + type: type, + onConfirm: onConfirm, + ) + ), + ), + ); + } + else { + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).dialogBackgroundColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28) + ) + ), + child: SafeArea( + child: _Content( + type: type, + onConfirm: onConfirm, + ), + ) + ), + ); + } + } } -class _AddClientModalState extends State { - TextEditingController fieldController = TextEditingController(); +class _Content extends StatefulWidget { + final AccessSettingsList type; + final void Function(String, AccessSettingsList) onConfirm; + const _Content({ + required this.type, + required this.onConfirm, + }); + + @override + State<_Content> createState() => _ContentState(); +} + +class _ContentState extends State<_Content> { + TextEditingController fieldController = TextEditingController(); bool validData = false; void checkValidValues() { @@ -28,18 +81,18 @@ class _AddClientModalState extends State { setState(() => validData = false); } } - + @override Widget build(BuildContext context) { IconData icon() { switch (widget.type) { - case 'allowed': + case AccessSettingsList.allowed: return Icons.check; - case 'disallowed': + case AccessSettingsList.disallowed: return Icons.block; - case 'domains': + case AccessSettingsList.domains: return Icons.link_rounded; default: @@ -49,13 +102,13 @@ class _AddClientModalState extends State { String title() { switch (widget.type) { - case 'allowed': + case AccessSettingsList.allowed: return AppLocalizations.of(context)!.allowClient; - case 'disallowed': + case AccessSettingsList.disallowed: return AppLocalizations.of(context)!.disallowClient; - case 'domains': + case AccessSettingsList.domains: return AppLocalizations.of(context)!.disallowedDomains; default: @@ -64,40 +117,40 @@ class _AddClientModalState extends State { } return Padding( - padding: MediaQuery.of(context).viewInsets, - child: Container( - height: 305, - padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - color: Theme.of(context).dialogBackgroundColor, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28) - ) - ), - child: Column( - children: [ - Expanded( - child: ListView( - physics: 322 < MediaQuery.of(context).size.height - ? const NeverScrollableScrollPhysics() - : null, + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: SingleChildScrollView( + child: Wrap( children: [ - Icon( - icon(), - size: 24, - color: Theme.of(context).listTileTheme.iconColor + Row( + mainAxisAlignment: MainAxisAlignment.center, + 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 + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + title(), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + color: Theme.of(context).colorScheme.onSurface + ), + ), + ], ), ), - const SizedBox(height: 16), TextFormField( controller: fieldController, onChanged: (_) => checkValidValues(), @@ -108,9 +161,9 @@ class _AddClientModalState extends State { Radius.circular(10) ) ), - helperText: widget.type == 'allowed' || widget.type == 'disallowed' + helperText: widget.type == AccessSettingsList.allowed || widget.type == AccessSettingsList.disallowed ? AppLocalizations.of(context)!.addClientFieldDescription : null, - labelText: widget.type == 'allowed' || widget.type == 'disallowed' + labelText: widget.type == AccessSettingsList.allowed || widget.type == AccessSettingsList.disallowed ? AppLocalizations.of(context)!.clientIdentifier : AppLocalizations.of(context)!.domain, ), @@ -118,37 +171,37 @@ class _AddClientModalState extends State { ], ), ), - Padding( - padding: const EdgeInsets.only(top: 24), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(AppLocalizations.of(context)!.cancel) - ), - const SizedBox(width: 20), - TextButton( - onPressed: validData == true - ? () { - Navigator.pop(context); - widget.onConfirm(fieldController.text, widget.type); - } - : null, - child: Text( - AppLocalizations.of(context)!.confirm, - style: TextStyle( - color: validData == true - ? Theme.of(context).colorScheme.primary - : Colors.grey - ), - ) - ), - ], - ), - ) - ], - ), + ), + Padding( + padding: const EdgeInsets.only(top: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.cancel) + ), + const SizedBox(width: 16), + TextButton( + onPressed: validData == true + ? () { + Navigator.pop(context); + widget.onConfirm(fieldController.text, widget.type); + } + : null, + child: Text( + AppLocalizations.of(context)!.confirm, + style: TextStyle( + color: validData == true + ? Theme.of(context).colorScheme.primary + : Colors.grey + ), + ) + ), + ], + ), + ), + ], ), ); } diff --git a/lib/screens/settings/access_settings/clients_list.dart b/lib/screens/settings/access_settings/clients_list.dart index 9efcaa7..bf2c4d4 100644 --- a/lib/screens/settings/access_settings/clients_list.dart +++ b/lib/screens/settings/access_settings/clients_list.dart @@ -1,35 +1,35 @@ // ignore_for_file: use_build_context_synchronously +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/screens/settings/access_settings/add_client_modal.dart'; -import 'package:adguard_home_manager/screens/clients/remove_client_modal.dart'; +import 'package:adguard_home_manager/screens/clients/client/remove_client_modal.dart'; +import 'package:adguard_home_manager/widgets/tab_content_list.dart'; import 'package:adguard_home_manager/functions/snackbar.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/models/clients_allowed_blocked.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/providers/clients_provider.dart'; import 'package:adguard_home_manager/classes/process_modal.dart'; class ClientsList extends StatefulWidget { - final String type; + final AccessSettingsList type; final ScrollController scrollController; - final int loadStatus; + final LoadStatus loadStatus; final List data; - final Future Function() fetchClients; const ClientsList({ - Key? key, + super.key, required this.type, required this.scrollController, required this.loadStatus, required this.data, - required this.fetchClients - }) : super(key: key); + }); @override State createState() => _ClientsListState(); @@ -63,110 +63,97 @@ class _ClientsListState extends State { @override Widget build(BuildContext context) { - final serversProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final clientsProvider = Provider.of(context); - void confirmRemoveItem(String client, String type) async { - Map> body = { - "allowed_clients": serversProvider.clients.data!.clientsAllowedBlocked?.allowedClients ?? [], - "disallowed_clients": serversProvider.clients.data!.clientsAllowedBlocked?.disallowedClients ?? [], - "blocked_hosts": serversProvider.clients.data!.clientsAllowedBlocked?.blockedHosts ?? [], - }; + final width = MediaQuery.of(context).size.width; - if (type == 'allowed') { - body['allowed_clients'] = body['allowed_clients']!.where((c) => c != client).toList(); - } - else if (type == 'disallowed') { - body['disallowed_clients'] = body['disallowed_clients']!.where((c) => c != client).toList(); - } - else if (type == 'domains') { - body['blocked_hosts'] = body['blocked_hosts']!.where((c) => c != client).toList(); - } - - ProcessModal processModal = ProcessModal(context: context); - processModal.open(AppLocalizations.of(context)!.removingClient); - - final result = await requestAllowedBlockedClientsHosts(serversProvider.selectedServer!, body); - - processModal.close(); - - if (result['result'] == 'success') { - serversProvider.setAllowedDisallowedClientsBlockedDomains( - ClientsAllowedBlocked( - allowedClients: body['allowed_clients'] ?? [], - disallowedClients: body['disallowed_clients'] ?? [], - blockedHosts: body['blocked_hosts'] ?? [], - ) - ); - } - else if (result['result'] == 'error' && result['message'] == 'client_another_list') { - showSnacbkar( - context: context, + Future refetchClients() async { + final result = await clientsProvider.fetchClients(); + if (result == false && mounted) { + showSnackbar( appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.clientAnotherList, - color: Colors.red - ); - } - else { - appConfigProvider.addLog(result['log']); - - showSnacbkar( - context: context, - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.clientNotRemoved, + label: AppLocalizations.of(context)!.clientsNotLoaded, color: Colors.red ); } } - void confirmAddItem(String item, String type) async { + void confirmRemoveItem(String client, AccessSettingsList type) async { Map> body = { - "allowed_clients": serversProvider.clients.data!.clientsAllowedBlocked?.allowedClients ?? [], - "disallowed_clients": serversProvider.clients.data!.clientsAllowedBlocked?.disallowedClients ?? [], - "blocked_hosts": serversProvider.clients.data!.clientsAllowedBlocked?.blockedHosts ?? [], + "allowed_clients": clientsProvider.clients!.clientsAllowedBlocked?.allowedClients ?? [], + "disallowed_clients": clientsProvider.clients!.clientsAllowedBlocked?.disallowedClients ?? [], + "blocked_hosts": clientsProvider.clients!.clientsAllowedBlocked?.blockedHosts ?? [], }; - if (type == 'allowed') { - body['allowed_clients']!.add(item); + if (type == AccessSettingsList.allowed) { + body['allowed_clients'] = body['allowed_clients']!.where((c) => c != client).toList(); } - else if (type == 'disallowed') { - body['disallowed_clients']!.add(item); + else if (type == AccessSettingsList.disallowed) { + body['disallowed_clients'] = body['disallowed_clients']!.where((c) => c != client).toList(); } - else if (type == 'domains') { - body['blocked_hosts']!.add(item); + else if (type == AccessSettingsList.domains) { + body['blocked_hosts'] = body['blocked_hosts']!.where((c) => c != client).toList(); } - ProcessModal processModal = ProcessModal(context: context); + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.removingClient); - final result = await requestAllowedBlockedClientsHosts(serversProvider.selectedServer!, body); + final result = await clientsProvider.removeClientList(client, type); processModal.close(); - if (result['result'] == 'success') { - serversProvider.setAllowedDisallowedClientsBlockedDomains( - ClientsAllowedBlocked( - allowedClients: body['allowed_clients'] ?? [], - disallowedClients: body['disallowed_clients'] ?? [], - blockedHosts: body['blocked_hosts'] ?? [], - ) + if (result.successful == true) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.clientRemovedSuccessfully, + color: Colors.green ); } - else if (result['result'] == 'error' && result['message'] == 'client_another_list') { - showSnacbkar( - context: context, + else if (result.successful == false && result.content == 'client_another_list') { + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.clientAnotherList, color: Colors.red ); } else { - appConfigProvider.addLog(result['log']); - - showSnacbkar( - context: context, + showSnackbar( appConfigProvider: appConfigProvider, - label: type == 'allowed' || type == 'blocked' + label: type == AccessSettingsList.allowed || type == AccessSettingsList.disallowed + ? AppLocalizations.of(context)!.clientNotRemoved + : AppLocalizations.of(context)!.domainNotAdded, + color: Colors.red + ); + } + } + + void confirmAddItem(String item, AccessSettingsList type) async { + ProcessModal processModal = ProcessModal(); + processModal.open(AppLocalizations.of(context)!.removingClient); + + final result = await clientsProvider.addClientList(item, type); + + processModal.close(); + + if (result.successful == true) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.clientAddedSuccessfully, + color: Colors.green + ); + } + else if (result.successful == false && result.content == 'client_another_list') { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.clientAnotherList, + color: Colors.red + ); + } + else { + showSnackbar( + appConfigProvider: appConfigProvider, + label: type == AccessSettingsList.allowed || type == AccessSettingsList.disallowed ? AppLocalizations.of(context)!.clientNotRemoved : AppLocalizations.of(context)!.domainNotAdded, color: Colors.red @@ -176,13 +163,13 @@ class _ClientsListState extends State { String description() { switch (widget.type) { - case 'allowed': + case AccessSettingsList.allowed: return AppLocalizations.of(context)!.allowedClientsDescription; - case 'disallowed': + case AccessSettingsList.disallowed: return AppLocalizations.of(context)!.blockedClientsDescription; - case 'domains': + case AccessSettingsList.domains: return AppLocalizations.of(context)!.disallowedDomainsDescription; default: @@ -192,13 +179,13 @@ class _ClientsListState extends State { String noItems() { switch (widget.type) { - case 'allowed': + case AccessSettingsList.allowed: return AppLocalizations.of(context)!.noAllowedClients; - case 'disallowed': + case AccessSettingsList.disallowed: return AppLocalizations.of(context)!.noBlockedClients; - case 'domains': + case AccessSettingsList.domains: return AppLocalizations.of(context)!.noDisallowedDomains; default: @@ -206,167 +193,188 @@ class _ClientsListState extends State { } } - switch (widget.loadStatus) { - case 0: - return SizedBox( - width: double.maxFinite, - height: MediaQuery.of(context).size.height-171, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const CircularProgressIndicator(), - const SizedBox(height: 30), - Text( - AppLocalizations.of(context)!.loadingClients, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ) - ], - ), - ); - - case 1: - return Stack( + return CustomTabContentList( + noSliver: !(Platform.isAndroid || Platform.isIOS) ? true : false, + loadingGenerator: () => SizedBox( + width: double.maxFinite, + height: MediaQuery.of(context).size.height-171, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - RefreshIndicator( - onRefresh: widget.fetchClients, - child: Column( - mainAxisSize: MainAxisSize.max, - children: [ - Padding( - padding: const EdgeInsets.all(10), - child: Card( - child: Padding( - padding: const EdgeInsets.all(20), - child: Row( - children: [ - Icon( - Icons.info_rounded, - color: Theme.of(context).listTileTheme.iconColor, - ), - const SizedBox(width: 20), - Flexible( - child: Text( - description(), - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ) - ], - ), - ), - ), - ), - if (widget.data.isNotEmpty) Expanded( - child: ListView.builder( - padding: const EdgeInsets.only(top: 0), - itemCount: widget.data.length, - itemBuilder: (context, index) => ListTile( - title: Text( - widget.data[index], - style: TextStyle( - fontWeight: FontWeight.normal, - color: Theme.of(context).colorScheme.onSurface - ), - ), - trailing: IconButton( - onPressed: () => { - showDialog( - context: context, - builder: (context) => RemoveClientModal( - onConfirm: () => confirmRemoveItem(widget.data[index], widget.type), - ) - ) - }, - icon: const Icon(Icons.delete_rounded) - ), - ) - ), - ), - if (widget.data.isEmpty) Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - noItems(), - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 30), - TextButton.icon( - onPressed: widget.fetchClients, - icon: const Icon(Icons.refresh_rounded), - label: Text(AppLocalizations.of(context)!.refresh), - ) - ], - ), - ), - ], - ), - ), - AnimatedPositioned( - duration: const Duration(milliseconds: 100), - curve: Curves.easeInOut, - bottom: isVisible ? - appConfigProvider.showingSnackbar - ? 70 : 20 - : -70, - right: 20, - child: FloatingActionButton( - onPressed: () { - showModalBottomSheet( - context: context, - builder: (context) => AddClientModal( - type: widget.type, - onConfirm: confirmAddItem - ), - backgroundColor: Colors.transparent, - isScrollControlled: true - ); - }, - child: const Icon(Icons.add), + const CircularProgressIndicator(), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.loadingClients, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, ), ) - ] - ); - - case 2: - return SizedBox( - width: double.maxFinite, - height: MediaQuery.of(context).size.height-171, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon( - Icons.error, - color: Colors.red, - size: 50, - ), - const SizedBox(height: 30), - Text( - AppLocalizations.of(context)!.clientsNotLoaded, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, + ], + ), + ), + itemsCount: widget.data.isNotEmpty + ? widget.data.length+1 + : 0, + contentWidget: (index) { + if (index == 0) { + return Padding( + padding: const EdgeInsets.all(10), + child: Card( + child: Padding( + padding: const EdgeInsets.all(20), + child: Row( + children: [ + Icon( + Icons.info_rounded, + color: Theme.of(context).listTileTheme.iconColor, + ), + const SizedBox(width: 20), + Flexible( + child: Text( + description(), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ) + ], ), - ) - ], + ), + ), + ); + } + else { + return ListTile( + title: Text( + widget.data[index-1], + style: TextStyle( + fontWeight: FontWeight.normal, + color: Theme.of(context).colorScheme.onSurface + ), + ), + trailing: IconButton( + onPressed: () => { + showDialog( + context: context, + builder: (context) => RemoveClientModal( + onConfirm: () => confirmRemoveItem(widget.data[index-1], widget.type), + ) + ) + }, + icon: const Icon(Icons.delete_rounded) + ), + ); + } + }, + noData: Column( + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: Card( + child: Padding( + padding: const EdgeInsets.all(20), + child: Row( + children: [ + Icon( + Icons.info_rounded, + color: Theme.of(context).listTileTheme.iconColor, + ), + const SizedBox(width: 20), + Flexible( + child: Text( + description(), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ) + ], + ), + ), + ), ), - ); - - default: - return const SizedBox(); - } - + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + noItems(), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 30), + TextButton.icon( + onPressed: refetchClients, + icon: const Icon(Icons.refresh_rounded), + label: Text(AppLocalizations.of(context)!.refresh), + ) + ], + ), + ), + ], + ), + errorGenerator: () => SizedBox( + width: double.maxFinite, + height: MediaQuery.of(context).size.height-101, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon( + Icons.error, + color: Colors.red, + size: 50, + ), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.clientsNotLoaded, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ), + loadStatus: widget.loadStatus, + onRefresh: refetchClients, + refreshIndicatorOffset: 0, + fab: FloatingActionButton( + onPressed: () { + if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + context: context, + builder: (context) => AddClientModal( + type: widget.type, + onConfirm: confirmAddItem, + dialog: true, + ), + ); + } + else { + showModalBottomSheet( + context: context, + useRootNavigator: true, + builder: (context) => AddClientModal( + type: widget.type, + onConfirm: confirmAddItem, + dialog: false, + ), + backgroundColor: Colors.transparent, + isScrollControlled: true + ); + } + }, + child: const Icon(Icons.add), + ), + fabVisible: isVisible, + ); } } \ No newline at end of file diff --git a/lib/screens/settings/advanced_setings.dart b/lib/screens/settings/advanced_setings.dart index b0cbc5a..6026082 100644 --- a/lib/screens/settings/advanced_setings.dart +++ b/lib/screens/settings/advanced_setings.dart @@ -5,33 +5,38 @@ import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; -import 'package:adguard_home_manager/screens/settings/app_logs/app_logs.dart'; +import 'package:adguard_home_manager/functions/desktop_mode.dart'; +import 'package:adguard_home_manager/functions/snackbar.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; class AdvancedSettings extends StatelessWidget { - const AdvancedSettings({Key? key}) : super(key: key); + const AdvancedSettings({super.key}); @override Widget build(BuildContext context) { final appConfigProvider = Provider.of(context); - - Future updateSslCheck(bool newStatus) async { - final result = await appConfigProvider.setOverrideSslCheck(newStatus); + + final width = MediaQuery.of(context).size.width; + + Future updateSettings({ + required bool newStatus, + required Future Function(bool) function + }) async { + final result = await function(newStatus); + if (!context.mounted) return; if (result == true) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.restartAppTakeEffect), - backgroundColor: Colors.green, - ) + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.settingsUpdatedSuccessfully, + color: Colors.green ); } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.cannotUpdateSettings), - backgroundColor: Colors.red, - ) + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.cannotUpdateSettings, + color: Colors.red ); } } @@ -39,46 +44,36 @@ class AdvancedSettings extends StatelessWidget { return Scaffold( appBar: AppBar( title: Text(AppLocalizations.of(context)!.advancedSettings), + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, ), - body: ListView( - children: [ - CustomListTile( - icon: Icons.lock, - title: AppLocalizations.of(context)!.dontCheckCertificate, - subtitle: AppLocalizations.of(context)!.dontCheckCertificateDescription, - trailing: Switch( - value: appConfigProvider.overrideSslCheck, - onChanged: updateSslCheck, - activeColor: Theme.of(context).colorScheme.primary, - ), - onTap: () => updateSslCheck(!appConfigProvider.overrideSslCheck), - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - left: 20, - right: 10 - ) - ), - CustomListTile( - icon: Icons.list_rounded, - title: AppLocalizations.of(context)!.logs, - subtitle: AppLocalizations.of(context)!.checkAppLogs, - onTap: () => { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const AppLogs() - ) + body: SafeArea( + child: ListView( + children: [ + CustomListTile( + icon: Icons.lock, + title: AppLocalizations.of(context)!.dontCheckCertificate, + subtitle: AppLocalizations.of(context)!.dontCheckCertificateDescription, + trailing: Switch( + value: appConfigProvider.overrideSslCheck, + onChanged: (value) => updateSettings( + newStatus: value, + function: appConfigProvider.setOverrideSslCheck + ), + ), + onTap: () => updateSettings( + newStatus: !appConfigProvider.overrideSslCheck, + function: appConfigProvider.setOverrideSslCheck + ), + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + left: 20, + right: 10 ) - }, - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - left: 20, - right: 10 - ) - ), - ], - ), - ); + ), + ], + ), + ) + ); } } \ No newline at end of file diff --git a/lib/screens/settings/app_logs/app_log_details_modal.dart b/lib/screens/settings/app_logs/app_log_details_modal.dart deleted file mode 100644 index 26d07f2..0000000 --- a/lib/screens/settings/app_logs/app_log_details_modal.dart +++ /dev/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 createState() => _AppLogDetailsModalState(); -} - -class _AppLogDetailsModalState extends State { - 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") - ) - ], - ); - } -} \ No newline at end of file diff --git a/lib/screens/settings/app_logs/app_logs.dart b/lib/screens/settings/app_logs/app_logs.dart deleted file mode 100644 index 48a3c9d..0000000 --- a/lib/screens/settings/app_logs/app_logs.dart +++ /dev/null @@ -1,82 +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(context); - - return Scaffold( - appBar: AppBar( - title: Text(AppLocalizations.of(context)!.logs), - actions: [ - IconButton( - onPressed: appConfigProvider.logs.isNotEmpty - ? () => copyToClipboard( - context: context, - 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, - ), - ), - ) - ); - } -} \ No newline at end of file diff --git a/lib/screens/settings/appbar.dart b/lib/screens/settings/appbar.dart deleted file mode 100644 index ca12964..0000000 --- a/lib/screens/settings/appbar.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flutter/material.dart'; - -class SettingsAppBar extends StatelessWidget with PreferredSizeWidget { - const SettingsAppBar({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return AppBar( - toolbarHeight: 80, - title: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Image.asset( - 'assets/icon/icon1024-white-center.png', - width: 60, - ), - const SizedBox(width: 20), - const Text( - "AdGuard Home Manager", - style: TextStyle( - fontSize: 22 - ), - ) - ], - ) - ); - } - - @override - Size get preferredSize => const Size.fromHeight(80); -} \ No newline at end of file diff --git a/lib/screens/settings/customization/color_item.dart b/lib/screens/settings/customization/color_item.dart index 0d6ea03..00873d0 100644 --- a/lib/screens/settings/customization/color_item.dart +++ b/lib/screens/settings/customization/color_item.dart @@ -1,29 +1,37 @@ import 'package:flutter/material.dart'; class ColorItem extends StatelessWidget { + final int index; + final int total; final Color color; final int numericValue; final int? selectedValue; final void Function(int) onChanged; const ColorItem({ - Key? key, + super.key, + required this.index, + required this.total, required this.color, required this.numericValue, required this.selectedValue, required this.onChanged - }) : super(key: key); + }); @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.all(10), + padding: index == 0 + ? const EdgeInsets.only(top: 10, right: 10, bottom: 10) + : index == total-1 + ? const EdgeInsets.only(top: 10, bottom: 10, left: 10) + : const EdgeInsets.all(10), child: Material( borderRadius: BorderRadius.circular(50), child: InkWell( onTap: () => onChanged(numericValue), borderRadius: BorderRadius.circular(50), - overlayColor: const MaterialStatePropertyAll(Colors.grey), + overlayColor: const WidgetStatePropertyAll(Colors.grey), child: Container( width: 50, height: 50, diff --git a/lib/screens/settings/customization/customization.dart b/lib/screens/settings/customization/customization.dart index 9f6abe2..38f93a3 100644 --- a/lib/screens/settings/customization/customization.dart +++ b/lib/screens/settings/customization/customization.dart @@ -1,4 +1,5 @@ -import 'package:adguard_home_manager/functions/generate_color_translation.dart'; +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -9,11 +10,13 @@ import 'package:adguard_home_manager/screens/settings/customization/theme_mode_b import 'package:adguard_home_manager/widgets/custom_switch_list_tile.dart'; import 'package:adguard_home_manager/widgets/section_label.dart'; +import 'package:adguard_home_manager/functions/generate_color_translation.dart'; +import 'package:adguard_home_manager/functions/desktop_mode.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; import 'package:adguard_home_manager/constants/colors.dart'; class Customization extends StatelessWidget { - const Customization({Key? key}) : super(key: key); + const Customization({super.key}); @override Widget build(BuildContext context) { @@ -29,9 +32,9 @@ class CustomizationWidget extends StatefulWidget { final AppConfigProvider appConfigProvider; const CustomizationWidget({ - Key? key, + super.key, required this.appConfigProvider, - }) : super(key: key); + }); @override State createState() => _CustomizationWidgetState(); @@ -43,6 +46,8 @@ class _CustomizationWidgetState extends State { int selectedColor = 0; bool useThemeColorInsteadGreenRed = false; + final _colorsScrollController = ScrollController(); + @override void initState() { selectedTheme = widget.appConfigProvider.selectedThemeNumber; @@ -56,156 +61,157 @@ class _CustomizationWidgetState extends State { Widget build(BuildContext context) { final appConfigProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + return Scaffold( appBar: AppBar( title: Text(AppLocalizations.of(context)!.customization), + centerTitle: false, + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, ), - body: ListView( - children: [ - SectionLabel( - label: AppLocalizations.of(context)!.theme, - padding: const EdgeInsets.only(top: 10, left: 16, right: 16, bottom: 5), - ), - Column( - children: [ - CustomSwitchListTile( - value: selectedTheme == 0 ? true : false, - onChanged: (value) { - selectedTheme = value == true ? 0 : 1; - appConfigProvider.setSelectedTheme(value == true ? 0 : 1); - }, - title: AppLocalizations.of(context)!.systemDefined, - ), - const SizedBox(height: 10), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - ThemeModeButton( - icon: Icons.light_mode, - value: 1, - selected: selectedTheme, - label: AppLocalizations.of(context)!.light, - onChanged: (value) { - selectedTheme = value; - appConfigProvider.setSelectedTheme(value); - }, - disabled: selectedTheme == 0 ? true : false, - ), - ThemeModeButton( - icon: Icons.dark_mode, - value: 2, - selected: selectedTheme, - label: AppLocalizations.of(context)!.dark, - onChanged: (value) { - selectedTheme = value; - appConfigProvider.setSelectedTheme(value); - }, - disabled: selectedTheme == 0 ? true : false, - ), - ], - ), - ], - ), - SectionLabel( - label: AppLocalizations.of(context)!.color, - padding: const EdgeInsets.only(top: 45, left: 16, right: 16, bottom: 5), - ), - if (appConfigProvider.androidDeviceInfo != null && appConfigProvider.androidDeviceInfo!.version.sdkInt! >= 31) CustomSwitchListTile( - value: dynamicColor, - onChanged: (value) { - setState(() => dynamicColor = value); - appConfigProvider.setUseDynamicColor(value); - }, - title: AppLocalizations.of(context)!.useDynamicTheme, - ), - if (!(appConfigProvider.androidDeviceInfo != null && appConfigProvider.androidDeviceInfo!.version.sdkInt! >= 31)) const SizedBox(height: 20), - if (dynamicColor == false) ...[ - SizedBox( - width: MediaQuery.of(context).size.width, - height: 70, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: colors.length, - itemBuilder: (context, index) { - if (index == 0) { - return Row( - children: [ - const SizedBox(width: 15), - ColorItem( - color: colors[index], - numericValue: index, - selectedValue: selectedColor, - onChanged: (value) { - setState(() => selectedColor = value); - appConfigProvider.setStaticColor(value); - } - ), - Container( - margin: const EdgeInsets.symmetric(horizontal: 10), - width: 1, - height: 60, - decoration: BoxDecoration( - color: Colors.grey, - borderRadius: BorderRadius.circular(1) - ), - ) - ], - ); - } - else if (index == colors.length-1) { - return Row( - children: [ - ColorItem( - color: colors[index], - numericValue: index, - selectedValue: selectedColor, - onChanged: (value) { - setState(() => selectedColor = value); - appConfigProvider.setStaticColor(value); - } - ), - const SizedBox(width: 15) - ], - ); - } - else { - return ColorItem( - color: colors[index], - numericValue: index, - selectedValue: selectedColor, - onChanged: (value) { - setState(() => selectedColor = value); - appConfigProvider.setStaticColor(value); - } - ); - } - }, - ), + body: SafeArea( + child: ListView( + children: [ + SectionLabel( + label: AppLocalizations.of(context)!.theme, + padding: const EdgeInsets.only(top: 10, left: 16, right: 16, bottom: 5), ), - Padding( - padding: const EdgeInsets.only( - left: 25, - top: 10 - ), - child: Text( - colorTranslation(context, selectedColor), - style: TextStyle( - color: Theme.of(context).listTileTheme.iconColor, - fontSize: 16 + Column( + children: [ + CustomSwitchListTile( + value: selectedTheme == 0 ? true : false, + onChanged: (value) { + selectedTheme = value == true ? 0 : 1; + appConfigProvider.setSelectedTheme(value == true ? 0 : 1); + }, + title: AppLocalizations.of(context)!.systemDefined, + ), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ThemeModeButton( + icon: Icons.light_mode, + value: 1, + selected: selectedTheme, + label: AppLocalizations.of(context)!.light, + onChanged: (value) { + selectedTheme = value; + appConfigProvider.setSelectedTheme(value); + }, + disabled: selectedTheme == 0 ? true : false, + ), + ThemeModeButton( + icon: Icons.dark_mode, + value: 2, + selected: selectedTheme, + label: AppLocalizations.of(context)!.dark, + onChanged: (value) { + selectedTheme = value; + appConfigProvider.setSelectedTheme(value); + }, + disabled: selectedTheme == 0 ? true : false, + ), + ], + ), + ], + ), + SectionLabel( + label: AppLocalizations.of(context)!.color, + padding: const EdgeInsets.only(top: 45, left: 16, right: 16, bottom: 5), + ), + if (appConfigProvider.supportsDynamicTheme) CustomSwitchListTile( + value: dynamicColor, + onChanged: (value) { + setState(() => dynamicColor = value); + appConfigProvider.setUseDynamicColor(value); + }, + title: AppLocalizations.of(context)!.useDynamicTheme, + ), + if (!(appConfigProvider.androidDeviceInfo != null && appConfigProvider.androidDeviceInfo!.version.sdkInt >= 31)) const SizedBox(height: 20), + if ( + appConfigProvider.supportsDynamicTheme == false || + (appConfigProvider.supportsDynamicTheme == true && dynamicColor == false) + ) Padding( + padding: const EdgeInsets.only(bottom: 8, left: 16, right: 16), + child: Scrollbar( + controller: _colorsScrollController, + thumbVisibility: Platform.isMacOS || Platform.isLinux || Platform.isWindows, + interactive: Platform.isMacOS || Platform.isLinux || Platform.isWindows, + thickness: Platform.isMacOS || Platform.isLinux || Platform.isWindows ? 8 : 0, + child: Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: MediaQuery.of(context).size.width, + height: 70, + child: ListView.builder( + controller: _colorsScrollController, + scrollDirection: Axis.horizontal, + itemCount: colors.length, + padding: const EdgeInsets.all(0), + itemBuilder: (context, index) { + if (index == 0) { + return Row( + children: [ + ColorItem( + index: index, + total: colors.length, + color: colors[index], + numericValue: index, + selectedValue: selectedColor, + onChanged: (value) { + setState(() => selectedColor = value); + appConfigProvider.setStaticColor(value); + } + ), + Container( + margin: const EdgeInsets.symmetric(horizontal: 10), + width: 1, + height: 60, + decoration: BoxDecoration( + color: Colors.grey, + borderRadius: BorderRadius.circular(1) + ), + ) + ], + ); + } + else { + return ColorItem( + index: index, + total: colors.length, + color: colors[index], + numericValue: index, + selectedValue: selectedColor, + onChanged: (value) { + setState(() => selectedColor = value); + appConfigProvider.setStaticColor(value); + } + ); + } + }, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Text( + colorTranslation(context, selectedColor), + style: TextStyle( + color: Theme.of(context).listTileTheme.iconColor, + fontSize: 16 + ), + ), + ) + ], + ), ), ), - ) + ), ], - CustomSwitchListTile( - value: useThemeColorInsteadGreenRed, - onChanged: (value) { - setState(() => useThemeColorInsteadGreenRed = value); - appConfigProvider.setUseThemeColorForStatus(value); - }, - title: AppLocalizations.of(context)!.useThemeColorStatus, - subtitle: AppLocalizations.of(context)!.useThemeColorStatusDescription, - ) - ], + ), ), ); } diff --git a/lib/screens/settings/customization/theme_mode_button.dart b/lib/screens/settings/customization/theme_mode_button.dart index df3f8d1..ec949c0 100644 --- a/lib/screens/settings/customization/theme_mode_button.dart +++ b/lib/screens/settings/customization/theme_mode_button.dart @@ -9,14 +9,14 @@ class ThemeModeButton extends StatelessWidget { final bool? disabled; const ThemeModeButton({ - Key? key, + super.key, required this.icon, required this.value, required this.selected, required this.label, required this.onChanged, this.disabled - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -32,19 +32,19 @@ class ThemeModeButton extends StatelessWidget { ? () => onChanged(value) : null, style: ButtonStyle( - elevation: MaterialStateProperty.all(0), - shape: MaterialStateProperty.all( + elevation: WidgetStateProperty.all(0), + shape: WidgetStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular(30), ) ), - backgroundColor: MaterialStateProperty.all( + backgroundColor: WidgetStateProperty.all( value == selected ? disabled == null || disabled == false ? Theme.of(context).colorScheme.primary : greyBackgroundColor : disabled == null || disabled == false - ? Theme.of(context).colorScheme.surfaceVariant + ? Theme.of(context).colorScheme.surfaceTint.withOpacity(0.1) : greyBackgroundColor, ) ), diff --git a/lib/screens/settings/dhcp/add_static_lease_modal.dart b/lib/screens/settings/dhcp/add_static_lease_modal.dart index 2c8e316..d40c506 100644 --- a/lib/screens/settings/dhcp/add_static_lease_modal.dart +++ b/lib/screens/settings/dhcp/add_static_lease_modal.dart @@ -1,21 +1,63 @@ +import 'package:adguard_home_manager/constants/regexps.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/models/dhcp.dart'; -class AddStaticLeaseModal extends StatefulWidget { +class AddStaticLeaseModal extends StatelessWidget { final void Function(Lease) onConfirm; + final bool dialog; const AddStaticLeaseModal({ - Key? key, + super.key, required this.onConfirm, - }) : super(key: key); + required this.dialog + }); @override - State createState() => _AddStaticLeaseModalState(); + Widget build(BuildContext context) { + if (dialog == true) { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 400 + ), + child: _Content(onConfirm: onConfirm) + ), + ); + } + else { + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).dialogBackgroundColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28) + ) + ), + child: SafeArea( + child: _Content(onConfirm: onConfirm) + ) + ), + ); + } + } } -class _AddStaticLeaseModalState extends State { +class _Content extends StatefulWidget { + final void Function(Lease) onConfirm; + + const _Content({ + required this.onConfirm + }); + + @override + State<_Content> createState() => __ContentState(); +} + +class __ContentState extends State<_Content> { final TextEditingController macController = TextEditingController(); String? macError; final TextEditingController ipController = TextEditingController(); @@ -26,8 +68,7 @@ class _AddStaticLeaseModalState extends State { bool validData = false; void validateMac(String value) { - final RegExp macRegex = RegExp(r'^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$'); - if (macRegex.hasMatch(value)) { + if (Regexps.macAddress.hasMatch(value)) { setState(() => macError = null); } else { @@ -37,8 +78,7 @@ class _AddStaticLeaseModalState extends State { } void validateIp(String value) { - RegExp ipAddress = RegExp(r'^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$'); - if (ipAddress.hasMatch(value) == true) { + if (Regexps.ipv4Address.hasMatch(value) == true) { setState(() => ipError = null); } else { @@ -65,145 +105,147 @@ class _AddStaticLeaseModalState extends State { @override Widget build(BuildContext context) { - return Padding( - padding: MediaQuery.of(context).viewInsets, - child: Container( - height: 510, - decoration: BoxDecoration( - color: Theme.of(context).dialogBackgroundColor, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28) - ) - ), - child: Column( - children: [ - Expanded( - child: ListView( - physics: 550 < MediaQuery.of(context).size.height - ? const NeverScrollableScrollPhysics() - : null, - children: [ - Padding( - padding: const EdgeInsets.only(top: 24), - child: Icon( - Icons.add, - size: 24, - color: Theme.of(context).listTileTheme.iconColor - ), - ), - const SizedBox(height: 16), - Text( - AppLocalizations.of(context)!.addStaticLease, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSurface - ), - ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 28), - child: TextFormField( - controller: macController, - onChanged: validateMac, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.smartphone_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: SingleChildScrollView( + child: Wrap( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 24), + child: Icon( + Icons.add, + size: 24, + color: Theme.of(context).listTileTheme.iconColor + ), + ), + const SizedBox(height: 16), + Text( + AppLocalizations.of(context)!.addStaticLease, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + color: Theme.of(context).colorScheme.onSurface + ), + ), + ], ), - errorText: macError, - labelText: AppLocalizations.of(context)!.macAddress, ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only( + left: 24, right: 24, bottom: 12 + ), + child: TextFormField( + controller: macController, + onChanged: validateMac, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.smartphone_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + errorText: macError, + labelText: AppLocalizations.of(context)!.macAddress, ), ), - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 28), - child: TextFormField( - controller: ipController, - onChanged: validateIp, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.link_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - errorText: ipError, - labelText: AppLocalizations.of(context)!.ipAddress, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + child: TextFormField( + controller: ipController, + onChanged: validateIp, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.link_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) ), + errorText: ipError, + labelText: AppLocalizations.of(context)!.ipAddress, ), ), - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 28), - child: TextFormField( - controller: hostNameController, - onChanged: (value) { - if (value != '') { - setState(() => hostNameError = null); - } - else { - setState(() => hostNameError = AppLocalizations.of(context)!.hostNameError); - } - validateData(); - }, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.badge_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - errorText: hostNameError, - labelText: AppLocalizations.of(context)!.hostName, - ), - ), + ), + Padding( + padding: const EdgeInsets.only( + left: 24, right: 24, top: 12 ), - ], - ), - ), - Padding( - padding: const EdgeInsets.all(20), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(AppLocalizations.of(context)!.cancel), - ), - const SizedBox(width: 20), - TextButton( - onPressed: validData == true - ? () { - Navigator.pop(context); - widget.onConfirm( - Lease( - mac: macController.text, - hostname: hostNameController.text, - ip: ipController.text - ) - ); + child: TextFormField( + controller: hostNameController, + onChanged: (value) { + if (value != '') { + setState(() => hostNameError = null); } - : null, - child: Text( - AppLocalizations.of(context)!.confirm, - style: TextStyle( - color: validData == true - ? Theme.of(context).colorScheme.primary - : Colors.grey + else { + setState(() => hostNameError = AppLocalizations.of(context)!.hostNameError); + } + validateData(); + }, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.badge_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) ), + errorText: hostNameError, + labelText: AppLocalizations.of(context)!.hostName, ), ), - ], - ), - ) - ], + ), + ], + ), + ), ), - ), + Padding( + padding: const EdgeInsets.all(24), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.cancel), + ), + const SizedBox(width: 20), + TextButton( + onPressed: validData == true + ? () { + Navigator.pop(context); + widget.onConfirm( + Lease( + mac: macController.text, + hostname: hostNameController.text, + ip: ipController.text + ) + ); + } + : null, + child: Text( + AppLocalizations.of(context)!.confirm, + style: TextStyle( + color: validData == true + ? Theme.of(context).colorScheme.primary + : Colors.grey + ), + ), + ), + ], + ), + ) + ], ); } } \ No newline at end of file diff --git a/lib/screens/settings/dhcp/delete_static_lease_modal.dart b/lib/screens/settings/dhcp/delete_static_lease_modal.dart index 77ad241..f797e89 100644 --- a/lib/screens/settings/dhcp/delete_static_lease_modal.dart +++ b/lib/screens/settings/dhcp/delete_static_lease_modal.dart @@ -5,9 +5,9 @@ class DeleteStaticLeaseModal extends StatelessWidget { final void Function() onConfirm; const DeleteStaticLeaseModal({ - Key? key, + super.key, required this.onConfirm - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/screens/settings/dhcp/dhcp.dart b/lib/screens/settings/dhcp/dhcp.dart index 8c4bb94..76867ef 100644 --- a/lib/screens/settings/dhcp/dhcp.dart +++ b/lib/screens/settings/dhcp/dhcp.dart @@ -1,52 +1,38 @@ // ignore_for_file: use_build_context_synchronously +import 'dart:io'; + +import 'package:adguard_home_manager/constants/regexps.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:bottom_sheet/bottom_sheet.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:adguard_home_manager/widgets/section_label.dart'; +import 'package:adguard_home_manager/screens/settings/settings.dart'; +import 'package:adguard_home_manager/screens/settings/dhcp/dhcp_not_available.dart'; import 'package:adguard_home_manager/widgets/confirm_action_modal.dart'; +import 'package:adguard_home_manager/screens/settings/dhcp/dhcp_main_button.dart'; +import 'package:adguard_home_manager/widgets/section_label.dart'; import 'package:adguard_home_manager/screens/settings/dhcp/dhcp_leases.dart'; import 'package:adguard_home_manager/screens/settings/dhcp/select_interface_modal.dart'; +import 'package:adguard_home_manager/functions/desktop_mode.dart'; +import 'package:adguard_home_manager/providers/status_provider.dart'; import 'package:adguard_home_manager/functions/snackbar.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/providers/dhcp_provider.dart'; import 'package:adguard_home_manager/classes/process_modal.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; import 'package:adguard_home_manager/models/dhcp.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart'; -class Dhcp extends StatelessWidget { - const Dhcp({Key? key}) : super(key: key); +class DhcpScreen extends StatefulWidget { + const DhcpScreen({super.key}); @override - Widget build(BuildContext context) { - final serversProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - - return DhcpWidget( - serversProvider: serversProvider, - appConfigProvider: appConfigProvider - ); - } + State createState() => _DhcpScreenState(); } -class DhcpWidget extends StatefulWidget { - final ServersProvider serversProvider; - final AppConfigProvider appConfigProvider; - - const DhcpWidget({ - Key? key, - required this.serversProvider, - required this.appConfigProvider - }) : super(key: key); - - @override - State createState() => _DhcpWidgetState(); -} - -class _DhcpWidgetState extends State { +class _DhcpScreenState extends State { NetworkInterface? selectedInterface; bool enabled = false; @@ -72,32 +58,26 @@ class _DhcpWidgetState extends State { bool dataValid = false; void loadDhcpStatus() async { - widget.serversProvider.setDhcpLoadStatus(0, false); + final result = await Provider.of(context, listen: false).loadDhcpStatus(); + if (!mounted || result == false) return; - final result = await getDhcpData(server: widget.serversProvider.selectedServer!); - - if (mounted) { - if (result['result'] == 'success') { - widget.serversProvider.setDhcpLoadStatus(1, true); - widget.serversProvider.setDhcpData(result['data']); - setState(() { - if (result['data'].dhcpStatus.interfaceName != '') { - selectedInterface = result['data'].networkInterfaces.firstWhere((interface) => interface.name == result['data'].dhcpStatus.interfaceName); - - enabled = result['data'].dhcpStatus.enabled; - ipv4StartRangeController.text = result['data'].dhcpStatus.v4.rangeStart; - ipv4StartRangeController.text = result['data'].dhcpStatus.v4.rangeStart; - ipv4EndRangeController.text = result['data'].dhcpStatus.v4.rangeEnd; - ipv4SubnetMaskController.text = result['data'].dhcpStatus.v4.subnetMask; - ipv4GatewayController.text = result['data'].dhcpStatus.v4.gatewayIp; - ipv4LeaseTimeController.text = result['data'].dhcpStatus.v4.leaseDuration.toString(); - } - }); + final dhcpProvider = Provider.of(context, listen: false); + if (dhcpProvider.dhcp == null) return; + + setState(() { + if (dhcpProvider.dhcp!.dhcpStatus!.interfaceName != null && dhcpProvider.dhcp!.dhcpStatus!.interfaceName != '') { + try {selectedInterface = dhcpProvider.dhcp!.networkInterfaces.firstWhere((iface) => iface.name == dhcpProvider.dhcp!.dhcpStatus!.interfaceName);} catch (_) {} + enabled = dhcpProvider.dhcp!.dhcpStatus!.enabled; + if (dhcpProvider.dhcp!.dhcpStatus!.v4 != null) { + ipv4StartRangeController.text = dhcpProvider.dhcp!.dhcpStatus!.v4!.rangeStart; + ipv4EndRangeController.text = dhcpProvider.dhcp!.dhcpStatus!.v4!.rangeEnd ?? ''; + ipv4SubnetMaskController.text = dhcpProvider.dhcp!.dhcpStatus!.v4!.subnetMask ?? ''; + ipv4GatewayController.text = dhcpProvider.dhcp!.dhcpStatus!.v4!.gatewayIp ?? ''; + ipv4LeaseTimeController.text = dhcpProvider.dhcp!.dhcpStatus!.v4!.leaseDuration.toString(); + } } - else { - widget.serversProvider.setDhcpLoadStatus(2, true); - } - } + }); + checkDataValid(); } @@ -121,8 +101,8 @@ class _DhcpWidgetState extends State { break; } } - final regex = RegExp(r'^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$'); - if (regex.hasMatch(value)) { + + if (Regexps.ipv4Address.hasMatch(value)) { setValue(null); } else { @@ -147,8 +127,8 @@ class _DhcpWidgetState extends State { break; } } - final regex = RegExp(r'^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$'); - if (regex.hasMatch(value)) { + + if (Regexps.ipv6Address.hasMatch(value)) { setValue(null); } else { @@ -204,51 +184,52 @@ class _DhcpWidgetState extends State { @override void initState() { - loadDhcpStatus(); + final statusProvider = Provider.of(context, listen: false); + if (mounted && statusProvider.serverStatus?.dhcpAvailable == true) loadDhcpStatus(); super.initState(); } @override Widget build(BuildContext context) { final serversProvider = Provider.of(context); + final statusProvider = Provider.of(context); + final dhcpProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + void saveSettings() async { - ProcessModal processModal = ProcessModal(context: context); + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.savingSettings); - - final result = await saveDhcpConfig(server: serversProvider.selectedServer!, data: { - "enabled": enabled, - "interface_name": selectedInterface!.name, - if (selectedInterface!.ipv4Addresses.isNotEmpty) "v4": { - "gateway_ip": ipv4GatewayController.text, - "subnet_mask": ipv4SubnetMaskController.text, - "range_start": ipv4StartRangeController.text, - "range_end": ipv4EndRangeController.text, - "lease_duration": ipv4LeaseTimeController.text != '' ? int.parse(ipv4LeaseTimeController.text) : null - }, - if (selectedInterface!.ipv6Addresses.isNotEmpty) "v6": { - "range_start": ipv6StartRangeController.text, - "range_end": ipv6EndRangeController.text, - "lease_duration": ipv6LeaseTimeController.text != '' ? int.parse(ipv6LeaseTimeController.text) : null + final result = await serversProvider.apiClient2!.saveDhcpConfig( + data: { + "enabled": enabled, + "interface_name": selectedInterface!.name, + if (selectedInterface!.ipv4Addresses.isNotEmpty) "v4": { + "gateway_ip": ipv4GatewayController.text, + "subnet_mask": ipv4SubnetMaskController.text, + "range_start": ipv4StartRangeController.text, + "range_end": ipv4EndRangeController.text, + "lease_duration": ipv4LeaseTimeController.text != '' ? int.parse(ipv4LeaseTimeController.text) : null + }, + if (selectedInterface!.ipv6Addresses.isNotEmpty) "v6": { + "range_start": ipv6StartRangeController.text, + "range_end": ipv6EndRangeController.text, + "lease_duration": ipv6LeaseTimeController.text != '' ? int.parse(ipv6LeaseTimeController.text) : null + } } - }); - + ); + if (!mounted) return; processModal.close(); - - if (result['result'] == 'success') { - showSnacbkar( - context: context, + if (result.successful == true) { + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.settingsSaved, color: Colors.green ); } else { - appConfigProvider.addLog(result['log']); - - showSnacbkar( - context: context, + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.settingsNotSaved, color: Colors.red @@ -258,28 +239,21 @@ class _DhcpWidgetState extends State { void restoreConfig() async { Future.delayed(const Duration(seconds: 0), () async { - ProcessModal processModal = ProcessModal(context: context); + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.restoringConfig); - - final result = await resetDhcpConfig(server: serversProvider.selectedServer!); - + final result = await serversProvider.apiClient2!.resetDhcpConfig(); + if (!mounted) return; processModal.close(); - - if (result['result'] == 'success') { + if (result.successful == true) { clearAll(); - - showSnacbkar( - context: context, + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.configRestored, color: Colors.green ); } else { - appConfigProvider.addLog(result['log']); - - showSnacbkar( - context: context, + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.configNotRestored, color: Colors.red @@ -290,31 +264,27 @@ class _DhcpWidgetState extends State { void restoreLeases() async { Future.delayed(const Duration(seconds: 0), () async { - ProcessModal processModal = ProcessModal(context: context); + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.restoringLeases); - final result = await restoreAllLeases(server: serversProvider.selectedServer!); + final result = await serversProvider.apiClient2!.restoreAllLeases(); processModal.close(); - if (result['result'] == 'success') { - DhcpData data = serversProvider.dhcp.data!; - data.dhcpStatus.staticLeases = []; - data.dhcpStatus.leases = []; - serversProvider.setDhcpData(data); + if (result.successful == true) { + DhcpModel data = dhcpProvider.dhcp!; + data.dhcpStatus!.staticLeases = []; + data.dhcpStatus!.leases = []; + dhcpProvider.setDhcpData(data); - showSnacbkar( - context: context, + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.leasesRestored, color: Colors.green ); } else { - appConfigProvider.addLog(result['log']); - - showSnacbkar( - context: context, + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.leasesNotRestored, color: Colors.red @@ -354,417 +324,48 @@ class _DhcpWidgetState extends State { void selectInterface() { ScaffoldMessenger.of(context).clearSnackBars(); Future.delayed(const Duration(seconds: 0), () { - showFlexibleBottomSheet( - minHeight: 0.6, - initHeight: 0.6, - maxHeight: 0.95, - isCollapsible: true, - duration: const Duration(milliseconds: 250), - anchors: [0.95], - context: context, - builder: (ctx, controller, offset) => SelectInterfaceModal( - interfaces: serversProvider.dhcp.data!.networkInterfaces, - scrollController: controller, - onSelect: (interface) => setState(() { - clearAll(); - selectedInterface = interface; - }) - ), - bottomSheetColor: Colors.transparent - ); + if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + context: context, + builder: (context) => SelectInterfaceModal( + interfaces: dhcpProvider.dhcp!.networkInterfaces, + onSelect: (interface) => setState(() { + clearAll(); + selectedInterface = interface; + }), + dialog: true, + ) + ); + } + else { + showModalBottomSheet( + context: context, + useRootNavigator: true, + builder: (context) => SelectInterfaceModal( + interfaces: dhcpProvider.dhcp!.networkInterfaces, + onSelect: (i) => setState(() { + clearAll(); + selectedInterface = i; + }), + dialog: false, + ), + isScrollControlled: true, + useSafeArea: true, + backgroundColor: Colors.transparent + ); + } }); } - Widget generateBody() { - switch (serversProvider.dhcp.loadStatus) { - case 0: - return SizedBox( - width: double.maxFinite, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const CircularProgressIndicator(), - const SizedBox(height: 30), - Text( - AppLocalizations.of(context)!.loadingDhcp, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ) - ], - ), - ); - - case 1: - if (selectedInterface != null) { - return ListView( - children: [ - Padding( - padding: const EdgeInsets.only( - top: 10, - left: 16, - right: 16 - ), - child: Material( - color: Theme.of(context).colorScheme.primary.withOpacity(0.1), - borderRadius: BorderRadius.circular(28), - child: InkWell( - onTap: selectedInterface != null - ? () => setState(() => enabled = !enabled) - : null, - borderRadius: BorderRadius.circular(28), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 12 - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - AppLocalizations.of(context)!.enableDhcpServer, - style: TextStyle( - fontSize: 16, - color: Theme.of(context).colorScheme.onSurface - ), - ), - if (selectedInterface != null) ...[ - Text( - selectedInterface!.name, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).listTileTheme.textColor - ), - ) - ] - ], - ), - Switch( - value: enabled, - onChanged: selectedInterface != null - ? (value) => setState(() => enabled = value) - : null, - activeColor: Theme.of(context).colorScheme.primary, - ), - ], - ), - ), - ), - ), - ), - if (selectedInterface!.ipv4Addresses.isNotEmpty) ...[ - SectionLabel( - label: AppLocalizations.of(context)!.ipv4settings, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: TextFormField( - controller: ipv4StartRangeController, - onChanged: (value) => validateIpV4(value, 'ipv4StartRangeError', AppLocalizations.of(context)!.ipNotValid), - decoration: InputDecoration( - prefixIcon: const Icon(Icons.skip_previous_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - errorText: ipv4StartRangeError, - labelText: AppLocalizations.of(context)!.startOfRange, - ), - keyboardType: TextInputType.number, - ), - ), - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: TextFormField( - controller: ipv4EndRangeController, - onChanged: (value) => validateIpV4(value, 'ipv4EndRangeError', AppLocalizations.of(context)!.ipNotValid), - decoration: InputDecoration( - prefixIcon: const Icon(Icons.skip_next_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - errorText: ipv4EndRangeError, - labelText: AppLocalizations.of(context)!.endOfRange, - ), - keyboardType: TextInputType.number, - ), - ), - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: TextFormField( - controller: ipv4SubnetMaskController, - onChanged: (value) => validateIpV4(value, 'ipv4SubnetMaskError', AppLocalizations.of(context)!.subnetMaskNotValid), - decoration: InputDecoration( - prefixIcon: const Icon(Icons.hub_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - errorText: ipv4SubnetMaskError, - labelText: AppLocalizations.of(context)!.subnetMask, - ), - keyboardType: TextInputType.number, - ), - ), - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: TextFormField( - controller: ipv4GatewayController, - onChanged: (value) => validateIpV4(value, 'ipv4GatewayError', AppLocalizations.of(context)!.gatewayNotValid), - decoration: InputDecoration( - prefixIcon: const Icon(Icons.router_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - errorText: ipv4GatewayError, - labelText: AppLocalizations.of(context)!.gateway, - ), - keyboardType: TextInputType.number, - ), - ), - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: TextFormField( - controller: ipv4LeaseTimeController, - onChanged: (value) { - if (int.tryParse(value).runtimeType == int) { - setState(() => ipv4LeaseTimeError = null); - } - else { - setState(() => ipv4LeaseTimeError = AppLocalizations.of(context)!.leaseTimeNotValid); - } - }, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.timer), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - errorText: ipv4LeaseTimeError, - labelText: AppLocalizations.of(context)!.leaseTime, - ), - keyboardType: TextInputType.number, - ), - ), - ], - if (selectedInterface!.ipv6Addresses.isNotEmpty) ...[ - SectionLabel( - label: AppLocalizations.of(context)!.ipv6settings, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: TextFormField( - controller: ipv6StartRangeController, - onChanged: (value) => validateIpV4(value, 'ipv6StartRangeError', AppLocalizations.of(context)!.ipNotValid), - decoration: InputDecoration( - prefixIcon: const Icon(Icons.skip_next_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - errorText: ipv6StartRangeError, - labelText: AppLocalizations.of(context)!.startOfRange, - ), - keyboardType: TextInputType.number, - ), - ), - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: TextFormField( - controller: ipv6EndRangeController, - onChanged: (value) => validateIpV4(value, 'ipv6EndRangeError', AppLocalizations.of(context)!.ipNotValid), - decoration: InputDecoration( - prefixIcon: const Icon(Icons.skip_previous_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - errorText: ipv6EndRangeError, - labelText: AppLocalizations.of(context)!.endOfRange, - ), - keyboardType: TextInputType.number, - ), - ), - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: TextFormField( - controller: ipv6LeaseTimeController, - onChanged: (value) { - if (int.tryParse(value).runtimeType == int) { - setState(() => ipv6LeaseTimeError = null); - } - else { - setState(() => ipv6LeaseTimeError = AppLocalizations.of(context)!.leaseTimeNotValid); - } - }, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.timer), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - errorText: ipv6LeaseTimeError, - labelText: AppLocalizations.of(context)!.leaseTime, - ), - keyboardType: TextInputType.number, - ), - ), - ], - const SizedBox(height: 20), - SectionLabel( - label: AppLocalizations.of(context)!.dhcpLeases, - ), - Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - Navigator.push(context, MaterialPageRoute( - builder: (context) => DhcpLeases( - items: serversProvider.dhcp.data!.dhcpStatus.leases, - staticLeases: false, - ) - )); - }, - child: Container( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - AppLocalizations.of(context)!.dhcpLeases, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - Icon( - Icons.arrow_forward_rounded, - color: Theme.of(context).colorScheme.onSurface, - ) - ], - ), - ), - ), - ), - Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - Navigator.push(context, MaterialPageRoute( - builder: (context) => DhcpLeases( - items: serversProvider.dhcp.data!.dhcpStatus.staticLeases, - staticLeases: true, - ) - )); - }, - child: Container( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - AppLocalizations.of(context)!.dhcpStatic, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - Icon( - Icons.arrow_forward_rounded, - color: Theme.of(context).colorScheme.onSurface, - ) - ], - ), - ), - ), - ), - const SizedBox(height: 10) - ], - ); - } - else { - return Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Text( - AppLocalizations.of(context)!.neededSelectInterface, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5) - ), - ), - ), - const SizedBox(height: 30), - ElevatedButton( - onPressed: selectInterface, - child: Text(AppLocalizations.of(context)!.selectInterface) - ), - ], - ); - } - - case 2: - return SizedBox( - width: double.maxFinite, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon( - Icons.error, - color: Colors.red, - size: 50, - ), - const SizedBox(height: 30), - Text( - AppLocalizations.of(context)!.dhcpSettingsNotLoaded, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ) - ], - ), - ); - - default: - return const SizedBox(); - } + if (statusProvider.serverStatus?.dhcpAvailable != true) { + return const DhcpNotAvailable(); } - return Scaffold( appBar: AppBar( title: Text(AppLocalizations.of(context)!.dhcpSettings), + centerTitle: false, + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, actions: selectedInterface != null ? [ IconButton( onPressed: checkDataValid() == true @@ -810,7 +411,376 @@ class _DhcpWidgetState extends State { const SizedBox(width: 10) ] : null, ), - body: generateBody(), + body: SafeArea( + child: Builder( + builder: (context) { + switch (dhcpProvider.loadStatus) { + case LoadStatus.loading: + return SizedBox( + width: double.maxFinite, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.loadingDhcp, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ); + + case LoadStatus.loaded: + if (selectedInterface != null) { + return SingleChildScrollView( + child: Wrap( + children: [ + DhcpMainButton( + selectedInterface: selectedInterface, + enabled: enabled, + setEnabled: (v) => setState(() => enabled = v) + ), + if (selectedInterface!.ipv4Addresses.isNotEmpty) ...[ + SectionLabel( + label: AppLocalizations.of(context)!.ipv4settings, + padding: const EdgeInsets.only( + top: 24, left: 16, right: 16, bottom: 8 + ) + ), + _DhcpField( + icon: Icons.skip_previous_rounded, + label: AppLocalizations.of(context)!.startOfRange, + controller: ipv4StartRangeController, + onChanged: (value) => validateIpV4(value, 'ipv4StartRangeError', AppLocalizations.of(context)!.ipNotValid), + error: ipv4StartRangeError + ), + _DhcpField( + icon: Icons.skip_next_rounded, + label: AppLocalizations.of(context)!.endOfRange, + controller: ipv4EndRangeController, + onChanged: (value) => validateIpV4(value, 'ipv4EndRangeError', AppLocalizations.of(context)!.ipNotValid), + error: ipv4EndRangeError + ), + _DhcpField( + icon: Icons.hub_rounded, + label: AppLocalizations.of(context)!.subnetMask, + controller: ipv4SubnetMaskController, + onChanged: (value) => validateIpV4(value, 'ipv4SubnetMaskError', AppLocalizations.of(context)!.subnetMaskNotValid), + error: ipv4SubnetMaskError + ), + _DhcpField( + icon: Icons.router_rounded, + label: AppLocalizations.of(context)!.gateway, + controller: ipv4GatewayController, + onChanged: (value) => validateIpV4(value, 'ipv4GatewayError', AppLocalizations.of(context)!.gatewayNotValid), + error: ipv4GatewayError + ), + _DhcpField( + icon: Icons.timer, + label: AppLocalizations.of(context)!.leaseTime, + controller: ipv4LeaseTimeController, + onChanged: (value) { + if (int.tryParse(value).runtimeType == int) { + setState(() => ipv4LeaseTimeError = null); + } + else { + setState(() => ipv4LeaseTimeError = AppLocalizations.of(context)!.leaseTimeNotValid); + } + }, + error: ipv4LeaseTimeError + ), + ], + if (selectedInterface!.ipv6Addresses.isNotEmpty) ...[ + SectionLabel( + label: AppLocalizations.of(context)!.ipv6settings, + padding: const EdgeInsets.all(16) + ), + _DhcpField( + icon: Icons.skip_next_rounded, + label: AppLocalizations.of(context)!.startOfRange, + controller: ipv6StartRangeController, + onChanged: (value) => validateIpV6(value, 'ipv6StartRangeError', AppLocalizations.of(context)!.ipNotValid), + error: ipv6StartRangeError + ), + _DhcpField( + icon: Icons.skip_previous_rounded, + label: AppLocalizations.of(context)!.endOfRange, + controller: ipv6EndRangeController, + onChanged: (value) => validateIpV6(value, 'ipv6EndRangeError', AppLocalizations.of(context)!.ipNotValid), + error: ipv6EndRangeError + ), + _DhcpField( + icon: Icons.timer, + label: AppLocalizations.of(context)!.leaseTime, + controller: ipv6LeaseTimeController, + onChanged: (value) { + if (int.tryParse(value).runtimeType == int) { + setState(() => ipv6LeaseTimeError = null); + } + else { + setState(() => ipv6LeaseTimeError = AppLocalizations.of(context)!.leaseTimeNotValid); + } + }, + error: ipv6LeaseTimeError + ) + ], + const SizedBox(height: 20), + SectionLabel( + label: AppLocalizations.of(context)!.dhcpLeases, + padding: const EdgeInsets.all(16), + ), + if (width <= 900) Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => DhcpLeases( + items: dhcpProvider.dhcp!.dhcpStatus!.leases, + staticLeases: false, + ) + ) + ); + }, + child: Container( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + AppLocalizations.of(context)!.dhcpLeases, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + Icon( + Icons.arrow_forward_rounded, + color: Theme.of(context).colorScheme.onSurface, + ) + ], + ), + ), + ), + ), + if (width <= 900) Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => DhcpLeases( + items: dhcpProvider.dhcp!.dhcpStatus!.staticLeases, + staticLeases: true, + ) + ) + ); + }, + child: Container( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + AppLocalizations.of(context)!.dhcpStatic, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + Icon( + Icons.arrow_forward_rounded, + color: Theme.of(context).colorScheme.onSurface, + ) + ], + ), + ), + ), + ), + if (width > 900) Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + onPressed: () { + if (!(Platform.isAndroid || Platform.isIOS)) { + Navigator.of(settingsNavigatorKey.currentContext!).push( + MaterialPageRoute(builder: (ctx) => DhcpLeases( + items: dhcpProvider.dhcp!.dhcpStatus!.leases, + staticLeases: false, + )) + ); + } + else { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => DhcpLeases( + items: dhcpProvider.dhcp!.dhcpStatus!.leases, + staticLeases: false, + ) + ) + ); + } + }, + child: Row( + children: [ + Text(AppLocalizations.of(context)!.dhcpLeases), + const SizedBox(width: 8), + const Icon(Icons.arrow_forward_rounded) + ], + ) + ), + ElevatedButton( + onPressed: () { + if (!(Platform.isAndroid || Platform.isIOS)) { + Navigator.of(settingsNavigatorKey.currentContext!).push( + MaterialPageRoute(builder: (ctx) => DhcpLeases( + items: dhcpProvider.dhcp!.dhcpStatus!.staticLeases, + staticLeases: true, + )) + ); + } + else { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => DhcpLeases( + items: dhcpProvider.dhcp!.dhcpStatus!.staticLeases, + staticLeases: true, + ) + ) + ); + } + }, + child: Row( + children: [ + Text(AppLocalizations.of(context)!.dhcpStatic), + const SizedBox(width: 8), + const Icon(Icons.arrow_forward_rounded) + ], + ) + ), + ], + ), + const SizedBox(height: 10) + ], + ), + ); + } + else { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + AppLocalizations.of(context)!.neededSelectInterface, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5) + ), + ), + ), + const SizedBox(height: 30), + ElevatedButton( + onPressed: selectInterface, + child: Text(AppLocalizations.of(context)!.selectInterface) + ), + ], + ), + ), + ], + ); + } + + case LoadStatus.error: + return SizedBox( + width: double.maxFinite, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon( + Icons.error, + color: Colors.red, + size: 50, + ), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.dhcpSettingsNotLoaded, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ); + + default: + return const SizedBox(); + } + }, + ), + ) + ); + } +} + +class _DhcpField extends StatelessWidget { + final IconData icon; + final String label; + final TextEditingController controller; + final void Function(String) onChanged; + final String? error; + + const _DhcpField({ + required this.icon, + required this.label, + required this.controller, + required this.onChanged, + required this.error, + }); + + @override + Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width; + + return FractionallySizedBox( + widthFactor: width > 900 ? 0.5 : 1, + child: Padding( + padding: width > 900 + ? const EdgeInsets.only(top: 12, bottom: 12, left: 16, right: 8) + : const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: TextFormField( + controller: controller, + onChanged: onChanged, + decoration: InputDecoration( + prefixIcon: Icon(icon), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + errorText: error, + labelText: label, + ), + keyboardType: TextInputType.number, + ), + ), ); } } \ No newline at end of file diff --git a/lib/screens/settings/dhcp/dhcp_interface_item.dart b/lib/screens/settings/dhcp/dhcp_interface_item.dart new file mode 100644 index 0000000..9d68f4e --- /dev/null +++ b/lib/screens/settings/dhcp/dhcp_interface_item.dart @@ -0,0 +1,139 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/models/dhcp.dart'; + +class DhcpInterfaceItem extends StatelessWidget { + final NetworkInterface networkInterface; + final void Function(NetworkInterface) onSelect; + + const DhcpInterfaceItem({ + super.key, + required this.networkInterface, + required this.onSelect + }); + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + Navigator.pop(context); + onSelect(networkInterface); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + networkInterface.name, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface + ), + ), + Row( + children: [ + Text( + "${AppLocalizations.of(context)!.hardwareAddress}: ", + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ), + Text( + networkInterface.hardwareAddress, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ), + ], + ), + const SizedBox(height: 5), + if (networkInterface.flags.isNotEmpty) ...[ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Flags: ", + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ), + Flexible( + child: Text( + networkInterface.flags.join(', '), + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ), + ), + ], + ), + const SizedBox(height: 5), + ], + if (networkInterface.gatewayIp != null && networkInterface.gatewayIp != '') ...[ + Row( + children: [ + Text( + "${AppLocalizations.of(context)!.gatewayIp}: ", + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ), + Text( + networkInterface.gatewayIp!, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ), + ], + ), + const SizedBox(height: 5), + ], + if (networkInterface.ipv4Addresses.isNotEmpty) ...[ + Row( + children: [ + Flexible( + child: Text( + "${AppLocalizations.of(context)!.ipv4addresses}: ${networkInterface.ipv4Addresses.join(', ')}", + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ), + ) + ], + ), + const SizedBox(height: 5), + ], + if (networkInterface.ipv6Addresses.isNotEmpty) ...[ + Row( + children: [ + Flexible( + child: Text( + "${AppLocalizations.of(context)!.ipv6addresses}: ${networkInterface.ipv6Addresses.join(', ')}", + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ), + ) + ], + ), + ] + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/settings/dhcp/dhcp_leases.dart b/lib/screens/settings/dhcp/dhcp_leases.dart index aed4f47..7b90d13 100644 --- a/lib/screens/settings/dhcp/dhcp_leases.dart +++ b/lib/screens/settings/dhcp/dhcp_leases.dart @@ -1,5 +1,8 @@ // ignore_for_file: use_build_context_synchronously +import 'dart:io'; + +import 'package:adguard_home_manager/functions/desktop_mode.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:animations/animations.dart'; @@ -8,56 +11,46 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/screens/settings/dhcp/delete_static_lease_modal.dart'; import 'package:adguard_home_manager/screens/settings/dhcp/add_static_lease_modal.dart'; +import 'package:adguard_home_manager/providers/dhcp_provider.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; import 'package:adguard_home_manager/functions/snackbar.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; import 'package:adguard_home_manager/classes/process_modal.dart'; import 'package:adguard_home_manager/models/dhcp.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; class DhcpLeases extends StatelessWidget { final List items; final bool staticLeases; const DhcpLeases({ - Key? key, + super.key, required this.items, required this.staticLeases, - }) : super(key: key); + }); @override Widget build(BuildContext context) { - final serversProvider = Provider.of(context); + final dhcpProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + void deleteLease(Lease lease) async { - ProcessModal processModal = ProcessModal(context: context); + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.deleting); - final result = await deleteStaticLease(server: serversProvider.selectedServer!, data: { - "mac": lease.mac, - "ip": lease.ip, - "hostname": lease.hostname - }); + final result = await dhcpProvider.deleteLease(lease); processModal.close(); - if (result['result'] == 'success') { - DhcpData data = serversProvider.dhcp.data!; - data.dhcpStatus.staticLeases = data.dhcpStatus.staticLeases.where((l) => l.mac != lease.mac).toList(); - serversProvider.setDhcpData(data); - - showSnacbkar( - context: context, + if (result == true) { + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.staticLeaseDeleted, color: Colors.green ); } else { - appConfigProvider.addLog(result['log']); - showSnacbkar( - context: context, + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.staticLeaseNotDeleted, color: Colors.red @@ -66,51 +59,36 @@ class DhcpLeases extends StatelessWidget { } void createLease(Lease lease) async { - ProcessModal processModal = ProcessModal(context: context); + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.creating); - final result = await createStaticLease(server: serversProvider.selectedServer!, data: { - "mac": lease.mac, - "ip": lease.ip, - "hostname": lease.hostname, - }); + final result = await dhcpProvider.createLease(lease); processModal.close(); - if (result['result'] == 'success') { - DhcpData data = serversProvider.dhcp.data!; - data.dhcpStatus.staticLeases.add(lease); - serversProvider.setDhcpData(data); - - showSnacbkar( - context: context, + if (result.successful == true) { + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.staticLeaseCreated, color: Colors.green ); } - else if (result['result'] == 'error' && result['message'] == 'already_exists' ) { - appConfigProvider.addLog(result['log']); - showSnacbkar( - context: context, + else if (result.successful == false && result.content == "already_exists") { + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.staticLeaseExists, color: Colors.red ); } - else if (result['result'] == 'error' && result['message'] == 'server_not_configured' ) { - appConfigProvider.addLog(result['log']); - showSnacbkar( - context: context, + else if (result.successful == false && result.content == "server_not_configured") { + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.serverNotConfigured, color: Colors.red ); } else { - appConfigProvider.addLog(result['log']); - showSnacbkar( - context: context, + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.staticLeaseNotCreated, color: Colors.red @@ -119,18 +97,32 @@ class DhcpLeases extends StatelessWidget { } void openAddStaticLease() { - showModalBottomSheet( - context: context, - builder: (context) => AddStaticLeaseModal( - onConfirm: createLease - ), - backgroundColor: Colors.transparent, - isScrollControlled: true - ); + if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + context: context, + builder: (context) => AddStaticLeaseModal( + onConfirm: createLease, + dialog: true, + ), + ); + } + else { + showModalBottomSheet( + context: context, + useRootNavigator: true, + builder: (context) => AddStaticLeaseModal( + onConfirm: createLease, + dialog: false, + ), + backgroundColor: Colors.transparent, + isScrollControlled: true + ); + } } return Scaffold( appBar: AppBar( + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, title: Text( staticLeases == true ? AppLocalizations.of(context)!.dhcpStatic @@ -138,32 +130,34 @@ class DhcpLeases extends StatelessWidget { ), ), body: items.isNotEmpty - ? ListView.builder( - padding: const EdgeInsets.only(top: 0), - itemCount: items.length, - itemBuilder: (context, index) => ListTile( - isThreeLine: true, - title: Text(items[index].ip), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(items[index].mac), - Text(items[index].hostname), - ], + ? SafeArea( + child: ListView.builder( + padding: const EdgeInsets.only(top: 0), + itemCount: items.length, + itemBuilder: (context, index) => ListTile( + isThreeLine: true, + title: Text(items[index].ip), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(items[index].mac), + Text(items[index].hostname), + ], + ), + trailing: staticLeases == true + ? IconButton( + onPressed: () { + showModal( + context: context, + builder: (context) => DeleteStaticLeaseModal( + onConfirm: () => deleteLease(items[index]) + ) + ); + }, + icon: const Icon(Icons.delete) + ) + : null, ), - trailing: staticLeases == true - ? IconButton( - onPressed: () { - showModal( - context: context, - builder: (context) => DeleteStaticLeaseModal( - onConfirm: () => deleteLease(items[index]) - ) - ); - }, - icon: const Icon(Icons.delete) - ) - : null, ), ) : Center( diff --git a/lib/screens/settings/dhcp/dhcp_main_button.dart b/lib/screens/settings/dhcp/dhcp_main_button.dart new file mode 100644 index 0000000..3e46a14 --- /dev/null +++ b/lib/screens/settings/dhcp/dhcp_main_button.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/models/dhcp.dart'; + +class DhcpMainButton extends StatelessWidget { + final NetworkInterface? selectedInterface; + final bool enabled; + final void Function(bool) setEnabled; + + const DhcpMainButton({ + super.key, + required this.selectedInterface, + required this.enabled, + required this.setEnabled, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only( + top: 10, + left: 16, + right: 16 + ), + child: Material( + color: Theme.of(context).colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(28), + child: InkWell( + onTap: selectedInterface != null + ? () => setEnabled(!enabled) + : null, + borderRadius: BorderRadius.circular(28), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 12 + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppLocalizations.of(context)!.enableDhcpServer, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface + ), + ), + if (selectedInterface != null) ...[ + Text( + selectedInterface!.name, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).listTileTheme.textColor + ), + ) + ] + ], + ), + Switch( + value: enabled, + onChanged: selectedInterface != null + ? (value) => setEnabled(value) + : null, + ), + ], + ), + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/settings/dhcp/dhcp_not_available.dart b/lib/screens/settings/dhcp/dhcp_not_available.dart new file mode 100644 index 0000000..ceb3f6b --- /dev/null +++ b/lib/screens/settings/dhcp/dhcp_not_available.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/functions/desktop_mode.dart'; + +class DhcpNotAvailable extends StatelessWidget { + const DhcpNotAvailable({super.key}); + + @override + Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width; + + return Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context)!.dhcpSettings), + centerTitle: false, + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + AppLocalizations.of(context)!.dhcpNotAvailable, + textAlign: TextAlign.center, + style: const TextStyle( + fontWeight: FontWeight.w400, + fontSize: 24 + ), + ), + const SizedBox(height: 20), + Text( + AppLocalizations.of(context)!.osServerInstalledIncompatible, + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 16, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/settings/dhcp/select_interface_modal.dart b/lib/screens/settings/dhcp/select_interface_modal.dart index 49b5957..40291e9 100644 --- a/lib/screens/settings/dhcp/select_interface_modal.dart +++ b/lib/screens/settings/dhcp/select_interface_modal.dart @@ -1,203 +1,110 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:adguard_home_manager/screens/settings/dhcp/dhcp_interface_item.dart'; +import 'package:adguard_home_manager/widgets/list_bottom_sheet.dart'; + import 'package:adguard_home_manager/models/dhcp.dart'; class SelectInterfaceModal extends StatelessWidget { final List interfaces; - final ScrollController scrollController; final void Function(NetworkInterface) onSelect; + final bool dialog; const SelectInterfaceModal({ - Key? key, + super.key, required this.interfaces, - required this.scrollController, required this.onSelect, - }) : super(key: key); + required this.dialog + }); @override Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - color: Theme.of(context).dialogBackgroundColor, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28) - ) - ), - child: Column( - children: [ - Expanded( - child: ListView( - controller: scrollController, - children: [ - Padding( - padding: const EdgeInsets.only(top: 24), - child: 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 - ), - ), - const SizedBox(height: 16), - ListView.builder( - primary: false, - shrinkWrap: true, - itemCount: interfaces.length, - itemBuilder: (context, index) => Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - Navigator.pop(context); - onSelect(interfaces[index]); - }, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + if (dialog == true) { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Wrap( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( children: [ + Icon( + Icons.settings_ethernet_rounded, + size: 24, + color: Theme.of(context).listTileTheme.iconColor + ), + const SizedBox(height: 16), Text( - interfaces[index].name, + AppLocalizations.of(context)!.selectInterface, + textAlign: TextAlign.center, style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w400, + fontSize: 24, color: Theme.of(context).colorScheme.onSurface ), - ), - Row( - children: [ - Text( - "${AppLocalizations.of(context)!.hardwareAddress}: ", - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.onSurfaceVariant - ), - ), - Text( - interfaces[index].hardwareAddress, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.onSurfaceVariant - ), - ), - ], - ), - const SizedBox(height: 5), - if (interfaces[index].flags.isNotEmpty) ...[ - Row( - children: [ - Text( - "Flags: ", - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.onSurfaceVariant - ), - ), - Text( - interfaces[index].flags.join(', '), - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.onSurfaceVariant - ), - ), - ], ), - const SizedBox(height: 5), - ], - if (interfaces[index].gatewayIp != '') ...[ - Row( - children: [ - Text( - "${AppLocalizations.of(context)!.gatewayIp}: ", - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.onSurfaceVariant - ), - ), - Text( - interfaces[index].gatewayIp, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.onSurfaceVariant - ), - ), - ], - ), - const SizedBox(height: 5), - ], - if (interfaces[index].ipv4Addresses.isNotEmpty) ...[ - Row( - children: [ - Text( - "${AppLocalizations.of(context)!.ipv4addresses}: ", - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.onSurfaceVariant - ), - ), - Text( - interfaces[index].ipv4Addresses.join(', '), - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.onSurfaceVariant - ), - ), - ], - ), - const SizedBox(height: 5), - ], - if (interfaces[index].ipv6Addresses.isNotEmpty) ...[ - Row( - children: [ - Text( - "${AppLocalizations.of(context)!.ipv4addresses}: ", - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.onSurfaceVariant - ), - ), - Text( - interfaces[index].ipv6Addresses.join(', '), - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.onSurfaceVariant - ), - ), - ], - ), - ] ], ), - ), + ], ), + const SizedBox(height: 16), + ], + ), + ), + Expanded( + child: ListView.builder( + itemCount: interfaces.length, + itemBuilder: (context, index) => DhcpInterfaceItem( + networkInterface: interfaces[index], + onSelect: onSelect ) ), - ], - ), + ), + Padding( + padding: const EdgeInsets.all(20), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.cancel) + ) + ], + ), + ), + if (Platform.isIOS) const SizedBox(height: 16) + ], ), - Padding( - padding: const EdgeInsets.all(20), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(AppLocalizations.of(context)!.cancel) - ) - ], - ), - ) - ], - ), - ); + ), + ); + } + else { + return ListBottomSheet( + icon: Icons.settings_ethernet_rounded, + title: AppLocalizations.of(context)!.selectInterface, + children: [ + ListView.builder( + primary: false, + shrinkWrap: true, + itemCount: interfaces.length, + itemBuilder: (context, index) => DhcpInterfaceItem( + networkInterface: interfaces[index], + onSelect: onSelect + ) + ), + ] + ); + } } } \ No newline at end of file diff --git a/lib/screens/settings/dns/bootstrap_dns.dart b/lib/screens/settings/dns/bootstrap_dns.dart index 32f02ed..dba929a 100644 --- a/lib/screens/settings/dns/bootstrap_dns.dart +++ b/lib/screens/settings/dns/bootstrap_dns.dart @@ -1,23 +1,18 @@ // ignore_for_file: use_build_context_synchronously +import 'package:adguard_home_manager/constants/regexps.dart'; +import 'package:adguard_home_manager/functions/desktop_mode.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/providers/servers_provider.dart'; import 'package:adguard_home_manager/classes/process_modal.dart'; +import 'package:adguard_home_manager/providers/dns_provider.dart'; import 'package:adguard_home_manager/functions/snackbar.dart'; -import 'package:adguard_home_manager/models/dns_info.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; class BootstrapDnsScreen extends StatefulWidget { - final ServersProvider serversProvider; - - const BootstrapDnsScreen({ - Key? key, - required this.serversProvider, - }) : super(key: key); + const BootstrapDnsScreen({super.key}); @override State createState() => _BootstrapDnsScreenState(); @@ -29,8 +24,7 @@ class _BootstrapDnsScreenState extends State { bool validValues = false; void validateIp(Map field, String value) { - RegExp ipAddress = RegExp(r'(?:^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}$)|(?:^(?:(?:[a-fA-F\d]{1,4}:){7}(?:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){5}(?::(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,2}|:)|(?:[a-fA-F\d]{1,4}:){4}(?:(?::[a-fA-F\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,3}|:)|(?:[a-fA-F\d]{1,4}:){3}(?:(?::[a-fA-F\d]{1,4}){0,2}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,4}|:)|(?:[a-fA-F\d]{1,4}:){2}(?:(?::[a-fA-F\d]{1,4}){0,3}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,5}|:)|(?:[a-fA-F\d]{1,4}:){1}(?:(?::[a-fA-F\d]{1,4}){0,4}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,6}|:)|(?::(?:(?::[a-fA-F\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,7}|:)))(?:%[0-9a-zA-Z]{1,})?$)'); - if (ipAddress.hasMatch(value) == true) { + if (Regexps.ipv4Address.hasMatch(value) == true || Regexps.ipv6Address.hasMatch(value) == true) { setState(() => field['error'] = null); } else { @@ -54,7 +48,9 @@ class _BootstrapDnsScreenState extends State { @override void initState() { - for (var item in widget.serversProvider.dnsInfo.data!.bootstrapDns) { + final dnsProvider = Provider.of(context, listen: false); + + for (var item in dnsProvider.dnsInfo!.bootstrapDns) { final controller = TextEditingController(); controller.text = item; bootstrapControllers.add({ @@ -68,46 +64,37 @@ class _BootstrapDnsScreenState extends State { @override Widget build(BuildContext context) { - final serversProvider = Provider.of(context); + final dnsProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + void saveData() async { - ProcessModal processModal = ProcessModal(context: context); + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.savingConfig); - final result = await setDnsConfig(server: serversProvider.selectedServer!, data: { + final result = await dnsProvider.saveBootstrapDnsConfig({ "bootstrap_dns": bootstrapControllers.map((e) => e['controller'].text).toList(), }); processModal.close(); - if (result['result'] == 'success') { - DnsInfoData data = serversProvider.dnsInfo.data!; - data.bootstrapDns = List.from(bootstrapControllers.map((e) => e['controller'].text)); - serversProvider.setDnsInfoData(data); - - showSnacbkar( - context: context, + if (result.successful == true) { + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.dnsConfigSaved, color: Colors.green ); } - else if (result['log'] != null && result['log'].statusCode == '400') { - appConfigProvider.addLog(result['log']); - - showSnacbkar( - context: context, + else if (result.successful == false && result.statusCode == 400) { + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.someValueNotValid, color: Colors.red ); } else { - appConfigProvider.addLog(result['log']); - - showSnacbkar( - context: context, + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.dnsConfigNotSaved, color: Colors.red @@ -118,6 +105,7 @@ class _BootstrapDnsScreenState extends State { return Scaffold( appBar: AppBar( title: Text(AppLocalizations.of(context)!.bootstrapDns), + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, actions: [ IconButton( onPressed: validValues == true @@ -129,105 +117,107 @@ class _BootstrapDnsScreenState extends State { const SizedBox(width: 10) ], ), - body: ListView( - padding: const EdgeInsets.only(top: 10), - children: [ - Card( - margin: const EdgeInsets.only( - left: 16, right: 16, bottom: 20 - ), - child: Padding( - padding: const EdgeInsets.all(20), - child: Row( - children: [ - Icon( - Icons.info_rounded, - color: Theme.of(context).listTileTheme.iconColor, - ), - const SizedBox(width: 20), - Flexible( - child: Text( - AppLocalizations.of(context)!.bootstrapDnsServersInfo, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface - ), + body: SafeArea( + child: ListView( + padding: const EdgeInsets.only(top: 10), + children: [ + Card( + margin: const EdgeInsets.only( + left: 16, right: 16, bottom: 20 + ), + child: Padding( + padding: const EdgeInsets.all(20), + child: Row( + children: [ + Icon( + Icons.info_rounded, + color: Theme.of(context).listTileTheme.iconColor, + ), + const SizedBox(width: 20), + Flexible( + child: Text( + AppLocalizations.of(context)!.bootstrapDnsServersInfo, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface + ), + ) ) - ) - ], + ], + ), ), ), - ), - const SizedBox(height: 10), - if (bootstrapControllers.isEmpty) Column( - children: [ - Padding( - padding: const EdgeInsets.all(10), - child: Center( - child: Text( - AppLocalizations.of(context)!.noBootstrapDns, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontSize: 16 + const SizedBox(height: 10), + if (bootstrapControllers.isEmpty) Column( + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: Center( + child: Text( + AppLocalizations.of(context)!.noBootstrapDns, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 16 + ), ), ), ), - ), - const SizedBox(height: 20), - ], - ), - ...bootstrapControllers.map((c) => Padding( - padding: const EdgeInsets.only( - left: 16, right: 6, bottom: 20 - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SizedBox( - width: MediaQuery.of(context).size.width-74, - child: TextFormField( - controller: c['controller'], - onChanged: (value) => validateIp(c, value), - decoration: InputDecoration( - prefixIcon: const Icon(Icons.dns_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - errorText: c['error'], - labelText: AppLocalizations.of(context)!.dnsServer, - ) - ), - ), - IconButton( - onPressed: () { - setState(() => bootstrapControllers = bootstrapControllers.where((con) => con != c).toList()); - checkValidValues(); - }, - icon: const Icon(Icons.remove_circle_outline) - ) + const SizedBox(height: 20), ], ), - )).toList(), - Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - ElevatedButton.icon( - onPressed: () { - setState(() => bootstrapControllers.add({ - 'controller': TextEditingController(), - 'error': null - })); - checkValidValues(); - }, - icon: const Icon(Icons.add), - label: Text(AppLocalizations.of(context)!.addItem) + ...bootstrapControllers.map((c) => Padding( + padding: const EdgeInsets.only( + left: 16, right: 6, bottom: 20 ), - ], - ), - const SizedBox(height: 20) - ], + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: TextFormField( + controller: c['controller'], + onChanged: (value) => validateIp(c, value), + decoration: InputDecoration( + prefixIcon: const Icon(Icons.dns_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + errorText: c['error'], + labelText: AppLocalizations.of(context)!.dnsServer, + ) + ), + ), + const SizedBox(width: 8), + IconButton( + onPressed: () { + setState(() => bootstrapControllers = bootstrapControllers.where((con) => con != c).toList()); + checkValidValues(); + }, + icon: const Icon(Icons.remove_circle_outline) + ) + ], + ), + )), + Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + ElevatedButton.icon( + onPressed: () { + setState(() => bootstrapControllers.add({ + 'controller': TextEditingController(), + 'error': null + })); + checkValidValues(); + }, + icon: const Icon(Icons.add), + label: Text(AppLocalizations.of(context)!.addItem) + ), + ], + ), + const SizedBox(height: 20) + ], + ), ), ); } diff --git a/lib/screens/settings/dns/cache_config.dart b/lib/screens/settings/dns/cache_config.dart index 16090fb..e2edb4d 100644 --- a/lib/screens/settings/dns/cache_config.dart +++ b/lib/screens/settings/dns/cache_config.dart @@ -7,22 +7,16 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/widgets/custom_switch_list_tile.dart'; import 'package:adguard_home_manager/screens/settings/dns/clear_dns_cache_dialog.dart'; +import 'package:adguard_home_manager/functions/desktop_mode.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart'; - +import 'package:adguard_home_manager/providers/dns_provider.dart'; import 'package:adguard_home_manager/classes/process_modal.dart'; import 'package:adguard_home_manager/functions/clear_dns_cache.dart'; import 'package:adguard_home_manager/functions/snackbar.dart'; -import 'package:adguard_home_manager/models/dns_info.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; class CacheConfigDnsScreen extends StatefulWidget { - final ServersProvider serversProvider; - - const CacheConfigDnsScreen({ - Key? key, - required this.serversProvider - }) : super(key: key); + const CacheConfigDnsScreen({super.key}); @override State createState() => _CacheConfigDnsScreenState(); @@ -60,10 +54,12 @@ class _CacheConfigDnsScreenState extends State { @override void initState() { - cacheSizeController.text = widget.serversProvider.dnsInfo.data!.cacheSize.toString(); - overrideMinTtlController.text = widget.serversProvider.dnsInfo.data!.cacheTtlMin.toString(); - overrideMaxTtlController.text = widget.serversProvider.dnsInfo.data!.cacheTtlMax.toString(); - optimisticCache = widget.serversProvider.dnsInfo.data!.cacheOptimistic; + final dnsProvider = Provider.of(context, listen: false); + + cacheSizeController.text = dnsProvider.dnsInfo!.cacheSize.toString(); + overrideMinTtlController.text = dnsProvider.dnsInfo!.cacheTtlMin.toString(); + overrideMaxTtlController.text = dnsProvider.dnsInfo!.cacheTtlMax.toString(); + optimisticCache = dnsProvider.dnsInfo!.cacheOptimistic ?? false; validData = true; super.initState(); } @@ -71,13 +67,16 @@ class _CacheConfigDnsScreenState extends State { @override Widget build(BuildContext context) { final serversProvider = Provider.of(context); + final dnsProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + void saveData() async { - ProcessModal processModal = ProcessModal(context: context); + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.savingConfig); - final result = await setDnsConfig(server: serversProvider.selectedServer!, data: { + final result = await dnsProvider.saveCacheCacheConfig({ "cache_size": int.parse(cacheSizeController.text), "cache_ttl_min": int.parse(overrideMinTtlController.text), "cache_ttl_max": int.parse(overrideMaxTtlController.text), @@ -86,36 +85,22 @@ class _CacheConfigDnsScreenState extends State { processModal.close(); - if (result['result'] == 'success') { - DnsInfoData data = serversProvider.dnsInfo.data!; - data.cacheSize = int.parse(cacheSizeController.text); - data.cacheTtlMin = int.parse(overrideMinTtlController.text); - data.cacheTtlMax = int.parse(overrideMaxTtlController.text); - data.cacheOptimistic = optimisticCache; - serversProvider.setDnsInfoData(data); - - showSnacbkar( - context: context, + if (result.successful == true) { + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.dnsConfigSaved, color: Colors.green ); } - else if (result['log'] != null && result['log'].statusCode == '400') { - appConfigProvider.addLog(result['log']); - - showSnacbkar( - context: context, + else if (result.successful== false && result.statusCode == 400) { + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.someValueNotValid, color: Colors.red ); } else { - appConfigProvider.addLog(result['log']); - - showSnacbkar( - context: context, + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.dnsConfigNotSaved, color: Colors.red @@ -153,17 +138,15 @@ class _CacheConfigDnsScreenState extends State { void clearCache() async { final result = await clearDnsCache(context, serversProvider.selectedServer!); - if (result == true) { - showSnacbkar( - context: context, + if (result.successful == true) { + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.dnsCacheCleared, color: Colors.green ); } else { - showSnacbkar( - context: context, + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.dnsCacheNotCleared, color: Colors.red @@ -174,6 +157,7 @@ class _CacheConfigDnsScreenState extends State { return Scaffold( appBar: AppBar( title: Text(AppLocalizations.of(context)!.dnsCacheConfig), + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, actions: [ IconButton( onPressed: validData == true @@ -185,80 +169,83 @@ class _CacheConfigDnsScreenState extends State { const SizedBox(width: 10) ], ), - body: ListView( - padding: const EdgeInsets.only(top: 10), - children: [ - numericField( - controller: cacheSizeController, - label: AppLocalizations.of(context)!.cacheSize, - helper: AppLocalizations.of(context)!.inBytes, - error: cacheSizeError, - onChanged: (value) { - if (int.tryParse(value) != null) { - setState(() => cacheSizeError = null); + body: SafeArea( + child: ListView( + padding: const EdgeInsets.only(top: 10), + children: [ + numericField( + controller: cacheSizeController, + label: AppLocalizations.of(context)!.cacheSize, + helper: AppLocalizations.of(context)!.inBytes, + error: cacheSizeError, + onChanged: (value) { + if (int.tryParse(value) != null) { + setState(() => cacheSizeError = null); + } + else { + setState(() => cacheSizeError = AppLocalizations.of(context)!.valueNotNumber); + } + checkValidData(); } - else { - setState(() => cacheSizeError = AppLocalizations.of(context)!.valueNotNumber); + ), + const SizedBox(height: 30), + numericField( + controller: overrideMinTtlController, + label: AppLocalizations.of(context)!.overrideMinimumTtl, + helper: AppLocalizations.of(context)!.overrideMinimumTtlDescription, + error: overrideMinTtlError, + onChanged: (value) { + if (int.tryParse(value) != null) { + setState(() => overrideMinTtlError = null); + } + else { + setState(() => overrideMinTtlError = AppLocalizations.of(context)!.valueNotNumber); + } + checkValidData(); } - checkValidData(); - } - ), - const SizedBox(height: 30), - numericField( - controller: overrideMinTtlController, - label: AppLocalizations.of(context)!.overrideMinimumTtl, - helper: AppLocalizations.of(context)!.overrideMinimumTtlDescription, - error: overrideMinTtlError, - onChanged: (value) { - if (int.tryParse(value) != null) { - setState(() => overrideMinTtlError = null); + ), + const SizedBox(height: 30), + numericField( + controller: overrideMaxTtlController, + label: AppLocalizations.of(context)!.overrideMaximumTtl, + helper: AppLocalizations.of(context)!.overrideMaximumTtlDescription, + error: overrideMaxTtlError, + onChanged: (value) { + if (int.tryParse(value) != null) { + setState(() => overrideMaxTtlError = null); + } + else { + setState(() => overrideMaxTtlError = AppLocalizations.of(context)!.valueNotNumber); + } + checkValidData(); } - else { - setState(() => overrideMinTtlError = AppLocalizations.of(context)!.valueNotNumber); - } - checkValidData(); - } - ), - const SizedBox(height: 30), - numericField( - controller: overrideMaxTtlController, - label: AppLocalizations.of(context)!.overrideMaximumTtl, - helper: AppLocalizations.of(context)!.overrideMaximumTtlDescription, - error: overrideMaxTtlError, - onChanged: (value) { - if (int.tryParse(value) != null) { - setState(() => overrideMaxTtlError = null); - } - else { - setState(() => overrideMaxTtlError = AppLocalizations.of(context)!.valueNotNumber); - } - checkValidData(); - } - ), - const SizedBox(height: 10), - CustomSwitchListTile( - value: optimisticCache, - onChanged: (value) => setState(() => optimisticCache = value), - title: AppLocalizations.of(context)!.optimisticCaching, - subtitle: AppLocalizations.of(context)!.optimisticCachingDescription, - ), - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ElevatedButton.icon( - onPressed: () => showDialog( - context: context, - builder: (context) => ClearDnsCacheDialog( - onConfirm: clearCache - ) - ), - icon: const Icon(Icons.delete_rounded), - label: Text(AppLocalizations.of(context)!.clearDnsCache), - ), - ], - ) - ], + ), + const SizedBox(height: 10), + CustomSwitchListTile( + value: optimisticCache, + onChanged: (value) => setState(() => optimisticCache = value), + title: AppLocalizations.of(context)!.optimisticCaching, + subtitle: AppLocalizations.of(context)!.optimisticCachingDescription, + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton.icon( + onPressed: () => showDialog( + context: context, + builder: (context) => ClearDnsCacheDialog( + onConfirm: clearCache + ) + ), + icon: const Icon(Icons.delete_rounded), + label: Text(AppLocalizations.of(context)!.clearDnsCache), + ), + ], + ), + const SizedBox(height: 16) + ], + ), ), ); } diff --git a/lib/screens/settings/dns/clear_dns_cache_dialog.dart b/lib/screens/settings/dns/clear_dns_cache_dialog.dart index 425d11d..b04c889 100644 --- a/lib/screens/settings/dns/clear_dns_cache_dialog.dart +++ b/lib/screens/settings/dns/clear_dns_cache_dialog.dart @@ -5,9 +5,9 @@ class ClearDnsCacheDialog extends StatelessWidget { final void Function() onConfirm; const ClearDnsCacheDialog({ - Key? key, + super.key, required this.onConfirm - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -22,6 +22,7 @@ class ClearDnsCacheDialog extends StatelessWidget { const SizedBox(height: 16), Text( AppLocalizations.of(context)!.clearDnsCache, + textAlign: TextAlign.center, style: TextStyle( fontSize: 24, color: Theme.of(context).colorScheme.onSurface diff --git a/lib/screens/settings/dns/comment_modal.dart b/lib/screens/settings/dns/comment_modal.dart index 20ad859..92d4c0f 100644 --- a/lib/screens/settings/dns/comment_modal.dart +++ b/lib/screens/settings/dns/comment_modal.dart @@ -1,21 +1,70 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -class CommentModal extends StatefulWidget { +class CommentModal extends StatelessWidget { + final String? comment; + final void Function(String) onConfirm; + final bool dialog; + + const CommentModal({ + super.key, + this.comment, + required this.onConfirm, + required this.dialog + }); + + @override + Widget build(BuildContext context) { + if (dialog == true) { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 400 + ), + child: _Content( + comment: comment, + onConfirm: onConfirm, + ) + ), + ); + } + else { + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28) + ), + color: Theme.of(context).dialogBackgroundColor + ), + child: SafeArea( + child: _Content( + comment: comment, + onConfirm: onConfirm, + ), + ) + ), + ); + } + } +} + +class _Content extends StatefulWidget { final String? comment; final void Function(String) onConfirm; - const CommentModal({ - Key? key, - this.comment, + const _Content({ + required this.comment, required this.onConfirm - }) : super(key: key); + }); @override - State createState() => _CommentModalState(); + State<_Content> createState() => __ContentState(); } -class _CommentModalState extends State { +class __ContentState extends State<_Content> { final TextEditingController commentController = TextEditingController(); bool validData = false; @@ -30,103 +79,99 @@ class _CommentModalState extends State { @override Widget build(BuildContext context) { - return Padding( - padding: MediaQuery.of(context).viewInsets, - child: Container( - height: 310, - decoration: BoxDecoration( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28) - ), - color: Theme.of(context).dialogBackgroundColor - ), - child: Column( - children: [ - Expanded( - child: ListView( - physics: MediaQuery.of(context).size.height >= 330 == true - ? const NeverScrollableScrollPhysics() - : null, - children: [ - Padding( - padding: const EdgeInsets.only(top: 24), - child: Icon( - Icons.comment_rounded, - size: 24, - color: Theme.of(context).colorScheme.secondary, - ), - ), - const SizedBox(height: 16), - Text( - AppLocalizations.of(context)!.comment, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSurface - ), - ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: TextFormField( - controller: commentController, - onChanged: (value) { - if (value != '') { - setState(() => validData = true); - } - else { - setState(() => validData = false); - } - }, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.comment_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) + 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.comment_rounded, + size: 24, + color: Theme.of(context).colorScheme.secondary, + ), ), - labelText: AppLocalizations.of(context)!.comment, - helperText: AppLocalizations.of(context)!.commentsDescription, - helperMaxLines: 3 - ) + const SizedBox(height: 16), + Text( + AppLocalizations.of(context)!.comment, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + color: Theme.of(context).colorScheme.onSurface + ), + ), + const SizedBox(height: 16), + ], ), - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.all(24), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(AppLocalizations.of(context)!.cancel) - ), - const SizedBox(width: 20), - TextButton( - onPressed: validData == true - ? () { - Navigator.pop(context); - widget.onConfirm("# ${commentController.text}"); - } - : null, - child: Text( - AppLocalizations.of(context)!.confirm, - style: TextStyle( - color: validData == true - ? Theme.of(context).colorScheme.primary - : Colors.grey + ], + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: TextFormField( + controller: commentController, + onChanged: (value) { + if (value != '') { + setState(() => validData = true); + } + else { + setState(() => validData = false); + } + }, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.comment_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) ), + labelText: AppLocalizations.of(context)!.comment, + helperText: AppLocalizations.of(context)!.commentsDescription, + helperMaxLines: 3 ) ), - ], - ), - ) - ], + ), + ], + ), + ), ), - ), + Padding( + padding: const EdgeInsets.all(24), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.cancel) + ), + const SizedBox(width: 20), + TextButton( + onPressed: validData == true + ? () { + Navigator.pop(context); + widget.onConfirm("# ${commentController.text}"); + } + : null, + child: Text( + AppLocalizations.of(context)!.confirm, + style: TextStyle( + color: validData == true + ? Theme.of(context).colorScheme.primary + : Colors.grey + ), + ) + ), + ], + ), + ) + ], ); } } \ No newline at end of file diff --git a/lib/screens/settings/dns/dns.dart b/lib/screens/settings/dns/dns.dart index 229c167..564fff2 100644 --- a/lib/screens/settings/dns/dns.dart +++ b/lib/screens/settings/dns/dns.dart @@ -4,6 +4,9 @@ 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/settings.dart'; +import 'package:adguard_home_manager/screens/settings/dns/fallback_dns.dart'; +import 'package:adguard_home_manager/screens/settings/dns/test_upstream_dns_modal.dart'; import 'package:adguard_home_manager/screens/settings/dns/clear_dns_cache_dialog.dart'; import 'package:adguard_home_manager/screens/settings/dns/cache_config.dart'; import 'package:adguard_home_manager/screens/settings/dns/dns_server_settings.dart'; @@ -12,192 +15,65 @@ import 'package:adguard_home_manager/screens/settings/dns/private_reverse_server import 'package:adguard_home_manager/screens/settings/dns/upstream_dns.dart'; import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/functions/desktop_mode.dart'; +import 'package:adguard_home_manager/providers/dns_provider.dart'; import 'package:adguard_home_manager/functions/clear_dns_cache.dart'; import 'package:adguard_home_manager/functions/snackbar.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; -class DnsSettings extends StatelessWidget { - const DnsSettings({Key? key}) : super(key: key); +class DnsSettings extends StatefulWidget { + final bool splitView; + + const DnsSettings({ + super.key, + required this.splitView, + }); @override - Widget build(BuildContext context) { - final serversProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - - return DnsSettingsWidget( - serversProvider: serversProvider, - appConfigProvider: appConfigProvider, - ); - } -} -class DnsSettingsWidget extends StatefulWidget { - final ServersProvider serversProvider; - final AppConfigProvider appConfigProvider; - - const DnsSettingsWidget({ - required this.serversProvider, - required this.appConfigProvider, - Key? key - }) : super(key: key); - - @override - State createState() => _DnsSettingsWidgetState(); + State createState() => _DnsSettingsState(); } -class _DnsSettingsWidgetState extends State { - - void fetchData({bool? showRefreshIndicator}) async { - widget.serversProvider.setDnsInfoLoadStatus(0, showRefreshIndicator ?? false); - - final result = await getDnsInfo(server: widget.serversProvider.selectedServer!); - - if (mounted) { - if (result['result'] == 'success') { - widget.serversProvider.setDnsInfoData(result['data']); - widget.serversProvider.setDnsInfoLoadStatus(1, true); - } - else { - widget.appConfigProvider.addLog(result['log']); - widget.serversProvider.setDnsInfoLoadStatus(2, true); - } - } - } - +class _DnsSettingsState extends State { @override void initState() { - fetchData(); + Provider.of(context, listen: false).fetchDnsData(showLoading: true); super.initState(); } @override Widget build(BuildContext context) { final serversProvider = Provider.of(context); + final dnsProvider = Provider.of(context); final appConfigProvider = Provider.of(context); - Widget generateBody() { - switch (widget.serversProvider.dnsInfo.loadStatus) { - case 0: - return SizedBox( - width: double.maxFinite, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const CircularProgressIndicator(), - const SizedBox(height: 30), - Text( - AppLocalizations.of(context)!.loadingDnsConfig, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ) - ], - ) - ); + final width = MediaQuery.of(context).size.width; - case 1: - return ListView( - children: [ - CustomListTile( - title: AppLocalizations.of(context)!.upstreamDns, - subtitle: AppLocalizations.of(context)!.upstreamDnsDescription, - onTap: () => Navigator.push(context, MaterialPageRoute( - builder: (context) => UpstreamDnsScreen( - serversProvider: serversProvider - ) - )), - icon: Icons.upload_rounded, - ), - CustomListTile( - title: AppLocalizations.of(context)!.bootstrapDns, - subtitle: AppLocalizations.of(context)!.bootstrapDnsDescription, - onTap: () => Navigator.push(context, MaterialPageRoute( - builder: (context) => BootstrapDnsScreen( - serversProvider: serversProvider - ) - )), - icon: Icons.dns_rounded, - ), - CustomListTile( - title: AppLocalizations.of(context)!.privateReverseDnsServers, - subtitle: AppLocalizations.of(context)!.privateReverseDnsDescription, - onTap: () => Navigator.push(context, MaterialPageRoute( - builder: (context) => PrivateReverseDnsServersScreen( - serversProvider: serversProvider - ) - )), - icon: Icons.person_rounded, - ), - CustomListTile( - title: AppLocalizations.of(context)!.dnsServerSettings, - subtitle: AppLocalizations.of(context)!.dnsServerSettingsDescription, - onTap: () => Navigator.push(context, MaterialPageRoute( - builder: (context) => DnsServerSettingsScreen( - serversProvider: serversProvider - ) - )), - icon: Icons.settings, - ), - CustomListTile( - title: AppLocalizations.of(context)!.dnsCacheConfig, - subtitle: AppLocalizations.of(context)!.dnsCacheConfigDescription, - onTap: () => Navigator.push(context, MaterialPageRoute( - builder: (context) => CacheConfigDnsScreen( - serversProvider: serversProvider - ) - )), - icon: Icons.storage_rounded, - ), - ], - ); - - case 2: - return SizedBox( - width: double.maxFinite, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon( - Icons.error, - color: Colors.red, - size: 50, - ), - const SizedBox(height: 30), - Text( - AppLocalizations.of(context)!.dnsConfigNotLoaded, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ) - ], - ), - ); - - default: - return const SizedBox(); + void navigate(Widget w) { + if (widget.splitView) { + Navigator.of(settingsNavigatorKey.currentContext!).push(MaterialPageRoute(builder: (ctx) => w)); + } + else { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => w + ) + ); } } void clearCache() async { final result = await clearDnsCache(context, serversProvider.selectedServer!); - if (result == true) { - showSnacbkar( - context: context, + if (result.successful == true) { + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.dnsCacheCleared, color: Colors.green ); } else { - showSnacbkar( - context: context, + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.dnsCacheNotCleared, color: Colors.red @@ -208,11 +84,20 @@ class _DnsSettingsWidgetState extends State { return Scaffold( appBar: AppBar( title: Text(AppLocalizations.of(context)!.dnsSettings), + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, actions: [ + IconButton( + onPressed: () => showDialog( + context: context, + builder: (ctx) => const TestUpstreamDnsModal() + ), + icon: const Icon(Icons.upload_rounded), + tooltip: AppLocalizations.of(context)!.testUpstreamDnsServers, + ), PopupMenuButton( itemBuilder: (context) => [ PopupMenuItem( - onTap: () => fetchData(showRefreshIndicator: true), + onTap: () => dnsProvider.fetchDnsData(), child: Row( children: [ const Icon(Icons.refresh_rounded), @@ -243,7 +128,104 @@ class _DnsSettingsWidgetState extends State { const SizedBox(width: 10) ], ), - body: generateBody(), + body: SafeArea( + child: Builder( + builder: (context) { + switch (dnsProvider.loadStatus) { + case LoadStatus.loading: + return SizedBox( + width: double.maxFinite, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.loadingDnsConfig, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ) + ); + + case LoadStatus.loaded: + return ListView( + children: [ + CustomListTile( + title: AppLocalizations.of(context)!.upstreamDns, + subtitle: AppLocalizations.of(context)!.upstreamDnsDescription, + onTap: () => navigate(const UpstreamDnsScreen()), + icon: Icons.upload_rounded, + ), + CustomListTile( + title: AppLocalizations.of(context)!.bootstrapDns, + subtitle: AppLocalizations.of(context)!.bootstrapDnsDescription, + onTap: () => navigate(const BootstrapDnsScreen()), + icon: Icons.dns_rounded, + ), + if (dnsProvider.dnsInfo!.fallbackDns != null) CustomListTile( + title: AppLocalizations.of(context)!.fallbackDnsServers, + subtitle: AppLocalizations.of(context)!.fallbackDnsServersDescription, + onTap: () => navigate(const FallbackDnsScreen()), + icon: Icons.alt_route_rounded, + ), + CustomListTile( + title: AppLocalizations.of(context)!.privateReverseDnsServers, + subtitle: AppLocalizations.of(context)!.privateReverseDnsDescription, + onTap: () => navigate(const PrivateReverseDnsServersScreen()), + icon: Icons.person_rounded, + ), + CustomListTile( + title: AppLocalizations.of(context)!.dnsServerSettings, + subtitle: AppLocalizations.of(context)!.dnsServerSettingsDescription, + onTap: () => navigate(const DnsServerSettingsScreen()), + icon: Icons.settings, + ), + CustomListTile( + title: AppLocalizations.of(context)!.dnsCacheConfig, + subtitle: AppLocalizations.of(context)!.dnsCacheConfigDescription, + onTap: () => navigate(const CacheConfigDnsScreen()), + icon: Icons.storage_rounded, + ), + ], + ); + + case LoadStatus.error: + return SizedBox( + width: double.maxFinite, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon( + Icons.error, + color: Colors.red, + size: 50, + ), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.dnsConfigNotLoaded, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ); + + default: + return const SizedBox(); + } + }, + ), + ) ); } } \ No newline at end of file diff --git a/lib/screens/settings/dns/dns_server_settings.dart b/lib/screens/settings/dns/dns_server_settings.dart index f8165d0..fc74d88 100644 --- a/lib/screens/settings/dns/dns_server_settings.dart +++ b/lib/screens/settings/dns/dns_server_settings.dart @@ -1,57 +1,74 @@ // ignore_for_file: use_build_context_synchronously +import 'package:expandable/expandable.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/dns/rate_limit_allowlist_modal.dart'; +import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; import 'package:adguard_home_manager/widgets/custom_radio_list_tile.dart'; import 'package:adguard_home_manager/widgets/section_label.dart'; import 'package:adguard_home_manager/widgets/custom_switch_list_tile.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; +import 'package:adguard_home_manager/functions/desktop_mode.dart'; import 'package:adguard_home_manager/classes/process_modal.dart'; +import 'package:adguard_home_manager/providers/dns_provider.dart'; import 'package:adguard_home_manager/functions/snackbar.dart'; -import 'package:adguard_home_manager/models/dns_info.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; class DnsServerSettingsScreen extends StatefulWidget { - final ServersProvider serversProvider; - - const DnsServerSettingsScreen({ - Key? key, - required this.serversProvider - }) : super(key: key); + const DnsServerSettingsScreen({super.key}); @override State createState() => _DnsServerSettingsScreenState(); } class _DnsServerSettingsScreenState extends State { - final TextEditingController limitRequestsController = TextEditingController(); - String? limitRequestsError; - bool enableEdns = false; - bool enableDnssec = false; - bool disableIpv6Resolving = false; + final _limitRequestsController = TextEditingController(); + final _ipv4PrefixSubnetController = TextEditingController(); + String? _ipv4PrefixSubnetError; + final _ipv6PrefixSubnetController = TextEditingController(); + String? _ipv6PrefixSubnetError; + List _rateLimitAllowlist = []; + String? _limitRequestsError; + final _expandableCustomEdns = ExpandableController(); + final _expandableEdnsIp = ExpandableController(); + bool _enableEdns = false; + bool _useCustomIpEdns = false; + final _customIpEdnsController = TextEditingController(); + String? _ednsIpError; + bool _enableDnssec = false; + bool _disableIpv6Resolving = false; String blockingMode = "default"; - final TextEditingController ipv4controller = TextEditingController(); - String? ipv4error; - final TextEditingController ipv6controller = TextEditingController(); + final _ipv4controller = TextEditingController(); + String? _ipv4error; + final _ipv6controller = TextEditingController(); String? ipv6error; - bool isDataValid = false; + final _ttlController = TextEditingController(); + String? _ttlError; void validateIpv4(String value) { RegExp ipAddress = RegExp(r'^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$'); if (ipAddress.hasMatch(value) == true) { - setState(() => ipv4error = null); + setState(() => _ipv4error = null); } else { - setState(() => ipv4error = AppLocalizations.of(context)!.invalidIp); + setState(() => _ipv4error = AppLocalizations.of(context)!.invalidIp); + } + } + + void validateEdns(String value) { + RegExp ipAddress = RegExp(r'^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$'); + if (ipAddress.hasMatch(value) == true) { + setState(() => _ednsIpError = null); + } + else { + setState(() => _ednsIpError = AppLocalizations.of(context)!.ipNotValid); } - validateData(); } void validateIpv6(String value) { @@ -62,96 +79,125 @@ class _DnsServerSettingsScreenState extends State { else { setState(() => ipv6error = AppLocalizations.of(context)!.invalidIp); } - validateData(); } - void validateData() { + bool validateData() { if ( - limitRequestsController.text != '' && - limitRequestsError == null && + (_limitRequestsController.text == "" || (_limitRequestsController.text != "" && _limitRequestsError == null)) && ( blockingMode != 'custom_ip' || ( blockingMode == 'custom_ip' && - ipv4controller.text != '' && - ipv4error == null && - ipv6controller.text != '' && + _ipv4controller.text != '' && + _ipv4error == null && + _ipv6controller.text != '' && ipv6error == null ) - ) == true + ) == true && + _ednsIpError == null && + _ttlController.text != "" && _ttlError == null && + (_ipv4PrefixSubnetController.text == "" || (_ipv4PrefixSubnetController.text != "" && _ipv4PrefixSubnetError == null)) && + (_ipv6PrefixSubnetController.text == "" || (_ipv6PrefixSubnetController.text != "" && _ipv6PrefixSubnetError == null)) ) { - setState(() => isDataValid = true); + return true; } else { - setState(() => isDataValid = false); + return false; + } + } + + String? validateTtl(String value) { + if (value == "") return AppLocalizations.of(context)!.valueNotNumber; + if (int.tryParse(value) != null) { + return null; + } + else { + return AppLocalizations.of(context)!.valueNotNumber; + } + } + + String? validateNumber(String value) { + if (value == "") return null; + if (int.tryParse(value) != null) { + return null; + } + else { + return AppLocalizations.of(context)!.valueNotNumber; } } @override void initState() { - limitRequestsController.text = widget.serversProvider.dnsInfo.data!.ratelimit.toString(); - enableEdns = widget.serversProvider.dnsInfo.data!.ednsCsEnabled; - enableDnssec = widget.serversProvider.dnsInfo.data!.dnssecEnabled; - disableIpv6Resolving = widget.serversProvider.dnsInfo.data!.disableIpv6; - blockingMode = widget.serversProvider.dnsInfo.data!.blockingMode; - ipv4controller.text = widget.serversProvider.dnsInfo.data!.blockingIpv4; - ipv6controller.text = widget.serversProvider.dnsInfo.data!.blockingIpv6; - isDataValid = true; + final dnsProvider = Provider.of(context, listen: false); + + _limitRequestsController.text = dnsProvider.dnsInfo!.ratelimit.toString(); + _enableEdns = dnsProvider.dnsInfo!.ednsCsEnabled; + _useCustomIpEdns = dnsProvider.dnsInfo!.ednsCsUseCustom ?? false; + _customIpEdnsController.text = dnsProvider.dnsInfo!.ednsCsCustomIp ?? ""; + if (dnsProvider.dnsInfo!.ednsCsEnabled == true) _expandableCustomEdns.toggle(); + if (dnsProvider.dnsInfo!.ednsCsUseCustom == true) _expandableEdnsIp.toggle(); + _enableDnssec = dnsProvider.dnsInfo!.dnssecEnabled; + _disableIpv6Resolving = dnsProvider.dnsInfo!.disableIpv6; + blockingMode = dnsProvider.dnsInfo!.blockingMode; + _ipv4controller.text = dnsProvider.dnsInfo!.blockingIpv4; + _ipv6controller.text = dnsProvider.dnsInfo!.blockingIpv6; + _ttlController.text = dnsProvider.dnsInfo!.blockedResponseTtl != null + ? dnsProvider.dnsInfo!.blockedResponseTtl.toString() + : ""; + _ipv4PrefixSubnetController.text = dnsProvider.dnsInfo!.ratelimitSubnetLenIpv4 != null + ? dnsProvider.dnsInfo!.ratelimitSubnetLenIpv4.toString() + : ""; + _ipv6PrefixSubnetController.text = dnsProvider.dnsInfo!.ratelimitSubnetLenIpv6 != null + ? dnsProvider.dnsInfo!.ratelimitSubnetLenIpv6.toString() + : ""; + _rateLimitAllowlist = dnsProvider.dnsInfo!.ratelimitWhitelist ?? []; super.initState(); } @override Widget build(BuildContext context) { - final serversProvider = Provider.of(context); + final dnsProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; void saveData() async { - ProcessModal processModal = ProcessModal(context: context); + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.savingConfig); - final result = await setDnsConfig(server: serversProvider.selectedServer!, data: { - "ratelimit": int.parse(limitRequestsController.text), - "edns_cs_enabled": enableEdns, - "dnssec_enabled": enableDnssec, - "disable_ipv6": disableIpv6Resolving, - "blocking_mode": blockingMode + final result = await dnsProvider.saveDnsServerConfig({ + "ratelimit": int.tryParse(_limitRequestsController.text), + "edns_cs_enabled": _enableEdns, + "edns_cs_use_custom": _useCustomIpEdns, + "edns_cs_custom_ip": _customIpEdnsController.text, + "dnssec_enabled": _enableDnssec, + "disable_ipv6": _disableIpv6Resolving, + "blocking_mode": blockingMode, + "blocking_ipv4": _ipv4controller.text, + "blocking_ipv6": _ipv6controller.text, + "blocked_response_ttl": int.tryParse(_ttlController.text), + "ratelimit_subnet_len_ipv4": int.tryParse(_ipv4PrefixSubnetController.text), + "ratelimit_subnet_len_ipv6": int.tryParse(_ipv6PrefixSubnetController.text), + "ratelimit_whitelist": _rateLimitAllowlist }); processModal.close(); - if (result['result'] == 'success') { - DnsInfoData data = serversProvider.dnsInfo.data!; - data.ratelimit = int.parse(limitRequestsController.text); - data.ednsCsEnabled = enableEdns; - data.dnssecEnabled = enableDnssec; - data.disableIpv6 = disableIpv6Resolving; - data.blockingMode = blockingMode; - data.blockingIpv4 = ipv4controller.text; - data.blockingIpv6 = ipv6controller.text; - serversProvider.setDnsInfoData(data); - - showSnacbkar( - context: context, + if (result.successful == true) { + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.dnsConfigSaved, color: Colors.green ); } - else if (result['log'] != null && result['log'].statusCode == '400') { - appConfigProvider.addLog(result['log']); - - showSnacbkar( - context: context, + else if (result.successful == false && result.statusCode == 400) { + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.someValueNotValid, color: Colors.red ); } else { - appConfigProvider.addLog(result['log']); - - showSnacbkar( - context: context, + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.dnsConfigNotSaved, color: Colors.red @@ -161,21 +207,24 @@ class _DnsServerSettingsScreenState extends State { void updateBlockingMode(String mode) { if (mode != 'custom_ip') { - ipv4controller.text = ''; - ipv4error = null; - ipv6controller.text = ''; + _ipv4controller.text = ''; + _ipv4error = null; + _ipv6controller.text = ''; ipv6error = null; } setState(() => blockingMode = mode); validateData(); } + final dataValid = validateData(); + return Scaffold( appBar: AppBar( title: Text(AppLocalizations.of(context)!.dnsServerSettings), + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, actions: [ IconButton( - onPressed: isDataValid == true + onPressed: dataValid == true ? () => saveData() : null, icon: const Icon(Icons.save_rounded), @@ -184,141 +233,290 @@ class _DnsServerSettingsScreenState extends State { const SizedBox(width: 10) ], ), - body: ListView( - padding: const EdgeInsets.only(top: 10), - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: TextFormField( - controller: limitRequestsController, - onChanged: (value) { - if (int.tryParse(value) != null) { - setState(() => limitRequestsError = null); - } - else { - setState(() => limitRequestsError = AppLocalizations.of(context)!.valueNotNumber); + body: SafeArea( + child: ListView( + padding: const EdgeInsets.only(top: 0), + children: [ + SectionLabel( + label: AppLocalizations.of(context)!.rateLimit, + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 24, top: 8), + ), + Padding( + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 24, top: 4), + child: TextFormField( + controller: _limitRequestsController, + onChanged: (v) => setState(() => _limitRequestsError = validateNumber(v)), + decoration: InputDecoration( + prefixIcon: const Icon(Icons.looks_one_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + labelText: AppLocalizations.of(context)!.limitRequestsSecond, + errorText: _limitRequestsError + ), + keyboardType: TextInputType.number, + ), + ), + Padding( + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 24), + child: TextFormField( + controller: _ipv4PrefixSubnetController, + onChanged: (v) => setState(() => _ipv4PrefixSubnetError = validateNumber(v)), + decoration: InputDecoration( + prefixIcon: const Icon(Icons.skip_previous_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + labelText: AppLocalizations.of(context)!.subnetPrefixLengthIpv4, + errorText: _ipv4PrefixSubnetError + ), + keyboardType: TextInputType.number, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: TextFormField( + controller: _ipv6PrefixSubnetController, + onChanged: (v) => setState(() => _ipv6PrefixSubnetError = validateNumber(v)), + decoration: InputDecoration( + prefixIcon: const Icon(Icons.skip_previous_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + labelText: AppLocalizations.of(context)!.subnetPrefixLengthIpv6, + errorText: _ipv6PrefixSubnetError + ), + keyboardType: TextInputType.number, + ), + ), + const SizedBox(height: 16), + CustomListTile( + title: AppLocalizations.of(context)!.rateLimitAllowlist, + subtitle: AppLocalizations.of(context)!.rateLimitAllowlistDescription, + onTap: () => showDialog( + context: context, + builder: (context) => RateLimitAllowlistModal( + values: _rateLimitAllowlist, + onConfirm: (ips) => setState(() => _rateLimitAllowlist = ips) + ), + ), + trailing: _rateLimitAllowlist.isNotEmpty ? Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(40) + ), + child: Text( + _rateLimitAllowlist.length.toString(), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onPrimaryContainer + ), + ), + ) : null, + ), + SectionLabel(label: AppLocalizations.of(context)!.dnsOptions), + CustomSwitchListTile( + value: _enableEdns, + onChanged: (value) => setState(() { + _enableEdns = value; + _expandableCustomEdns.toggle(); + if (value == false) { + _useCustomIpEdns = false; + if (_expandableEdnsIp.expanded == true) _expandableEdnsIp.toggle(); + _customIpEdnsController.text = ""; + _ednsIpError = null; } validateData(); - }, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.looks_one_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - labelText: AppLocalizations.of(context)!.limitRequestsSecond, - errorText: limitRequestsError - ), - keyboardType: TextInputType.number, + }), + title: AppLocalizations.of(context)!.enableEdns, + subtitle: AppLocalizations.of(context)!.enableEdnsDescription, ), - ), - const SizedBox(height: 10), - CustomSwitchListTile( - value: enableEdns, - onChanged: (value) => setState(() => enableEdns = value), - title: AppLocalizations.of(context)!.enableEdns, - subtitle: AppLocalizations.of(context)!.enableEdnsDescription, - ), - CustomSwitchListTile( - value: enableDnssec, - onChanged: (value) => setState(() => enableDnssec = value), - title: AppLocalizations.of(context)!.enableDnssec, - subtitle: AppLocalizations.of(context)!.enableDnssecDescription, - ), - CustomSwitchListTile( - value: disableIpv6Resolving, - onChanged: (value) => setState(() => disableIpv6Resolving = value), - title: AppLocalizations.of(context)!.disableResolvingIpv6, - subtitle: AppLocalizations.of(context)!.disableResolvingIpv6Description, - ), - SectionLabel(label: AppLocalizations.of(context)!.blockingMode), - CustomRadioListTile( - groupValue: blockingMode, - value: "default", - radioBackgroundColor: Theme.of(context).dialogBackgroundColor, - title: AppLocalizations.of(context)!.defaultMode, - subtitle: AppLocalizations.of(context)!.defaultDescription, - onChanged: updateBlockingMode, - ), - CustomRadioListTile( - groupValue: blockingMode, - value: "refused", - radioBackgroundColor: Theme.of(context).dialogBackgroundColor, - title: "REFUSED", - subtitle: AppLocalizations.of(context)!.refusedDescription, - onChanged: updateBlockingMode, - ), - CustomRadioListTile( - groupValue: blockingMode, - value: "nxdomain", - radioBackgroundColor: Theme.of(context).dialogBackgroundColor, - title: "NXDOMAIN", - subtitle: AppLocalizations.of(context)!.nxdomainDescription, - onChanged: updateBlockingMode, - ), - CustomRadioListTile( - groupValue: blockingMode, - value: "null_ip", - radioBackgroundColor: Theme.of(context).dialogBackgroundColor, - title: AppLocalizations.of(context)!.nullIp, - subtitle: AppLocalizations.of(context)!.nullIpDescription, - onChanged: updateBlockingMode, - ), - CustomRadioListTile( - groupValue: blockingMode, - value: "custom_ip", - radioBackgroundColor: Theme.of(context).dialogBackgroundColor, - title: AppLocalizations.of(context)!.customIp, - subtitle: AppLocalizations.of(context)!.customIpDescription, - onChanged: updateBlockingMode, - ), - const SizedBox(height: 10), - if (blockingMode == 'custom_ip') ...[ + ExpandableNotifier( + controller: _expandableCustomEdns, + child: Expandable( + collapsed: const SizedBox(), + expanded: Column( + children: [ + CustomSwitchListTile( + padding: const EdgeInsets.only( + left: 50, + top: 12, + bottom: 12, + right: 16 + ), + value: _useCustomIpEdns, + onChanged: (value) => setState(() { + _useCustomIpEdns = value; + _expandableEdnsIp.toggle(); + if (_useCustomIpEdns == false) { + _customIpEdnsController.text = ""; + _ednsIpError = null; + } + validateData(); + }), + title: AppLocalizations.of(context)!.useCustomIpEdns, + subtitle: AppLocalizations.of(context)!.useCustomIpEdnsDescription, + ), + ExpandableNotifier( + controller: _expandableEdnsIp, + child: Expandable( + collapsed: const SizedBox(), + expanded: Padding( + padding: const EdgeInsets.only( + top: 16, + bottom: 16, + right: 16, + left: 70 + ), + child: TextFormField( + controller: _customIpEdnsController, + onChanged: validateEdns, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.link_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + errorText: _ednsIpError, + labelText: AppLocalizations.of(context)!.ipAddress, + ), + ), + ), + ) + ), + ], + ), + ) + ), + CustomSwitchListTile( + value: _enableDnssec, + onChanged: (value) => setState(() => _enableDnssec = value), + title: AppLocalizations.of(context)!.enableDnssec, + subtitle: AppLocalizations.of(context)!.enableDnssecDescription, + ), + CustomSwitchListTile( + value: _disableIpv6Resolving, + onChanged: (value) => setState(() => _disableIpv6Resolving = value), + title: AppLocalizations.of(context)!.disableResolvingIpv6, + subtitle: AppLocalizations.of(context)!.disableResolvingIpv6Description, + ), + SectionLabel(label: AppLocalizations.of(context)!.blockingMode), + CustomRadioListTile( + groupValue: blockingMode, + value: "default", + radioBackgroundColor: Theme.of(context).dialogBackgroundColor, + title: AppLocalizations.of(context)!.defaultMode, + subtitle: AppLocalizations.of(context)!.defaultDescription, + onChanged: updateBlockingMode, + ), + CustomRadioListTile( + groupValue: blockingMode, + value: "refused", + radioBackgroundColor: Theme.of(context).dialogBackgroundColor, + title: "REFUSED", + subtitle: AppLocalizations.of(context)!.refusedDescription, + onChanged: updateBlockingMode, + ), + CustomRadioListTile( + groupValue: blockingMode, + value: "nxdomain", + radioBackgroundColor: Theme.of(context).dialogBackgroundColor, + title: "NXDOMAIN", + subtitle: AppLocalizations.of(context)!.nxdomainDescription, + onChanged: updateBlockingMode, + ), + CustomRadioListTile( + groupValue: blockingMode, + value: "null_ip", + radioBackgroundColor: Theme.of(context).dialogBackgroundColor, + title: AppLocalizations.of(context)!.nullIp, + subtitle: AppLocalizations.of(context)!.nullIpDescription, + onChanged: updateBlockingMode, + ), + CustomRadioListTile( + groupValue: blockingMode, + value: "custom_ip", + radioBackgroundColor: Theme.of(context).dialogBackgroundColor, + title: AppLocalizations.of(context)!.customIp, + subtitle: AppLocalizations.of(context)!.customIpDescription, + onChanged: updateBlockingMode, + ), + const SizedBox(height: 10), + if (blockingMode == 'custom_ip') ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: TextFormField( + controller: _ipv4controller, + onChanged: validateIpv4, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.link_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + errorText: _ipv4error, + helperText: AppLocalizations.of(context)!.blockingIpv4Description, + helperMaxLines: 10, + labelText: AppLocalizations.of(context)!.blockingIpv4, + ), + keyboardType: TextInputType.number, + ), + ), + const SizedBox(height: 30), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: TextFormField( + controller: _ipv6controller, + onChanged: validateIpv6, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.link_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + errorText: ipv6error, + helperText: AppLocalizations.of(context)!.blockingIpv6Description, + helperMaxLines: 10, + labelText: AppLocalizations.of(context)!.blockingIpv6, + ), + keyboardType: TextInputType.number, + ), + ), + const SizedBox(height: 30), + ], Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), + padding: const EdgeInsets.all(16), child: TextFormField( - controller: ipv4controller, - onChanged: validateIpv4, + controller: _ttlController, + onChanged: (v) => setState(() => _ttlError = validateTtl(v)), decoration: InputDecoration( - prefixIcon: const Icon(Icons.link_rounded), + prefixIcon: const Icon(Icons.timer_rounded), border: const OutlineInputBorder( borderRadius: BorderRadius.all( Radius.circular(10) ) ), - errorText: ipv4error, - helperText: AppLocalizations.of(context)!.blockingIpv4Description, - helperMaxLines: 10, - labelText: AppLocalizations.of(context)!.blockingIpv4, + errorText: _ttlError, + labelText: AppLocalizations.of(context)!.blockedResponseTtl, + helperText: AppLocalizations.of(context)!.blockedResponseTtlDescription, + helperMaxLines: 2, ), keyboardType: TextInputType.number, ), ), - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: TextFormField( - controller: ipv6controller, - onChanged: validateIpv6, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.link_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - errorText: ipv6error, - helperText: AppLocalizations.of(context)!.blockingIpv6Description, - helperMaxLines: 10, - labelText: AppLocalizations.of(context)!.blockingIpv6, - ), - keyboardType: TextInputType.number, - ), - ), - const SizedBox(height: 30) - ] - ], + ], + ), ), ); } diff --git a/lib/screens/settings/dns/fallback_dns.dart b/lib/screens/settings/dns/fallback_dns.dart new file mode 100644 index 0000000..6c8d8ce --- /dev/null +++ b/lib/screens/settings/dns/fallback_dns.dart @@ -0,0 +1,334 @@ +import 'dart:io'; + +import 'package:adguard_home_manager/constants/regexps.dart'; +import 'package:adguard_home_manager/screens/settings/dns/comment_modal.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/functions/desktop_mode.dart'; +import 'package:adguard_home_manager/classes/process_modal.dart'; +import 'package:adguard_home_manager/providers/dns_provider.dart'; +import 'package:adguard_home_manager/functions/snackbar.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; + +class FallbackDnsScreen extends StatefulWidget { + const FallbackDnsScreen({super.key}); + + @override + State createState() => _FallbackDnsScreenState(); +} + +class _FallbackDnsScreenState extends State { + List> fallbackControllers = []; + + bool validValues = false; + + void validateIp(Map field, String value) { + if (Regexps.ipv4Address.hasMatch(value) == true || Regexps.ipv6Address.hasMatch(value) || Regexps.url.hasMatch(value) == true) { + setState(() => field['error'] = null); + } + else { + setState(() => field['error'] = AppLocalizations.of(context)!.invalidIpOrUrl); + } + checkValidValues(); + } + + void checkValidValues() { + if (fallbackControllers.every((element) => element['error'] == null)) { + setState(() => validValues = true); + } + else { + setState(() => validValues = false); + } + } + + @override + void initState() { + final dnsProvider = Provider.of(context, listen: false); + + for (var item in dnsProvider.dnsInfo!.fallbackDns!) { + if (item.contains("#")) { + fallbackControllers.add({ + 'comment': item + }); + } + else { + final controller = TextEditingController(); + controller.text = item; + fallbackControllers.add({ + 'controller': controller, + 'error': null, + 'isComment': item.contains("#") + }); + } + } + validValues = true; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final dnsProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + + final width = MediaQuery.of(context).size.width; + + void saveData() async { + ProcessModal processModal = ProcessModal(); + processModal.open(AppLocalizations.of(context)!.savingConfig); + + final result = await dnsProvider.saveFallbackDnsConfig({ + "fallback_dns": fallbackControllers.map( + (e) => e['controller'] != null + ? e['controller'].text + : e['comment'] + ).toList(), + }); + + processModal.close(); + + if (!context.mounted) return; + if (result.successful == true) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.dnsConfigSaved, + color: Colors.green + ); + } + else if (result.successful == false && result.statusCode == 400) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.someValueNotValid, + color: Colors.red + ); + } + else { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.dnsConfigNotSaved, + color: Colors.red + ); + } + } + + void openAddCommentModal() { + if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + context: context, + builder: (context) => CommentModal( + onConfirm: (value) { + setState(() { + fallbackControllers.add({ + 'comment': value + }); + }); + }, + dialog: true, + ), + ); + } + else { + showModalBottomSheet( + context: context, + useRootNavigator: true, + builder: (context) => CommentModal( + onConfirm: (value) { + setState(() { + fallbackControllers.add({ + 'comment': value + }); + }); + }, + dialog: false, + ), + backgroundColor: Colors.transparent, + isScrollControlled: true, + isDismissible: true + ); + } + } + + + void openEditCommentModal(Map item, int position) { + if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + context: context, + builder: (context) => CommentModal( + comment: item['comment'], + onConfirm: (value) { + setState(() => fallbackControllers[position] = { 'comment': value }); + }, + dialog: true, + ), + ); + } + else { + showModalBottomSheet( + context: context, + useRootNavigator: true, + builder: (context) => CommentModal( + comment: item['comment'], + onConfirm: (value) { + setState(() => fallbackControllers[position] = { 'comment': value }); + }, + dialog: false, + ), + backgroundColor: Colors.transparent, + isScrollControlled: true, + isDismissible: true + ); + } + } + + return Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context)!.fallbackDnsServers), + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, + actions: [ + IconButton( + onPressed: validValues == true + ? () => saveData() + : null, + icon: const Icon(Icons.save_rounded), + tooltip: AppLocalizations.of(context)!.save, + ), + const SizedBox(width: 10) + ], + ), + body: SafeArea( + child: ListView( + padding: const EdgeInsets.only(top: 10), + children: [ + Card( + margin: const EdgeInsets.only( + left: 16, right: 16, bottom: 20 + ), + child: Padding( + padding: const EdgeInsets.all(20), + child: Row( + children: [ + Icon( + Icons.info_rounded, + color: Theme.of(context).listTileTheme.iconColor, + ), + const SizedBox(width: 20), + Flexible( + child: Text( + AppLocalizations.of(context)!.fallbackDnsServersInfo, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface + ), + ) + ) + ], + ), + ), + ), + const SizedBox(height: 10), + if (fallbackControllers.isEmpty) Column( + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: Center( + child: Text( + AppLocalizations.of(context)!.noFallbackDnsAdded, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 16 + ), + ), + ), + ), + const SizedBox(height: 20), + ], + ), + ...fallbackControllers.map((c) => Padding( + padding: const EdgeInsets.only( + left: 16, right: 6, bottom: 20 + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (c['controller'] != null) Expanded( + child: TextFormField( + controller: c['controller'], + onChanged: (value) => validateIp(c, value), + decoration: InputDecoration( + prefixIcon: Icon( + c['isComment'] == true + ? Icons.comment_rounded + : Icons.dns_rounded + ), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + errorText: c['error'], + labelText: c['isComment'] == true + ? AppLocalizations.of(context)!.comment + : AppLocalizations.of(context)!.dnsServer, + ) + ), + ), + const SizedBox(width: 8), + if (c['comment'] != null) Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + c['comment'], + style: TextStyle( + fontSize: 16, + color: Theme.of(context).listTileTheme.iconColor + ), + ), + IconButton( + onPressed: () => openEditCommentModal(c, fallbackControllers.indexOf(c)), + icon: const Icon(Icons.edit), + tooltip: AppLocalizations.of(context)!.edit, + ) + ], + ), + ), + IconButton( + onPressed: () { + setState(() => fallbackControllers = fallbackControllers.where((con) => con != c).toList()); + checkValidValues(); + }, + icon: const Icon(Icons.remove_circle_outline), + tooltip: AppLocalizations.of(context)!.remove, + ) + ], + ), + )), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.min, + children: [ + ElevatedButton.icon( + onPressed: openAddCommentModal, + icon: const Icon(Icons.add), + label: Text(AppLocalizations.of(context)!.comment) + ), + ElevatedButton.icon( + onPressed: () { + setState(() => fallbackControllers.add({ + 'controller': TextEditingController(), + 'error': null + })); + checkValidValues(); + }, + icon: const Icon(Icons.add), + label: Text(AppLocalizations.of(context)!.address) + ), + ], + ), + const SizedBox(height: 20) + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/settings/dns/private_reverse_servers.dart b/lib/screens/settings/dns/private_reverse_servers.dart index a6c29bf..1ea4cc3 100644 --- a/lib/screens/settings/dns/private_reverse_servers.dart +++ b/lib/screens/settings/dns/private_reverse_servers.dart @@ -6,20 +6,14 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/widgets/custom_switch_list_tile.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; +import 'package:adguard_home_manager/functions/desktop_mode.dart'; +import 'package:adguard_home_manager/providers/dns_provider.dart'; import 'package:adguard_home_manager/classes/process_modal.dart'; import 'package:adguard_home_manager/functions/snackbar.dart'; -import 'package:adguard_home_manager/models/dns_info.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; class PrivateReverseDnsServersScreen extends StatefulWidget { - final ServersProvider serversProvider; - - const PrivateReverseDnsServersScreen({ - Key? key, - required this.serversProvider, - }) : super(key: key); + const PrivateReverseDnsServersScreen({super.key}); @override State createState() => _PrivateReverseDnsServersScreenState(); @@ -28,12 +22,7 @@ class PrivateReverseDnsServersScreen extends StatefulWidget { class _PrivateReverseDnsServersScreenState extends State { List defaultReverseResolvers = []; bool editReverseResolvers = false; - List> reverseResolversControllers = [ - { - 'controller': TextEditingController(), - 'error': null - } - ]; + List> reverseResolversControllers = []; bool usePrivateReverseDnsResolvers = false; bool enableReverseResolve = false; @@ -69,78 +58,75 @@ class _PrivateReverseDnsServersScreenState extends State(context, listen: false); + + for (var item in dnsProvider.dnsInfo!.defaultLocalPtrUpstreams) { defaultReverseResolvers.add(item); } - for (var item in widget.serversProvider.dnsInfo.data!.localPtrUpstreams) { + if (dnsProvider.dnsInfo!.localPtrUpstreams.isEmpty) { + reverseResolversControllers.add({ + 'controller': TextEditingController(), + 'error': null + }); + } + for (var item in dnsProvider.dnsInfo!.localPtrUpstreams) { final controller = TextEditingController(); controller.text = item; - reverseResolversControllers = [{ + reverseResolversControllers.add({ 'controller': controller, 'error': null - }]; + }); } - if (widget.serversProvider.dnsInfo.data!.localPtrUpstreams.isNotEmpty) { + if (dnsProvider.dnsInfo!.localPtrUpstreams.isNotEmpty) { editReverseResolvers = true; } - usePrivateReverseDnsResolvers = widget.serversProvider.dnsInfo.data!.usePrivatePtrResolvers; - enableReverseResolve = widget.serversProvider.dnsInfo.data!.resolveClients; + usePrivateReverseDnsResolvers = dnsProvider.dnsInfo!.usePrivatePtrResolvers ?? false; + enableReverseResolve = dnsProvider.dnsInfo!.resolveClients ?? false; validValues = true; super.initState(); } @override Widget build(BuildContext context) { - final serversProvider = Provider.of(context); + final dnsProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + void saveData() async { - ProcessModal processModal = ProcessModal(context: context); + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.savingConfig); - final result = await setDnsConfig(server: serversProvider.selectedServer!, data: editReverseResolvers == true - ? { - "local_ptr_upstreams": List.from(reverseResolversControllers.map((e) => e['controller'].text)), - "use_private_ptr_resolvers": usePrivateReverseDnsResolvers, - "resolve_clients": enableReverseResolve - } : { - "use_private_ptr_resolvers": usePrivateReverseDnsResolvers, - "resolve_clients": enableReverseResolve - }); + final result = await dnsProvider.savePrivateReverseServersConfig( + editReverseResolvers == true + ? { + "local_ptr_upstreams": List.from(reverseResolversControllers.map((e) => e['controller'].text)), + "use_private_ptr_resolvers": usePrivateReverseDnsResolvers, + "resolve_clients": enableReverseResolve + } : { + "use_private_ptr_resolvers": usePrivateReverseDnsResolvers, + "resolve_clients": enableReverseResolve + } + ); processModal.close(); - if (result['result'] == 'success') { - DnsInfoData data = serversProvider.dnsInfo.data!; - if (editReverseResolvers == true) { - data.localPtrUpstreams = List.from(reverseResolversControllers.map((e) => e['controller'].text)); - } - data.usePrivatePtrResolvers = usePrivateReverseDnsResolvers; - data.resolveClients = enableReverseResolve; - serversProvider.setDnsInfoData(data); - - showSnacbkar( - context: context, + if (result.successful == true) { + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.dnsConfigSaved, color: Colors.green ); } - else if (result['log'] != null && result['log'].statusCode == '400') { - appConfigProvider.addLog(result['log']); - - showSnacbkar( - context: context, + else if (result.successful == false && result.statusCode == 400) { + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.someValueNotValid, color: Colors.red ); } else { - appConfigProvider.addLog(result['log']); - - showSnacbkar( - context: context, + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.dnsConfigNotSaved, color: Colors.red @@ -151,6 +137,7 @@ class _PrivateReverseDnsServersScreenState extends State item).join(', ').toString().replaceAll(RegExp(r'\(|\)'), '')}", - textAlign: TextAlign.center, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontSize: 16 + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Icon( + Icons.info_rounded, + color: Theme.of(context).listTileTheme.iconColor, + ), + const SizedBox(width: 16), + Flexible( + child: Text( + AppLocalizations.of(context)!.privateReverseDnsServersDescription, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface + ), + ) + ) + ], ), ), ), - Padding( - padding: const EdgeInsets.only(top: 10, bottom: 20), - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ElevatedButton.icon( - onPressed: () { - setState(() => editReverseResolvers = true); - checkDataValid(); - }, - icon: const Icon(Icons.edit), - label: Text(AppLocalizations.of(context)!.edit) - ), - ], - ), - ) - ], - if (editReverseResolvers == true) ...[ - const SizedBox(height: 20), - ...reverseResolversControllers.map((c) => Padding( - padding: const EdgeInsets.only( - left: 16, right: 6, bottom: 20 - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SizedBox( - width: MediaQuery.of(context).size.width-74, - child: TextFormField( - controller: c['controller'], - onChanged: (value) => validateAddress(c, value), - decoration: InputDecoration( - prefixIcon: const Icon(Icons.dns_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - errorText: c['error'], - labelText: AppLocalizations.of(context)!.serverAddress, - ) - ), - ), - IconButton( - onPressed: () { - setState(() => reverseResolversControllers = reverseResolversControllers.where((con) => con != c).toList()); - checkDataValid(); - }, - icon: const Icon(Icons.remove_circle_outline) - ) - ], - ), - )), - if (reverseResolversControllers.isEmpty) Padding( - padding: const EdgeInsets.only( - left: 20, right: 20, bottom: 20 - ), - child: Center( + if (editReverseResolvers == false) ...[ + Padding( + padding: const EdgeInsets.all(20), child: Text( - AppLocalizations.of(context)!.noServerAddressesAdded, + "${AppLocalizations.of(context)!.reverseDnsDefault}:\n\n${defaultReverseResolvers.map((item) => item).join(', ').toString().replaceAll(RegExp(r'\(|\)'), '')}", textAlign: TextAlign.center, style: TextStyle( color: Theme.of(context).colorScheme.onSurfaceVariant, @@ -270,41 +190,110 @@ class _PrivateReverseDnsServersScreenState extends State reverseResolversControllers.add({ - 'controller': TextEditingController(), - 'error': null - })); - checkDataValid(); - }, - icon: const Icon(Icons.add), - label: Text(AppLocalizations.of(context)!.addItem) + Padding( + padding: const EdgeInsets.only(top: 10, bottom: 20), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton.icon( + onPressed: () { + setState(() => editReverseResolvers = true); + checkDataValid(); + }, + icon: const Icon(Icons.edit), + label: Text(AppLocalizations.of(context)!.edit) + ), + ], + ), + ) + ], + if (editReverseResolvers == true) ...[ + const SizedBox(height: 20), + ...reverseResolversControllers.map((c) => Padding( + padding: const EdgeInsets.only( + left: 16, right: 6, bottom: 20 + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: TextFormField( + controller: c['controller'], + onChanged: (value) => validateAddress(c, value), + decoration: InputDecoration( + prefixIcon: const Icon(Icons.dns_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + errorText: c['error'], + labelText: AppLocalizations.of(context)!.serverAddress, + ) + ), + ), + const SizedBox(width: 8), + IconButton( + onPressed: () { + setState(() => reverseResolversControllers = reverseResolversControllers.where((con) => con != c).toList()); + checkDataValid(); + }, + icon: const Icon(Icons.remove_circle_outline) + ) + ], + ), + )), + if (reverseResolversControllers.isEmpty) Padding( + padding: const EdgeInsets.only( + left: 20, right: 20, bottom: 20 + ), + child: Center( + child: Text( + AppLocalizations.of(context)!.noServerAddressesAdded, + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 16 + ), ), - ], + ), ), + Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + ElevatedButton.icon( + onPressed: () { + setState(() => reverseResolversControllers.add({ + 'controller': TextEditingController(), + 'error': null + })); + checkDataValid(); + }, + icon: const Icon(Icons.add), + label: Text(AppLocalizations.of(context)!.addItem) + ), + ], + ), + ), + ], + CustomSwitchListTile( + value: usePrivateReverseDnsResolvers, + onChanged: (value) => setState(() => usePrivateReverseDnsResolvers = value), + title: AppLocalizations.of(context)!.usePrivateReverseDnsResolvers, + subtitle: AppLocalizations.of(context)!.usePrivateReverseDnsResolversDescription + ), + CustomSwitchListTile( + value: enableReverseResolve, + onChanged: (value) => setState(() => enableReverseResolve = value), + title: AppLocalizations.of(context)!.enableReverseResolving, + subtitle: AppLocalizations.of(context)!.enableReverseResolvingDescription ), ], - CustomSwitchListTile( - value: usePrivateReverseDnsResolvers, - onChanged: (value) => setState(() => usePrivateReverseDnsResolvers = value), - title: AppLocalizations.of(context)!.usePrivateReverseDnsResolvers, - subtitle: AppLocalizations.of(context)!.usePrivateReverseDnsResolversDescription - ), - CustomSwitchListTile( - value: enableReverseResolve, - onChanged: (value) => setState(() => enableReverseResolve = value), - title: AppLocalizations.of(context)!.enableReverseResolving, - subtitle: AppLocalizations.of(context)!.enableReverseResolvingDescription - ), - ], + ), ), ); } diff --git a/lib/screens/settings/dns/rate_limit_allowlist_modal.dart b/lib/screens/settings/dns/rate_limit_allowlist_modal.dart new file mode 100644 index 0000000..2c2fed8 --- /dev/null +++ b/lib/screens/settings/dns/rate_limit_allowlist_modal.dart @@ -0,0 +1,186 @@ +import 'package:adguard_home_manager/constants/regexps.dart'; +import 'package:flutter/material.dart'; +import 'package:uuid/uuid.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class _IpListItemController { + final String id; + final TextEditingController controller; + bool error; + + _IpListItemController({ + required this.id, + required this.controller, + required this.error + }); +} + +class RateLimitAllowlistModal extends StatefulWidget { + final List values; + final void Function(List) onConfirm; + + const RateLimitAllowlistModal({ + super.key, + required this.values, + required this.onConfirm, + }); + + @override + State createState() => _RateLimitAllowlistModalState(); +} + +class _RateLimitAllowlistModalState extends State { + final Uuid uuid = const Uuid(); + List<_IpListItemController> _controllersList = []; + + void _validateIp(String value, _IpListItemController item) { + if (Regexps.ipv4Address.hasMatch(value)) { + setState(() => _controllersList = _controllersList.map((e) { + if (e.id == item.id) { + return _IpListItemController( + id: e.id, + controller: e.controller, + error: false + ); + } + return e; + }).toList()); + } + else { + setState(() => _controllersList = _controllersList.map((e) { + if (e.id == item.id) { + return _IpListItemController( + id: e.id, + controller: e.controller, + error: true + ); + } + return e; + }).toList()); + } + } + + @override + void initState() { + _controllersList = widget.values.map((e) => _IpListItemController( + id: uuid.v4(), + controller: TextEditingController(text: e), + error: false + )).toList(); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + final validData = _controllersList.where((e) => e.error == true).isEmpty; + + 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: [ + Center( + child: Icon( + Icons.check_circle_rounded, + size: 24, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 16), + Text( + AppLocalizations.of(context)!.rateLimitAllowlist, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 24, + ), + ), + const SizedBox(height: 16), + ..._controllersList.map((item) => Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + children: [ + Expanded( + child: TextFormField( + controller: item.controller, + onChanged: (v) => _validateIp(v, item), + decoration: InputDecoration( + prefixIcon: const Icon(Icons.link_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + labelText: AppLocalizations.of(context)!.ipAddress, + errorText: item.error == true + ? AppLocalizations.of(context)!.invalidIp + : null + ), + keyboardType: TextInputType.number, + ), + ), + const SizedBox(width: 16), + IconButton( + onPressed: () => setState( + () => _controllersList = _controllersList.where((c) => c.id != item.id).toList() + ), + icon: const Icon(Icons.remove_circle_outline_rounded), + tooltip: AppLocalizations.of(context)!.remove, + ) + ], + ), + )), + Center( + child: ElevatedButton.icon( + onPressed: () => setState(() => _controllersList.add( + _IpListItemController( + id: uuid.v4(), + controller: TextEditingController(), + error: false + ), + )), + icon: const Icon(Icons.add_rounded), + label: Text(AppLocalizations.of(context)!.addItem), + ), + ) + ], + ), + ) + ), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.cancel) + ), + const SizedBox(width: 8), + TextButton( + onPressed: validData == true + ? () { + widget.onConfirm( + _controllersList.map((e) => e.controller.text).toList() + ); + Navigator.pop(context); + } + : null, + child: Text(AppLocalizations.of(context)!.confirm) + ), + ], + ) + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/settings/dns/test_upstream_dns_modal.dart b/lib/screens/settings/dns/test_upstream_dns_modal.dart new file mode 100644 index 0000000..7bff710 --- /dev/null +++ b/lib/screens/settings/dns/test_upstream_dns_modal.dart @@ -0,0 +1,172 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/providers/dns_provider.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; + +class _Item { + final String url; + final bool value; + + const _Item({ + required this.url, + required this.value + }); +} + +class TestUpstreamDnsModal extends StatefulWidget { + const TestUpstreamDnsModal({super.key}); + + @override + State createState() => _TestUpstreamDnsModalState(); +} + +class _TestUpstreamDnsModalState extends State { + LoadStatus loadStatus = LoadStatus.loading; + List<_Item>? values; + + void checkDns() async { + final dnsProvider = Provider.of(context, listen: false); + if (dnsProvider.dnsInfo == null) { + setState(() => loadStatus = LoadStatus.error); + return; + } + final result = await Provider.of(context, listen: false).apiClient2!.testUpstreamDns( + body: { + "bootstrap_dns": dnsProvider.dnsInfo!.bootstrapDns, + "fallback_dns": [], + "private_upstream": dnsProvider.dnsInfo!.defaultLocalPtrUpstreams, + "upstream_dns": dnsProvider.dnsInfo!.upstreamDns + } + ); + if (!mounted) return; + if (result.successful == true) { + setState(() { + values = List<_Item>.from( + (result.content as Map).entries.map((e) => _Item( + url: e.key, + value: e.value == "OK" ? true : false + )) + ); + loadStatus = LoadStatus.loaded; + }); + } + else { + setState(() => loadStatus = LoadStatus.error); + } + } + + @override + void initState() { + checkDns(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Column( + children: [ + Icon( + Icons.upload_rounded, + size: 24, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + const SizedBox(height: 16), + Text( + AppLocalizations.of(context)!.testUpstreamDnsServers, + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface + ), + ) + ], + ), + content: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500 + ), + child: Builder( + builder: (context) { + switch (loadStatus) { + case LoadStatus.loading: + return const Padding( + padding: EdgeInsets.symmetric(vertical: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator() + ], + ), + ); + + case LoadStatus.loaded: + return SingleChildScrollView( + child: Wrap( + children: values!.map((v) => Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + v.url, + overflow: TextOverflow.ellipsis, + ), + ), + ...[ + const SizedBox(width: 8), + if (v.value == true) const Icon( + Icons.check_circle_rounded, + color: Colors.green, + size: 16, + ), + if (v.value == false) const Icon( + Icons.cancel_rounded, + color: Colors.red, + ) + ] + ], + ), + )).toList(), + ), + ); + + case LoadStatus.error: + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.error_rounded, + color: Theme.of(context).colorScheme.onSurfaceVariant, + size: 30, + ), + const SizedBox(height: 16), + Text( + AppLocalizations.of(context)!.errorTestUpstreamDns, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 18, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ) + ], + ); + + default: + return const SizedBox(); + } + }, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.close) + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/settings/dns/upstream_dns.dart b/lib/screens/settings/dns/upstream_dns.dart index 0005252..1ae8c5e 100644 --- a/lib/screens/settings/dns/upstream_dns.dart +++ b/lib/screens/settings/dns/upstream_dns.dart @@ -1,4 +1,4 @@ -// ignore_for_file: use_build_context_synchronously +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -8,20 +8,14 @@ import 'package:adguard_home_manager/widgets/section_label.dart'; import 'package:adguard_home_manager/screens/settings/dns/comment_modal.dart'; import 'package:adguard_home_manager/widgets/custom_radio_list_tile.dart'; -import 'package:adguard_home_manager/models/dns_info.dart'; import 'package:adguard_home_manager/classes/process_modal.dart'; +import 'package:adguard_home_manager/functions/desktop_mode.dart'; +import 'package:adguard_home_manager/providers/dns_provider.dart'; import 'package:adguard_home_manager/functions/snackbar.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; class UpstreamDnsScreen extends StatefulWidget { - final ServersProvider serversProvider; - - const UpstreamDnsScreen({ - Key? key, - required this.serversProvider, - }) : super(key: key); + const UpstreamDnsScreen({super.key}); @override State createState() => _UpstreamDnsScreenState(); @@ -34,6 +28,9 @@ class _UpstreamDnsScreenState extends State { bool validValues = false; + final upstreamTimeoutController = TextEditingController(); + String? upstreamTimeoutError = null; + checkValidValues() { if ( dnsServers.isNotEmpty && @@ -48,8 +45,10 @@ class _UpstreamDnsScreenState extends State { @override void initState() { - for (var item in widget.serversProvider.dnsInfo.data!.upstreamDns) { - if (item[0] == '#') { + final dnsProvider = Provider.of(context, listen: false); + + for (var item in dnsProvider.dnsInfo!.upstreamDns) { + if (item.contains("#")) { dnsServers.add({ 'comment': item }); @@ -62,86 +61,133 @@ class _UpstreamDnsScreenState extends State { }); } } - upstreamMode = widget.serversProvider.dnsInfo.data!.upstreamMode; + upstreamMode = dnsProvider.dnsInfo!.upstreamMode ?? ""; + upstreamTimeoutController.text = dnsProvider.dnsInfo!.upstreamTimeout != null ? dnsProvider.dnsInfo!.upstreamTimeout.toString() : ""; validValues = true; super.initState(); } @override Widget build(BuildContext context) { - final serversProvider = Provider.of(context); + final dnsProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + + final width = MediaQuery.of(context).size.width; + + void validateTimeout(String value) { + if (value != '' && int.tryParse(value) != null && int.parse(value) > 0) { + setState(() { + upstreamTimeoutError = null; + validValues = true; + }); + } + else { + setState(() { + upstreamTimeoutError = value == '' + ? AppLocalizations.of(context)!.fieldCannotBeEmpty + : AppLocalizations.of(context)!.invalidValue; + validValues = false; + }); + } + } void openAddCommentModal() { - showModalBottomSheet( - context: context, - builder: (context) => CommentModal( - onConfirm: (value) { - dnsServers.add({ - 'comment': value - }); - }, - ), - backgroundColor: Colors.transparent, - isScrollControlled: true, - isDismissible: true - ); + if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + context: context, + builder: (context) => CommentModal( + onConfirm: (value) { + setState(() { + dnsServers.add({ + 'comment': value + }); + }); + }, + dialog: true, + ), + ); + } + else { + showModalBottomSheet( + context: context, + useRootNavigator: true, + builder: (context) => CommentModal( + onConfirm: (value) { + setState(() { + dnsServers.add({ + 'comment': value + }); + }); + }, + dialog: false, + ), + backgroundColor: Colors.transparent, + isScrollControlled: true, + isDismissible: true + ); + } } void openEditCommentModal(Map item, int position) { - showModalBottomSheet( - context: context, - builder: (context) => CommentModal( - comment: item['comment'], - onConfirm: (value) { - setState(() => dnsServers[position] = { 'comment': value }); - }, - ), - backgroundColor: Colors.transparent, - isScrollControlled: true, - isDismissible: true - ); + if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + context: context, + builder: (context) => CommentModal( + comment: item['comment'], + onConfirm: (value) { + setState(() => dnsServers[position] = { 'comment': value }); + }, + dialog: true, + ), + ); + } + else { + showModalBottomSheet( + context: context, + useRootNavigator: true, + builder: (context) => CommentModal( + comment: item['comment'], + onConfirm: (value) { + setState(() => dnsServers[position] = { 'comment': value }); + }, + dialog: false, + ), + backgroundColor: Colors.transparent, + isScrollControlled: true, + isDismissible: true + ); + } } void saveData() async { - ProcessModal processModal = ProcessModal(context: context); + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.savingConfig); - final result = await setDnsConfig(server: serversProvider.selectedServer!, data: { + final result = await dnsProvider.saveUpstreamDnsConfig({ "upstream_dns": dnsServers.map((e) => e['controller'] != null ? e['controller'].text : e['comment']).toList(), - "upstream_mode": upstreamMode + "upstream_mode": upstreamMode, + "upstream_timeout": int.tryParse(upstreamTimeoutController.text) }); processModal.close(); - if (result['result'] == 'success') { - DnsInfoData data = serversProvider.dnsInfo.data!; - data.upstreamDns = List.from(dnsServers.map((e) => e['controller'] != null ? e['controller'].text : e['comment'])); - data.upstreamMode = upstreamMode; - serversProvider.setDnsInfoData(data); - - showSnacbkar( - context: context, + if (!context.mounted) return; + if (result.successful == true) { + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.dnsConfigSaved, color: Colors.green ); } - else if (result['log'] != null && result['log'].statusCode == '400') { - appConfigProvider.addLog(result['log']); - - showSnacbkar( - context: context, + else if (result.successful == false && result.statusCode == 400) { + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.someValueNotValid, color: Colors.red ); } else { - appConfigProvider.addLog(result['log']); - - showSnacbkar( - context: context, + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.dnsConfigNotSaved, color: Colors.red @@ -152,6 +198,7 @@ class _UpstreamDnsScreenState extends State { return Scaffold( appBar: AppBar( title: Text(AppLocalizations.of(context)!.upstreamDns), + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, actions: [ IconButton( onPressed: validValues == true @@ -160,130 +207,155 @@ class _UpstreamDnsScreenState extends State { icon: const Icon(Icons.save_rounded), tooltip: AppLocalizations.of(context)!.save, ), - const SizedBox(width: 10) + const SizedBox(width: 8) ], ), - body: ListView( - padding: const EdgeInsets.only(top: 10), - children: [ - if (dnsServers.isEmpty) Column( - children: [ - Padding( - padding: const EdgeInsets.all(10), - child: Center( - child: Text( - AppLocalizations.of(context)!.noUpstreamDns, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontSize: 16 + body: SafeArea( + child: ListView( + padding: const EdgeInsets.only(top: 10), + children: [ + if (dnsServers.isEmpty) Column( + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: Center( + child: Text( + AppLocalizations.of(context)!.noUpstreamDns, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 16 + ), ), ), ), - ), - const SizedBox(height: 20), - ], - ), - ...dnsServers.map((item) => Padding( - padding: const EdgeInsets.only( - left: 16, right: 6, bottom: 20 - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - if (item['controller'] != null) SizedBox( - width: MediaQuery.of(context).size.width-74, - child: TextFormField( - controller: item['controller'], - onChanged: (_) => checkValidValues(), - decoration: InputDecoration( - prefixIcon: const Icon(Icons.dns_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - labelText: AppLocalizations.of(context)!.dnsServer, - ) - ), - ), - if (item['comment'] != null) Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - item['comment'], - style: TextStyle( - fontSize: 16, - color: Theme.of(context).listTileTheme.iconColor - ), - ), - IconButton( - onPressed: () => openEditCommentModal(item, dnsServers.indexOf(item)), - icon: const Icon(Icons.edit), - tooltip: AppLocalizations.of(context)!.edit, - ) - ], - ), - ), - IconButton( - onPressed: () { - setState(() => dnsServers = dnsServers.where((i) => i != item).toList()); - checkValidValues(); - }, - icon: const Icon(Icons.remove_circle_outline), - tooltip: AppLocalizations.of(context)!.remove, - ) + const SizedBox(height: 20), ], ), - )).toList(), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - mainAxisSize: MainAxisSize.min, - children: [ - ElevatedButton.icon( - onPressed: openAddCommentModal, - icon: const Icon(Icons.add), - label: Text(AppLocalizations.of(context)!.comment) + ...dnsServers.map((item) => Padding( + padding: const EdgeInsets.only( + left: 16, right: 6, bottom: 24 ), - ElevatedButton.icon( - onPressed: () { - setState(() => dnsServers.add({ - 'controller': TextEditingController() - })); - checkValidValues(); - }, - icon: const Icon(Icons.add), - label: Text(AppLocalizations.of(context)!.address) + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (item['controller'] != null) Expanded( + child: TextFormField( + controller: item['controller'], + onChanged: (_) => checkValidValues(), + decoration: InputDecoration( + prefixIcon: const Icon(Icons.dns_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + labelText: AppLocalizations.of(context)!.dnsServer, + ) + ), + ), + const SizedBox(width: 8), + if (item['comment'] != null) Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + item['comment'], + style: TextStyle( + fontSize: 16, + color: Theme.of(context).listTileTheme.iconColor + ), + ), + IconButton( + onPressed: () => openEditCommentModal(item, dnsServers.indexOf(item)), + icon: const Icon(Icons.edit), + tooltip: AppLocalizations.of(context)!.edit, + ) + ], + ), + ), + IconButton( + onPressed: () { + setState(() => dnsServers = dnsServers.where((i) => i != item).toList()); + checkValidValues(); + }, + icon: const Icon(Icons.remove_circle_outline), + tooltip: AppLocalizations.of(context)!.remove, + ), + const SizedBox(width: 4), + ], ), - ], - ), - const SizedBox(height: 16), - SectionLabel(label: AppLocalizations.of(context)!.dnsMode), - CustomRadioListTile( - groupValue: upstreamMode, - value: "", - radioBackgroundColor: Theme.of(context).dialogBackgroundColor, - title: AppLocalizations.of(context)!.loadBalancing, - subtitle: AppLocalizations.of(context)!.loadBalancingDescription, - onChanged: (value) => setState(() => upstreamMode = value), - ), - CustomRadioListTile( - groupValue: upstreamMode, - value: "parallel", - radioBackgroundColor: Theme.of(context).dialogBackgroundColor, - title: AppLocalizations.of(context)!.parallelRequests, - subtitle: AppLocalizations.of(context)!.parallelRequestsDescription, - onChanged: (value) => setState(() => upstreamMode = value), - ), - CustomRadioListTile( - groupValue: upstreamMode, - value: "fastest_addr", - radioBackgroundColor: Theme.of(context).dialogBackgroundColor, - title: AppLocalizations.of(context)!.fastestIpAddress, - subtitle: AppLocalizations.of(context)!.fastestIpAddressDescription, - onChanged: (value) => setState(() => upstreamMode = value), - ), - ], + )), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.min, + children: [ + ElevatedButton.icon( + onPressed: openAddCommentModal, + icon: const Icon(Icons.add), + label: Text(AppLocalizations.of(context)!.comment) + ), + ElevatedButton.icon( + onPressed: () { + setState(() => dnsServers.add({ + 'controller': TextEditingController() + })); + checkValidValues(); + }, + icon: const Icon(Icons.add), + label: Text(AppLocalizations.of(context)!.address) + ), + ], + ), + const SizedBox(height: 16), + SectionLabel(label: AppLocalizations.of(context)!.dnsMode), + CustomRadioListTile( + groupValue: upstreamMode, + value: "", + radioBackgroundColor: Theme.of(context).dialogBackgroundColor, + title: AppLocalizations.of(context)!.loadBalancing, + subtitle: AppLocalizations.of(context)!.loadBalancingDescription, + onChanged: (value) => setState(() => upstreamMode = value), + ), + CustomRadioListTile( + groupValue: upstreamMode, + value: "parallel", + radioBackgroundColor: Theme.of(context).dialogBackgroundColor, + title: AppLocalizations.of(context)!.parallelRequests, + subtitle: AppLocalizations.of(context)!.parallelRequestsDescription, + onChanged: (value) => setState(() => upstreamMode = value), + ), + CustomRadioListTile( + groupValue: upstreamMode, + value: "fastest_addr", + radioBackgroundColor: Theme.of(context).dialogBackgroundColor, + title: AppLocalizations.of(context)!.fastestIpAddress, + subtitle: AppLocalizations.of(context)!.fastestIpAddressDescription, + onChanged: (value) => setState(() => upstreamMode = value), + ), + const SizedBox(height: 16), + SectionLabel(label: AppLocalizations.of(context)!.others), + Padding( + padding: const EdgeInsets.all(16), + child: TextFormField( + controller: upstreamTimeoutController, + onChanged: validateTimeout, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.timer_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + labelText: AppLocalizations.of(context)!.upstreamTimeout, + helperText: AppLocalizations.of(context)!.upstreamTimeoutHelper, + helperMaxLines: 2, + errorText: upstreamTimeoutError + ) + ), + ), + ], + ), ), ); } diff --git a/lib/screens/settings/dns_rewrites/add_dns_rewrite_modal.dart b/lib/screens/settings/dns_rewrites/add_dns_rewrite_modal.dart deleted file mode 100644 index 3009553..0000000 --- a/lib/screens/settings/dns_rewrites/add_dns_rewrite_modal.dart +++ /dev/null @@ -1,164 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -import 'package:adguard_home_manager/models/rewrite_rules.dart'; - -class AddDnsRewriteModal extends StatefulWidget { - final void Function(RewriteRulesData) onConfirm; - - const AddDnsRewriteModal({ - Key? key, - required this.onConfirm - }) : super(key: key); - - @override - State createState() => _AddDnsRewriteModalState(); -} - -class _AddDnsRewriteModalState extends State { - final TextEditingController domainController = TextEditingController(); - String? domainError; - final TextEditingController answerController = TextEditingController(); - - bool validData = false; - - void validateDomain(String value) { - final domainRegex = RegExp(r'^([a-z0-9|-]+\.)*[a-z0-9|-]+\.[a-z]+$'); - if (domainRegex.hasMatch(value)) { - setState(() => domainError = null); - } - else { - setState(() => domainError = AppLocalizations.of(context)!.domainNotValid); - } - checkValidValues(); - } - - void checkValidValues() { - if ( - domainController.text != '' && - domainError == null && - answerController.text != '' - ) { - setState(() => validData = true); - } - else { - setState(() => validData = false); - } - } - - @override - Widget build(BuildContext context) { - return Padding( - padding: MediaQuery.of(context).viewInsets, - child: Container( - height: 400, - decoration: BoxDecoration( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28) - ), - color: Theme.of(context).dialogBackgroundColor, - ), - child: Column( - children: [ - Expanded( - child: ListView( - physics: 410 < MediaQuery.of(context).size.height - ? const NeverScrollableScrollPhysics() - : null, - children: [ - Padding( - padding: const EdgeInsets.only(top: 24), - child: Icon( - Icons.add, - size: 24, - color: Theme.of(context).listTileTheme.iconColor - ), - ), - const SizedBox(height: 16), - Text( - AppLocalizations.of(context)!.addDnsRewrite, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSurface - ), - ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: TextFormField( - controller: domainController, - onChanged: validateDomain, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.link_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - errorText: domainError, - labelText: AppLocalizations.of(context)!.domain, - ), - ), - ), - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: TextFormField( - controller: answerController, - onChanged: (_) => checkValidValues(), - decoration: InputDecoration( - prefixIcon: const Icon(Icons.system_update_alt_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - labelText: AppLocalizations.of(context)!.answer, - ), - ), - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.all(24), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(AppLocalizations.of(context)!.cancel), - ), - const SizedBox(width: 20), - TextButton( - onPressed: validData == true - ? () { - Navigator.pop(context); - widget.onConfirm( - RewriteRulesData( - domain: domainController.text, - answer: answerController.text - ) - ); - } - : null, - child: Text( - AppLocalizations.of(context)!.confirm, - style: TextStyle( - color: validData == true - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurface.withOpacity(0.38) - ), - ), - ), - ], - ), - ) - ], - ), - ), - ); - } -} \ No newline at end of file diff --git a/lib/screens/settings/dns_rewrites/delete_dns_rewrite.dart b/lib/screens/settings/dns_rewrites/delete_dns_rewrite.dart index fb2c705..be6b7b4 100644 --- a/lib/screens/settings/dns_rewrites/delete_dns_rewrite.dart +++ b/lib/screens/settings/dns_rewrites/delete_dns_rewrite.dart @@ -5,9 +5,9 @@ class DeleteDnsRewrite extends StatelessWidget { final void Function() onConfirm; const DeleteDnsRewrite({ - Key? key, + super.key, required this.onConfirm - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/screens/settings/dns_rewrites/dns_rewrite_modal.dart b/lib/screens/settings/dns_rewrites/dns_rewrite_modal.dart new file mode 100644 index 0000000..42524fa --- /dev/null +++ b/lib/screens/settings/dns_rewrites/dns_rewrite_modal.dart @@ -0,0 +1,255 @@ +import 'dart:io'; + +import 'package:adguard_home_manager/constants/regexps.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/models/rewrite_rules.dart'; + +class DnsRewriteModal extends StatelessWidget { + final void Function(RewriteRules newRule, RewriteRules? previousRule) onConfirm; + final bool dialog; + final RewriteRules? rule; + final void Function(RewriteRules) onDelete; + + const DnsRewriteModal({ + super.key, + required this.onConfirm, + required this.dialog, + this.rule, + required this.onDelete + }); + + @override + Widget build(BuildContext context) { + if (dialog == true) { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 400 + ), + child: _Content( + onConfirm: onConfirm, + onDelete: onDelete, + rule: rule, + ) + ), + ); + } + else { + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28) + ), + color: Theme.of(context).dialogBackgroundColor, + ), + child: SafeArea( + child: _Content( + onConfirm: onConfirm, + onDelete: onDelete, + rule: rule, + ), + ) + ), + ); + } + } +} + +class _Content extends StatefulWidget { + final void Function(RewriteRules newRule, RewriteRules? previousRule) onConfirm; + final RewriteRules? rule; + final void Function(RewriteRules) onDelete; + + const _Content({ + required this.onConfirm, + this.rule, + required this.onDelete + }); + + @override + State<_Content> createState() => _ContentState(); +} + +class _ContentState extends State<_Content> { + final TextEditingController domainController = TextEditingController(); + String? domainError; + final TextEditingController answerController = TextEditingController(); + + bool validData = false; + + void validateDomain(String value) { + if (Regexps.wildcardDomain.hasMatch(value)) { + setState(() => domainError = null); + } + else { + setState(() => domainError = AppLocalizations.of(context)!.domainNotValid); + } + checkValidValues(); + } + + void checkValidValues() { + if ( + domainController.text != '' && + domainError == null && + answerController.text != '' + ) { + setState(() => validData = true); + } + else { + setState(() => validData = false); + } + } + + @override + void initState() { + if (widget.rule != null) { + domainController.text = widget.rule!.domain; + answerController.text = widget.rule!.answer; + validData = true; + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + 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( + widget.rule != null + ? Icons.edit + : Icons.add, + size: 24, + color: Theme.of(context).listTileTheme.iconColor + ), + ), + const SizedBox(height: 16), + Text( + widget.rule != null + ? AppLocalizations.of(context)!.editRewriteRule + : AppLocalizations.of(context)!.addDnsRewrite, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + color: Theme.of(context).colorScheme.onSurface + ), + ), + const SizedBox(height: 24), + ], + ), + ], + ), + Padding( + padding: const EdgeInsets.only( + left: 24, right: 24, bottom: 12 + ), + child: TextFormField( + controller: domainController, + onChanged: validateDomain, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.link_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + errorText: domainError, + labelText: AppLocalizations.of(context)!.domain, + ), + ), + ), + Padding( + padding: const EdgeInsets.only( + left: 24, right: 24, top: 12 + ), + child: TextFormField( + controller: answerController, + onChanged: (_) => checkValidValues(), + decoration: InputDecoration( + prefixIcon: const Icon(Icons.system_update_alt_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + labelText: AppLocalizations.of(context)!.answer, + ), + ), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(24), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (widget.rule != null) TextButton( + onPressed: () { + Navigator.pop(context); + widget.onDelete( + RewriteRules( + domain: domainController.text, + answer: answerController.text + ) + ); + }, + child: Text(AppLocalizations.of(context)!.delete), + ), + if (widget.rule == null) const SizedBox(), + Row( + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.cancel), + ), + const SizedBox(width: 20), + TextButton( + onPressed: validData == true + ? () { + Navigator.pop(context); + widget.onConfirm( + RewriteRules( + domain: domainController.text, + answer: answerController.text + ), + widget.rule + ); + } + : null, + child: Text( + AppLocalizations.of(context)!.confirm, + style: TextStyle( + color: validData == true + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onSurface.withOpacity(0.38) + ), + ), + ), + ], + ) + ], + ), + ), + if (Platform.isIOS) const SizedBox(height: 16) + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/settings/dns_rewrites/dns_rewrites.dart b/lib/screens/settings/dns_rewrites/dns_rewrites.dart index 05407bf..ad38c44 100644 --- a/lib/screens/settings/dns_rewrites/dns_rewrites.dart +++ b/lib/screens/settings/dns_rewrites/dns_rewrites.dart @@ -1,102 +1,80 @@ // ignore_for_file: use_build_context_synchronously +import 'dart:io'; + import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:adguard_home_manager/screens/settings/dns_rewrites/add_dns_rewrite_modal.dart'; import 'package:adguard_home_manager/screens/settings/dns_rewrites/delete_dns_rewrite.dart'; +import 'package:adguard_home_manager/screens/settings/dns_rewrites/dns_rewrite_modal.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; +import 'package:adguard_home_manager/functions/desktop_mode.dart'; import 'package:adguard_home_manager/functions/snackbar.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/providers/rewrite_rules_provider.dart'; import 'package:adguard_home_manager/models/rewrite_rules.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; import 'package:adguard_home_manager/classes/process_modal.dart'; -class DnsRewrites extends StatelessWidget { - const DnsRewrites({Key? key}) : super(key: key); +class DnsRewritesScreen extends StatefulWidget { + const DnsRewritesScreen({super.key}); @override - Widget build(BuildContext context) { - final serversProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - - return DnsRewritesWidget( - serversProvider: serversProvider, - appConfigProvider: appConfigProvider, - ); - } + State createState() => _DnsRewritesScreenState(); } -class DnsRewritesWidget extends StatefulWidget { - final ServersProvider serversProvider; - final AppConfigProvider appConfigProvider; - - const DnsRewritesWidget({ - Key? key, - required this.serversProvider, - required this.appConfigProvider - }) : super(key: key); - - @override - State createState() => _DnsRewritesWidgetState(); -} - -class _DnsRewritesWidgetState extends State { - Future fetchData() async { - widget.serversProvider.setRewriteRulesLoadStatus(0, false); - - final result = await getDnsRewriteRules(server: widget.serversProvider.selectedServer!); - - if (result['result'] == 'success') { - widget.serversProvider.setRewriteRulesData(result['data']); - widget.serversProvider.setRewriteRulesLoadStatus(1, true); - } - else { - widget.appConfigProvider.addLog(result['log']); - widget.serversProvider.setRewriteRulesLoadStatus(2, true); - } - } +class _DnsRewritesScreenState extends State { + late bool isVisible; + final ScrollController scrollController = ScrollController(); @override void initState() { - fetchData(); + Provider.of(context, listen: false).fetchRules(); super.initState(); + + isVisible = true; + scrollController.addListener(() { + if (scrollController.position.userScrollDirection == ScrollDirection.reverse) { + if (mounted && isVisible == true) { + setState(() => isVisible = false); + } + } + else { + if (scrollController.position.userScrollDirection == ScrollDirection.forward) { + if (mounted && isVisible == false) { + setState(() => isVisible = true); + } + } + } + }); } @override Widget build(BuildContext context) { - final serversProvider = Provider.of(context); + final rewriteRulesProvider = Provider.of(context); final appConfigProvider = Provider.of(context); - void deleteDnsRewrite(RewriteRulesData rule) async { - ProcessModal processModal = ProcessModal(context: context); + final width = MediaQuery.of(context).size.width; + + void deleteDnsRewrite(RewriteRules rule) async { + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.deleting); - final result = await deleteDnsRewriteRule(server: serversProvider.selectedServer!, data: { - "domain": rule.domain, - "answer": rule.answer - }); + final result = await rewriteRulesProvider.deleteDnsRewrite(rule); processModal.close(); - if (result['result'] == 'success') { - List data = serversProvider.rewriteRules.data!; - data = data.where((item) => item.domain != rule.domain).toList(); - serversProvider.setRewriteRulesData(data); - - showSnacbkar( - context: context, + if (result == true) { + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.dnsRewriteRuleDeleted, color: Colors.green ); } else { - appConfigProvider.addLog(result['log']); - showSnacbkar( - context: context, + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.dnsRewriteRuleNotDeleted, color: Colors.red @@ -104,33 +82,23 @@ class _DnsRewritesWidgetState extends State { } } - void addDnsRewrite(RewriteRulesData rule) async { - ProcessModal processModal = ProcessModal(context: context); + void addDnsRewrite(RewriteRules rule, _) async { + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.addingRewrite); - final result = await addDnsRewriteRule(server: serversProvider.selectedServer!, data: { - "domain": rule.domain, - "answer": rule.answer - }); + final result = await rewriteRulesProvider.addDnsRewrite(rule); processModal.close(); - if (result['result'] == 'success') { - List data = serversProvider.rewriteRules.data!; - data.add(rule); - serversProvider.setRewriteRulesData(data); - - showSnacbkar( - context: context, + if (result == true) { + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.dnsRewriteRuleAdded, color: Colors.green ); } else { - appConfigProvider.addLog(result['log']); - showSnacbkar( - context: context, + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.dnsRewriteRuleNotAdded, color: Colors.red @@ -138,165 +106,271 @@ class _DnsRewritesWidgetState extends State { } } - Widget generateBody() { - switch (serversProvider.rewriteRules.loadStatus) { - case 0: - return SizedBox( - width: double.maxFinite, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const CircularProgressIndicator(), - const SizedBox(height: 30), - Text( - AppLocalizations.of(context)!.loadingRewriteRules, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ) - ], - ), - ); - - case 1: - if (serversProvider.rewriteRules.data!.isNotEmpty) { - return RefreshIndicator( - onRefresh: () async { - await fetchData(); - }, - child: ListView.builder( - padding: const EdgeInsets.only(top: 0), - itemCount: serversProvider.rewriteRules.data!.length, - itemBuilder: (context, index) => Container( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - width: 1, - color: Theme.of(context).colorScheme.outline.withOpacity(0.2) - ) - ) - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Text( - "${AppLocalizations.of(context)!.domain}: ", - style: TextStyle( - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.onSurface - ), - ), - Text( - serversProvider.rewriteRules.data![index].domain, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface - ), - ), - ], - ), - const SizedBox(height: 3), - Row( - children: [ - Text( - "${AppLocalizations.of(context)!.answer}: ", - style: TextStyle( - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.onSurface - ), - ), - Text( - serversProvider.rewriteRules.data![index].answer, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface - ), - ), - ], - ), - ], - ), - IconButton( - onPressed: () => { - showDialog( - context: context, - builder: (context) => DeleteDnsRewrite( - onConfirm: () => deleteDnsRewrite(serversProvider.rewriteRules.data![index]) - ) - ) - }, - icon: const Icon(Icons.delete) - ) - ], - ), - ) - ), - ); - } - else { - return Center( - child: Text( - AppLocalizations.of(context)!.noRewriteRules, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ); - } + void updateRewriteRule(RewriteRules newRule, RewriteRules? previousRule) async { + ProcessModal processModal = ProcessModal(); + processModal.open(AppLocalizations.of(context)!.updatingRule); - case 2: - return SizedBox( - width: double.maxFinite, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon( - Icons.error, - color: Colors.red, - size: 50, - ), - const SizedBox(height: 30), - Text( - AppLocalizations.of(context)!.rewriteRulesNotLoaded, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ) - ], - ), - ); + final result = await rewriteRulesProvider.editDnsRewrite(newRule, previousRule!); - default: - return const SizedBox(); + processModal.close(); + + if (result == true) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.dnsRewriteRuleUpdated, + color: Colors.green + ); + } + else { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.dnsRewriteRuleNotUpdated, + color: Colors.red + ); } } return Scaffold( appBar: AppBar( title: Text(AppLocalizations.of(context)!.dnsRewrites), + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, + centerTitle: false, ), - body: generateBody(), - floatingActionButton: FloatingActionButton( - onPressed: () => { - showModalBottomSheet( - context: context, - builder: (context) => AddDnsRewriteModal( - onConfirm: addDnsRewrite, + body: SafeArea( + child: Stack( + children: [ + Builder( + builder: (context) { + switch (rewriteRulesProvider.loadStatus) { + case LoadStatus.loading: + return SizedBox( + width: double.maxFinite, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.loadingRewriteRules, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ); + + case LoadStatus.loaded: + if (rewriteRulesProvider.rewriteRules!.isNotEmpty) { + return RefreshIndicator( + onRefresh: () async { + final result = await rewriteRulesProvider.fetchRules(); + if (result == false) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.rewriteRulesNotLoaded, + color: Colors.red + ); + } + }, + child: ListView.builder( + controller: scrollController, + padding: const EdgeInsets.only(top: 0), + itemCount: rewriteRulesProvider.rewriteRules!.length, + itemBuilder: (context, index) => Card( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: InkWell( + onTap: () => { + if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + context: context, + builder: (context) => DnsRewriteModal( + onConfirm: updateRewriteRule, + dialog: true, + rule: rewriteRulesProvider.rewriteRules![index], + onDelete: (rule) => showDialog( + context: context, + builder: (context) => DeleteDnsRewrite( + onConfirm: () => deleteDnsRewrite(rule) + ) + ), + ), + ) + } + else { + showModalBottomSheet( + context: context, + useRootNavigator: true, + builder: (context) => DnsRewriteModal( + onConfirm: updateRewriteRule, + dialog: false, + rule: rewriteRulesProvider.rewriteRules![index], + onDelete: (rule) => showDialog( + context: context, + builder: (context) => DeleteDnsRewrite( + onConfirm: () => deleteDnsRewrite(rule) + ) + ), + ), + backgroundColor: Colors.transparent, + isScrollControlled: true, + ) + } + }, + borderRadius: BorderRadius.circular(10), + child: Padding( + padding: const EdgeInsets.only( + left: 16, top: 16, bottom: 16, right: 8 + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + "${AppLocalizations.of(context)!.domain}: ", + style: TextStyle( + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface + ), + ), + Text( + rewriteRulesProvider.rewriteRules![index].domain, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface + ), + ), + ], + ), + const SizedBox(height: 3), + Row( + children: [ + Text( + "${AppLocalizations.of(context)!.answer}: ", + style: TextStyle( + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface + ), + ), + Text( + rewriteRulesProvider.rewriteRules![index].answer, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface + ), + ), + ], + ), + ], + ), + Icon( + Icons.keyboard_arrow_right_rounded, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ) + ], + ), + ), + ), + ) + ), + ); + } + else { + return Center( + child: Text( + AppLocalizations.of(context)!.noRewriteRules, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ); + } + + case LoadStatus.error: + return SizedBox( + width: double.maxFinite, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon( + Icons.error, + color: Colors.red, + size: 50, + ), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.rewriteRulesNotLoaded, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ); + + default: + return const SizedBox(); + } + }, ), - backgroundColor: Colors.transparent, - isScrollControlled: true - ) - }, - child: const Icon(Icons.add), + AnimatedPositioned( + duration: const Duration(milliseconds: 100), + curve: Curves.easeInOut, + bottom: isVisible ? + appConfigProvider.showingSnackbar + ? 70 : 20 + : -70, + right: 20, + child: FloatingActionButton( + onPressed: () => { + if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + context: context, + builder: (context) => DnsRewriteModal( + onConfirm: addDnsRewrite, + dialog: true, + onDelete: (rule) => showDialog( + context: context, + builder: (context) => DeleteDnsRewrite( + onConfirm: () => deleteDnsRewrite(rule) + ) + ), + ), + ) + } + else { + showModalBottomSheet( + context: context, + useRootNavigator: true, + builder: (context) => DnsRewriteModal( + onConfirm: addDnsRewrite, + dialog: false, + onDelete: (rule) => showDialog( + context: context, + builder: (context) => DeleteDnsRewrite( + onConfirm: () => deleteDnsRewrite(rule) + ) + ), + ), + backgroundColor: Colors.transparent, + isScrollControlled: true + ) + } + }, + child: const Icon(Icons.add), + ), + ) + ], + ), ), ); } diff --git a/lib/screens/settings/dns_rewrites/server_version_needed.dart b/lib/screens/settings/dns_rewrites/server_version_needed.dart new file mode 100644 index 0000000..68ea426 --- /dev/null +++ b/lib/screens/settings/dns_rewrites/server_version_needed.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class ServerVersionNeeded extends StatelessWidget { + final String version; + + // ignore: use_super_parameters + const ServerVersionNeeded({ + Key? key, + required this.version + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Column( + children: [ + Icon( + Icons.system_update_rounded, + size: 24, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + const SizedBox(height: 16), + Text( + AppLocalizations.of(context)!.serverUpdateNeeded, + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface + ), + ) + ], + ), + content: Text( + AppLocalizations.of(context)!.updateYourServer(version), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.confirm) + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/settings/encryption/config_error_modal.dart b/lib/screens/settings/encryption/config_error_modal.dart index 1e2cfd0..4fdcec3 100644 --- a/lib/screens/settings/encryption/config_error_modal.dart +++ b/lib/screens/settings/encryption/config_error_modal.dart @@ -5,9 +5,9 @@ class EncryptionErrorModal extends StatelessWidget { final String error; const EncryptionErrorModal({ - Key? key, + super.key, required this.error, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/screens/settings/encryption/custom_text_field.dart b/lib/screens/settings/encryption/custom_text_field.dart index c149873..1290104 100644 --- a/lib/screens/settings/encryption/custom_text_field.dart +++ b/lib/screens/settings/encryption/custom_text_field.dart @@ -12,7 +12,7 @@ class EncryptionTextField extends StatelessWidget { final String? helperText; const EncryptionTextField({ - Key? key, + super.key, required this.enabled, required this.controller, required this.icon, @@ -22,12 +22,16 @@ class EncryptionTextField extends StatelessWidget { this.keyboardType, this.multiline, this.helperText, - }) : super(key: key); + }); @override Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width; + return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), + padding: width > 900 + ? const EdgeInsets.symmetric(horizontal: 8) + : const EdgeInsets.symmetric(horizontal: 16), child: ConstrainedBox( constraints: const BoxConstraints( maxHeight: 200 diff --git a/lib/screens/settings/encryption/encryption.dart b/lib/screens/settings/encryption/encryption.dart index 2c136fe..764f303 100644 --- a/lib/screens/settings/encryption/encryption.dart +++ b/lib/screens/settings/encryption/encryption.dart @@ -4,55 +4,40 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:adguard_home_manager/widgets/custom_checkbox_list_tile.dart'; import 'package:adguard_home_manager/widgets/section_label.dart'; import 'package:adguard_home_manager/widgets/custom_switch_list_tile.dart'; -import 'package:adguard_home_manager/screens/settings/encryption/config_error_modal.dart'; import 'package:adguard_home_manager/screens/settings/encryption/status.dart'; +import 'package:adguard_home_manager/screens/settings/encryption/reset_settings_modal.dart'; import 'package:adguard_home_manager/screens/settings/encryption/custom_text_field.dart'; import 'package:adguard_home_manager/screens/settings/encryption/master_switch.dart'; import 'package:adguard_home_manager/screens/settings/encryption/encryption_functions.dart'; +import 'package:adguard_home_manager/screens/settings/encryption/error_message.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/models/encryption.dart'; import 'package:adguard_home_manager/classes/process_modal.dart'; -import 'package:adguard_home_manager/functions/encode_base64.dart'; +import 'package:adguard_home_manager/functions/desktop_mode.dart'; +import 'package:adguard_home_manager/functions/base64.dart'; import 'package:adguard_home_manager/functions/snackbar.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart'; -class EncryptionSettings extends StatelessWidget { - const EncryptionSettings({Key? key}) : super(key: key); +class EncryptionSettings extends StatefulWidget { + const EncryptionSettings({super.key}); + @override - Widget build(BuildContext context) { - final serversProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - - return EncryptionSettingsWidget( - serversProvider: serversProvider, - appConfigProvider: appConfigProvider, - ); - } + State createState() => _EncryptionSettingsState(); } -class EncryptionSettingsWidget extends StatefulWidget { - final ServersProvider serversProvider; - final AppConfigProvider appConfigProvider; - - const EncryptionSettingsWidget({ - Key? key, - required this.serversProvider, - required this.appConfigProvider, - }) : super(key: key); - - @override - State createState() => _EncryptionSettingsWidgetState(); -} - -class _EncryptionSettingsWidgetState extends State { - int loadStatus = 0; +class _EncryptionSettingsState extends State { + LoadStatus loadStatus = LoadStatus.loading; bool enabled = false; + bool? _plainDns; + final TextEditingController domainNameController = TextEditingController(); String? domainError; @@ -88,90 +73,97 @@ class _EncryptionSettingsWidgetState extends State { bool localValidationValid = false; String? validDataError; - int certKeyValidApi = 0; + bool _dataValidApi = true; - Map? certKeyValid; + EncryptionValidation? certKeyValid; + String? encryptionResultMessage; bool formEdited = false; void fetchData({bool? showRefreshIndicator}) async { - setState(() => loadStatus = 0); + setState(() => loadStatus = LoadStatus.loading); - final result = await getEncryptionSettings(server: widget.serversProvider.selectedServer!); + final result = await Provider.of(context, listen: false).apiClient2!.getEncryptionSettings(); + if (!mounted) return; + + if (result.successful == true) { + final data = result.content as EncryptionData; + + await checkValidDataApi(data: data.toJson()); + if (!mounted) return; - if (mounted) { - if (result['result'] == 'success') { - await checkValidDataApi(data: result['data'].toJson()); - - setState(() { - enabled = result['data'].enabled; - domainNameController.text = result['data'].serverName ?? ''; - redirectHttps = result['data'].forceHttps; - httpsPortController.text = result['data'].portHttps != null ? result['data'].portHttps.toString() : ''; - tlsPortController.text = result['data'].portDnsOverTls != null ? result['data'].portDnsOverTls.toString() : ''; - dnsOverQuicPortController.text = result['data'].portDnsOverQuic != null ? result['data'].portDnsOverQuic.toString() : ''; - if (result['data'].certificateChain != '') { - certificateOption = 1; - certificateContentController.text = "-----BEGIN CERTIFICATE-----\n${result['data'].certificateChain}\n-----END CERTIFICATE-----"; - } - else { - certificateOption = 0; - certificatePathController.text = result['data'].certificatePath; - } - if (result['data'].privateKey != '' || result['data'].privateKeySaved == true) { - privateKeyOption = 1; - } - else { - privateKeyOption = 0; - privateKeyPathController.text = result['data'].privateKeyPath; - } - usePreviouslySavedKey = result['data'].privateKeySaved; - - loadStatus = 1; - }); - } - else { - widget.appConfigProvider.addLog(result['log']); - setState(() => loadStatus = 2); - } + setState(() { + enabled = data.enabled; + domainNameController.text = data.serverName ?? ''; + redirectHttps = data.forceHttps ?? false; + httpsPortController.text = data.portHttps != null ? data.portHttps.toString() : ''; + tlsPortController.text = data.portDnsOverTls != null ? data.portDnsOverTls.toString() : ''; + dnsOverQuicPortController.text = data.portDnsOverQuic != null ? data.portDnsOverQuic.toString() : ''; + if (data.certificateChain != '') { + certificateOption = 1; + certificateContentController.text = decodeBase64(data.certificateChain); + } + else { + certificateOption = 0; + certificatePathController.text = data.certificatePath; + } + if (data.privateKey != '' || data.privateKeySaved == true) { + privateKeyOption = 1; + } + else { + privateKeyOption = 0; + privateKeyPathController.text = data.privateKeyPath; + } + usePreviouslySavedKey = data.privateKeySaved; + _plainDns = data.servePlainDns; + loadStatus = LoadStatus.loaded; + }); + } + else { + setState(() => loadStatus = LoadStatus.error); } } Future checkValidDataApi({Map? data}) async { - setState(() => certKeyValidApi = 0); - - final result = await checkEncryptionSettings(server: widget.serversProvider.selectedServer!, data: data ?? { - "enabled": enabled, - "server_name": domainNameController.text, - "force_https": redirectHttps, - "port_https": httpsPortController.text != '' ? int.parse(httpsPortController.text) : null, - "port_dns_over_tls": tlsPortController.text != '' ? int.parse(tlsPortController.text) : null, - "port_dns_over_quic": dnsOverQuicPortController.text != '' ? int.parse(dnsOverQuicPortController.text) : null, - if (certificateOption == 1) "certificate_chain": encodeBase64(certificateContentController.text), - if (privateKeyOption == 1 && usePreviouslySavedKey == false) "private_key": encodeBase64(pastePrivateKeyController.text), - "private_key_saved": usePreviouslySavedKey, - if (certificateOption == 0) "certificate_path": certificatePathController.text, - if (privateKeyOption == 0) "private_key_path": privateKeyPathController.text, - }); - - if (result['result'] == 'success') { - setState(() { - if (result['data']['warning_validation'] != null && result['data']['warning_validation'] != '') { - certKeyValidApi = 2; - validDataError = result['data']['warning_validation']; - } - else { - certKeyValidApi = 1; - validDataError = null; - } - certKeyValid = result['data']; - }); - } - else { - if (result['log'].resBody != null) { - setState(() => validDataError = result['log'].resBody); + final result = await Provider.of(context, listen: false).apiClient2!.checkEncryptionSettings( + data: data ?? { + "enabled": enabled, + "server_name": domainNameController.text, + "force_https": redirectHttps, + "port_https": httpsPortController.text != '' ? int.parse(httpsPortController.text) : null, + "port_dns_over_tls": tlsPortController.text != '' ? int.parse(tlsPortController.text) : null, + "port_dns_over_quic": dnsOverQuicPortController.text != '' ? int.parse(dnsOverQuicPortController.text) : null, + if (certificateOption == 1) "certificate_chain": encodeBase64(certificateContentController.text), + if (privateKeyOption == 1 && usePreviouslySavedKey == false) "private_key": encodeBase64(pastePrivateKeyController.text), + "private_key_saved": usePreviouslySavedKey, + if (certificateOption == 0) "certificate_path": certificatePathController.text, + if (privateKeyOption == 0) "private_key_path": privateKeyPathController.text, + } + ); + + if (!mounted) return; + if (result.successful == true) { + final data = result.content as EncryptionValidationResult; + if (data.isObject == true) { + final object = data.encryptionValidation!; + setState(() { + if (object.warningValidation != null && object.warningValidation != '') { + _dataValidApi = false; + validDataError = object.warningValidation; + } + else { + _dataValidApi = true; + validDataError = null; + } + certKeyValid = object; + }); + } + else { + setState(() { + encryptionResultMessage = data.message; + _dataValidApi = false; + }); } - setState(() => certKeyValidApi = 2); } } @@ -184,25 +176,7 @@ class _EncryptionSettingsWidgetState extends State { tlsPortController.text != '' && tlsPortError == null && dnsOverQuicPortController.text != '' && - dnsOverQuicPortError == null && - (( - certificateOption == 0 && - certificatePathController.text != '' && - certificatePathError == null - ) || ( - certificateOption == 1 && - certificateContentController.text != '' && - certificateContentError == null - )) && - (( - privateKeyOption == 0 && - privateKeyPathController.text != '' && - privateKeyPathError == null - ) || ( - privateKeyOption == 1 && - pastePrivateKeyController.text != '' && - pastePrivateKeyError == null - )) + dnsOverQuicPortError == null ) { setState(() => localValidationValid = true); return true; @@ -233,418 +207,113 @@ class _EncryptionSettingsWidgetState extends State { final serversProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + void saveData() async { - ProcessModal processModal = ProcessModal(context: context); + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.savingConfig); - final result = await saveEncryptionSettings(server: serversProvider.selectedServer!, data: { - "enabled": enabled, - "server_name": domainNameController.text, - "force_https": redirectHttps, - "port_https": int.parse(httpsPortController.text), - "port_dns_over_tls": int.parse(tlsPortController.text), - "port_dns_over_quic": int.parse(dnsOverQuicPortController.text), - "certificate_chain": certificateContentController.text, - "private_key": pastePrivateKeyController.text, - "private_key_saved": usePreviouslySavedKey, - "certificate_path": certificatePathController.text, - "private_key_path": privateKeyPathController.text, - }); + final result = await serversProvider.apiClient2!.saveEncryptionSettings( + data: { + "enabled": enabled, + "server_name": domainNameController.text, + "force_https": redirectHttps, + "port_https": int.tryParse(httpsPortController.text), + "port_dns_over_tls": int.tryParse(tlsPortController.text), + "port_dns_over_quic": int.tryParse(dnsOverQuicPortController.text), + "certificate_chain": encodeBase64(certificateContentController.text), + "private_key": encodeBase64(pastePrivateKeyController.text), + "private_key_saved": usePreviouslySavedKey, + "certificate_path": certificatePathController.text, + "private_key_path": privateKeyPathController.text, + "serve_plain_dns": _plainDns + } + ); processModal.close(); - if (result['result'] == 'success') { - showSnacbkar( - context: context, + if (result.successful == true) { + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.encryptionConfigSaved, color: Colors.green ); } else { - appConfigProvider.addLog(result['log']); - - showSnacbkar( - context: context, + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.encryptionConfigNotSaved, color: Colors.red ); + + if (result.content != null) { + showDialog( + context: context, + builder: (context) => ErrorMessageEncryption( + errorMessage: result.content + ) + ); + } } } - Widget generateBody() { - switch (loadStatus) { - case 0: - return SizedBox( - width: double.maxFinite, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const CircularProgressIndicator(), - const SizedBox(height: 30), - Text( - AppLocalizations.of(context)!.loadingEncryptionSettings, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ) - ], - ) - ); + void resetSettings() async { + ProcessModal processModal = ProcessModal(); + processModal.open(AppLocalizations.of(context)!.resettingConfig); - case 1: - return ListView( - children: [ - EncryptionMasterSwitch( - value: enabled, - onChange: (value) { - setState(() => enabled = value); - onEditValidate(); - } - ), - SectionLabel( - label: AppLocalizations.of(context)!.serverConfiguration, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), - ), - EncryptionTextField( - enabled: enabled, - controller: domainNameController, - icon: Icons.link_rounded, - onChanged: (value) { - setState(() => domainError = validateDomain(context, value)); - onEditValidate(); - }, - errorText: domainError, - label: AppLocalizations.of(context)!.domainName, - helperText: AppLocalizations.of(context)!.domainNameDescription, - ), - const SizedBox(height: 10), - CustomSwitchListTile( - value: redirectHttps, - onChanged: (value) { - setState(() => redirectHttps = value); - onEditValidate(); - }, - title: AppLocalizations.of(context)!.redirectHttps, - disabled: !enabled, - ), - const SizedBox(height: 10), - EncryptionTextField( - enabled: enabled, - controller: httpsPortController, - icon: Icons.numbers_rounded, - onChanged: (value) { - setState(() => httpsPortError = validatePort(context, value)); - onEditValidate(); - }, - errorText: httpsPortError, - label: AppLocalizations.of(context)!.httpsPort, - keyboardType: TextInputType.number, - ), - const SizedBox(height: 30), - EncryptionTextField( - enabled: enabled, - controller: tlsPortController, - icon: Icons.numbers_rounded, - onChanged: (value) { - setState(() => tlsPortError = validatePort(context, value)); - onEditValidate(); - }, - errorText: tlsPortError, - label: AppLocalizations.of(context)!.tlsPort, - keyboardType: TextInputType.number, - ), - const SizedBox(height: 30), - EncryptionTextField( - enabled: enabled, - controller: dnsOverQuicPortController, - icon: Icons.numbers_rounded, - onChanged: (value) { - setState(() => dnsOverQuicPortError = validatePort(context, value)); - onEditValidate(); - }, - errorText: dnsOverQuicPortError, - label: AppLocalizations.of(context)!.dnsOverQuicPort, - keyboardType: TextInputType.number, - ), - SectionLabel( - label: AppLocalizations.of(context)!.certificates, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), - ), - Card( - margin: const EdgeInsets.symmetric(horizontal: 16), - child: Padding( - padding: const EdgeInsets.all(20), - child: Row( - children: [ - Icon( - Icons.info_rounded, - color: Theme.of(context).listTileTheme.iconColor, - ), - const SizedBox(width: 20), - Flexible( - child: Text( - AppLocalizations.of(context)!.certificatesDescription, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface, - ), - ) - ) - ], - ), - ), - ), - const SizedBox(height: 20), - RadioListTile( - value: 0, - groupValue: certificateOption, - onChanged: enabled == true - ? (value) { - setState(() => certificateOption = int.parse(value.toString())); - onEditValidate(); - } - : null, - title: Text( - AppLocalizations.of(context)!.certificateFilePath, - style: const TextStyle( - fontWeight: FontWeight.normal - ), - ), - ), - RadioListTile( - value: 1, - groupValue: certificateOption, - onChanged: enabled == true - ? (value) { - setState(() => certificateOption = int.parse(value.toString())); - onEditValidate(); - } - : null, - title: Text( - AppLocalizations.of(context)!.pasteCertificateContent, - style: const TextStyle( - fontWeight: FontWeight.normal - ), - ), - ), - const SizedBox(height: 10), - if (certificateOption == 0) EncryptionTextField( - enabled: enabled, - controller: certificatePathController, - icon: Icons.description_rounded, - onChanged: (value) { - setState(() => certificatePathError = validatePath(context, value)); - onEditValidate(); - }, - label: AppLocalizations.of(context)!.certificatePath, - errorText: certificatePathError, - ), - if (certificateOption == 1) EncryptionTextField( - enabled: enabled, - controller: certificateContentController, - icon: Icons.description_rounded, - onChanged: (value) { - setState(() => certificateContentError = validateCertificate(context, value)); - onEditValidate(); - }, - label: AppLocalizations.of(context)!.certificateContent, - errorText: certificateContentError, - multiline: true, - keyboardType: TextInputType.multiline, - ), - if (certKeyValid != null && (certificateContentController.text != '' || certificatePathController.text != '')) ...[ - const SizedBox(height: 20), - if (certKeyValid!['valid_chain'] != null) ...[ - Status( - valid: certKeyValid!['valid_chain'], - label: certKeyValid!['valid_chain'] == true - ? AppLocalizations.of(context)!.validCertificateChain - : AppLocalizations.of(context)!.invalidCertificateChain, - ), - const SizedBox(height: 10), - ], - if (certKeyValid!['subject'] != null) ...[ - Status( - valid: true, - label: "${AppLocalizations.of(context)!.subject}: ${certKeyValid!['subject']}" - ), - const SizedBox(height: 10), - ], - if (certKeyValid!['issuer'] != null) ...[ - Status( - valid: true, - label: "${AppLocalizations.of(context)!.issuer}: ${certKeyValid!['issuer']}" - ), - const SizedBox(height: 10), - ], - if (certKeyValid!['not_after'] != null) ...[ - Status( - valid: true, - label: "${AppLocalizations.of(context)!.expirationDate}: ${certKeyValid!['not_after']}" - ), - const SizedBox(height: 10), - ], - if (certKeyValid!['dns_names'] != null) ...[ - Status( - valid: true, - label: "${AppLocalizations.of(context)!.hostNames}: ${certKeyValid!['dns_names'].join(', ')}" - ), - const SizedBox(height: 10), - ], - ], - SectionLabel( - label: AppLocalizations.of(context)!.privateKey, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), - ), - RadioListTile( - value: 0, - groupValue: privateKeyOption, - onChanged: enabled == true - ? (value) { - setState(() => privateKeyOption = int.parse(value.toString())); - onEditValidate(); - } - : null, - title: Text( - AppLocalizations.of(context)!.privateKeyFile, - style: const TextStyle( - fontWeight: FontWeight.normal - ), - ), - ), - RadioListTile( - value: 1, - groupValue: privateKeyOption, - onChanged: enabled == true - ? (value) { - setState(() => privateKeyOption = int.parse(value.toString())); - onEditValidate(); - } - : null, - title: Text( - AppLocalizations.of(context)!.pastePrivateKey, - style: const TextStyle( - fontWeight: FontWeight.normal - ), - ), - ), - if (privateKeyOption == 0) const SizedBox(height: 10), - if (privateKeyOption == 1) ...[ - CustomSwitchListTile( - value: usePreviouslySavedKey, - onChanged: (value) => setState(() => usePreviouslySavedKey = value), - title: AppLocalizations.of(context)!.usePreviousKey, - ), - const SizedBox(height: 10) - ], - if (privateKeyOption == 0) EncryptionTextField( - enabled: enabled, - controller: privateKeyPathController, - icon: Icons.description_rounded, - onChanged: (value) { - setState(() => privateKeyPathError = validatePath(context, value)); - onEditValidate(); - }, - label: AppLocalizations.of(context)!.privateKeyPath, - errorText: privateKeyPathError, - ), - if (privateKeyOption == 1) EncryptionTextField( - enabled: enabled == true - ? !usePreviouslySavedKey - : false, - controller: pastePrivateKeyController, - icon: Icons.description_rounded, - onChanged: (value) { - setState(() => pastePrivateKeyError = validatePrivateKey(context, value)); - onEditValidate(); - }, - label: AppLocalizations.of(context)!.pastePrivateKey, - errorText: pastePrivateKeyError, - keyboardType: TextInputType.multiline, - multiline: true, - ), - const SizedBox(height: 20), - if (certKeyValid != null && (privateKeyPathController.text != '' || pastePrivateKeyController.text != '' || usePreviouslySavedKey == true)) ...[ - if (certKeyValid!['valid_key'] != null) ...[ - Status( - valid: certKeyValid!['valid_key'], - label: certKeyValid!['valid_key'] == true - ? AppLocalizations.of(context)!.validPrivateKey - : AppLocalizations.of(context)!.invalidPrivateKey, - ), - const SizedBox(height: 10) - ], - if (certKeyValid!['valid_pair'] != null && certKeyValid!['valid_pair'] == false) ...[ - Status( - valid: false, - label: AppLocalizations.of(context)!.keysNotMatch, - ), - const SizedBox(height: 10) - ], - if (certKeyValid!['key_type'] != null) ...[ - Status( - valid: true, - label: "${AppLocalizations.of(context)!.keyType}: ${certKeyValid!['key_type']}" - ), - const SizedBox(height: 10), - ], - const SizedBox(height: 10) - ] - ], - ); + final result = await serversProvider.apiClient2!.saveEncryptionSettings( + data: { + "enabled": false, + "server_name": "", + "force_https": false, + "port_https": 443, + "port_dns_over_tls": 853, + "port_dns_over_quic": 853, + "certificate_chain": "", + "private_key": "", + "private_key_saved": false, + "certificate_path": "", + "private_key_path": "", + "serve_plain_dns": true + } + ); + if (!mounted) return; - case 2: - return SizedBox( - width: double.maxFinite, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon( - Icons.error, - color: Colors.red, - size: 50, - ), - const SizedBox(height: 30), - Text( - AppLocalizations.of(context)!.encryptionSettingsNotLoaded, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ) - ], - ), - ); + processModal.close(); - default: - return const SizedBox(); + if (result.successful == true) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.configurationResetSuccessfully, + color: Colors.green + ); + } + else { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.configurationResetError, + color: Colors.red + ); } } return Scaffold( appBar: AppBar( title: Text(AppLocalizations.of(context)!.encryptionSettings), + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, + centerTitle: false, actions: [ IconButton( - onPressed: certKeyValidApi == 2 && validDataError != null - ? () => { - showDialog( - context: context, - builder: (context) => EncryptionErrorModal(error: validDataError!) - ) - } : null, - icon: generateStatus(context, appConfigProvider, localValidationValid, certKeyValidApi, formEdited), - tooltip: generateStatusString(context, localValidationValid, certKeyValidApi) + onPressed: () => showDialog( + context: context, + builder: (ctx) => ResetSettingsModal(onConfirm: resetSettings) + ), + icon: const Icon(Icons.restore_rounded), + tooltip: AppLocalizations.of(context)!.resetSettings, ), IconButton( - onPressed: localValidationValid == true && certKeyValidApi == 1 - ? () => saveData() + onPressed: localValidationValid ? + () => saveData() : null, icon: const Icon(Icons.save), tooltip: AppLocalizations.of(context)!.save, @@ -652,7 +321,409 @@ class _EncryptionSettingsWidgetState extends State { const SizedBox(width: 10), ], ), - body: generateBody(), + body: SafeArea( + child: Builder( + builder: (context) { + switch (loadStatus) { + case LoadStatus.loading: + return SizedBox( + width: double.maxFinite, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.loadingEncryptionSettings, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ) + ); + + case LoadStatus.loaded: + return ListView( + children: [ + if (_dataValidApi == false && (validDataError != null || encryptionResultMessage != null)) Card( + margin: const EdgeInsets.all(16), + color: Colors.red.withOpacity(0.2), + 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(validDataError ?? encryptionResultMessage ?? AppLocalizations.of(context)!.unknownError) + ) + ], + ), + ), + ), + EncryptionMasterSwitch( + value: enabled, + onChange: (value) { + setState(() => enabled = value); + onEditValidate(); + } + ), + if (_plainDns != null) ...[ + const SizedBox(height: 8), + CustomCheckboxListTile( + value: _plainDns!, + onChanged: (v) => setState(() => _plainDns = v), + title: AppLocalizations.of(context)!.enablePlainDns, + subtitle: AppLocalizations.of(context)!.enablePlainDnsDescription, + disabled: enabled == false, + ), + ], + SectionLabel( + label: AppLocalizations.of(context)!.serverConfiguration, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), + ), + EncryptionTextField( + enabled: enabled, + controller: domainNameController, + icon: Icons.link_rounded, + onChanged: (value) { + setState(() => domainError = validateDomain(context, value)); + onEditValidate(); + }, + errorText: domainError, + label: AppLocalizations.of(context)!.domainName, + helperText: AppLocalizations.of(context)!.domainNameDescription, + ), + const SizedBox(height: 10), + CustomSwitchListTile( + value: redirectHttps, + onChanged: (value) { + setState(() => redirectHttps = value); + onEditValidate(); + }, + title: AppLocalizations.of(context)!.redirectHttps, + disabled: !enabled, + ), + const SizedBox(height: 10), + Wrap( + children: [ + FractionallySizedBox( + widthFactor: width > 900 ? 0.33 : 1, + child: EncryptionTextField( + enabled: enabled, + controller: httpsPortController, + icon: Icons.numbers_rounded, + onChanged: (value) { + setState(() => httpsPortError = validatePort(context, value)); + onEditValidate(); + }, + errorText: httpsPortError, + label: AppLocalizations.of(context)!.httpsPort, + keyboardType: TextInputType.number, + ), + ), + Padding( + padding: width <= 900 + ? const EdgeInsets.symmetric(vertical: 24) + : const EdgeInsets.all(0), + child: FractionallySizedBox( + widthFactor: width > 900 ? 0.33 : 1, + child: EncryptionTextField( + enabled: enabled, + controller: tlsPortController, + icon: Icons.numbers_rounded, + onChanged: (value) { + setState(() => tlsPortError = validatePort(context, value)); + onEditValidate(); + }, + errorText: tlsPortError, + label: AppLocalizations.of(context)!.tlsPort, + keyboardType: TextInputType.number, + ), + ), + ), + FractionallySizedBox( + widthFactor: width > 900 ? 0.33 : 1, + child: EncryptionTextField( + enabled: enabled, + controller: dnsOverQuicPortController, + icon: Icons.numbers_rounded, + onChanged: (value) { + setState(() => dnsOverQuicPortError = validatePort(context, value)); + onEditValidate(); + }, + errorText: dnsOverQuicPortError, + label: AppLocalizations.of(context)!.dnsOverQuicPort, + keyboardType: TextInputType.number, + ), + ), + ], + ), + SectionLabel( + label: AppLocalizations.of(context)!.certificates, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), + ), + Card( + margin: const EdgeInsets.symmetric(horizontal: 16), + child: Padding( + padding: const EdgeInsets.all(20), + child: Row( + children: [ + Icon( + Icons.info_rounded, + color: Theme.of(context).listTileTheme.iconColor, + ), + const SizedBox(width: 20), + Flexible( + child: Text( + AppLocalizations.of(context)!.certificatesDescription, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + ) + ) + ], + ), + ), + ), + const SizedBox(height: 20), + RadioListTile( + value: 0, + groupValue: certificateOption, + onChanged: enabled == true + ? (value) { + setState(() => certificateOption = int.parse(value.toString())); + onEditValidate(); + } + : null, + title: Text( + AppLocalizations.of(context)!.certificateFilePath, + style: const TextStyle( + fontWeight: FontWeight.normal + ), + ), + ), + RadioListTile( + value: 1, + groupValue: certificateOption, + onChanged: enabled == true + ? (value) { + setState(() => certificateOption = int.parse(value.toString())); + onEditValidate(); + } + : null, + title: Text( + AppLocalizations.of(context)!.pasteCertificateContent, + style: const TextStyle( + fontWeight: FontWeight.normal + ), + ), + ), + const SizedBox(height: 10), + if (certificateOption == 0) EncryptionTextField( + enabled: enabled, + controller: certificatePathController, + icon: Icons.description_rounded, + onChanged: (value) { + setState(() => certificatePathError = validatePath(context, value)); + onEditValidate(); + }, + label: AppLocalizations.of(context)!.certificatePath, + errorText: certificatePathError, + ), + if (certificateOption == 1) EncryptionTextField( + enabled: enabled, + controller: certificateContentController, + icon: Icons.description_rounded, + onChanged: (value) { + setState(() => certificateContentError = validateCertificate(context, value)); + onEditValidate(); + }, + label: AppLocalizations.of(context)!.certificateContent, + errorText: certificateContentError, + multiline: true, + keyboardType: TextInputType.multiline, + ), + if (certKeyValid != null && (certificateContentController.text != '' || certificatePathController.text != '')) ...[ + const SizedBox(height: 20), + if (certKeyValid!.validChain != null) ...[ + Status( + valid: certKeyValid!.validChain ?? false, + label: certKeyValid!.validChain == true + ? AppLocalizations.of(context)!.validCertificateChain + : AppLocalizations.of(context)!.invalidCertificateChain, + ), + const SizedBox(height: 10), + ], + if (certKeyValid!.subject != null) ...[ + Status( + valid: true, + label: "${AppLocalizations.of(context)!.subject}: ${certKeyValid?.subject}" + ), + const SizedBox(height: 10), + ], + if (certKeyValid!.issuer != null) ...[ + Status( + valid: true, + label: "${AppLocalizations.of(context)!.issuer}: ${certKeyValid?.issuer}" + ), + const SizedBox(height: 10), + ], + if (certKeyValid!.notAfter != null) ...[ + Status( + valid: true, + label: "${AppLocalizations.of(context)!.expirationDate}: ${certKeyValid?.notAfter}" + ), + const SizedBox(height: 10), + ], + if (certKeyValid!.dnsNames != null) ...[ + Status( + valid: true, + label: "${AppLocalizations.of(context)!.hostNames}: ${certKeyValid?.dnsNames?.join(', ')}" + ), + const SizedBox(height: 10), + ], + ], + SectionLabel( + label: AppLocalizations.of(context)!.privateKey, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), + ), + RadioListTile( + value: 0, + groupValue: privateKeyOption, + onChanged: enabled == true + ? (value) { + setState(() => privateKeyOption = int.parse(value.toString())); + onEditValidate(); + } + : null, + title: Text( + AppLocalizations.of(context)!.privateKeyFile, + style: const TextStyle( + fontWeight: FontWeight.normal + ), + ), + ), + RadioListTile( + value: 1, + groupValue: privateKeyOption, + onChanged: enabled == true + ? (value) { + setState(() => privateKeyOption = int.parse(value.toString())); + onEditValidate(); + } + : null, + title: Text( + AppLocalizations.of(context)!.pastePrivateKey, + style: const TextStyle( + fontWeight: FontWeight.normal + ), + ), + ), + if (privateKeyOption == 0) const SizedBox(height: 10), + if (privateKeyOption == 1) ...[ + CustomSwitchListTile( + value: usePreviouslySavedKey, + onChanged: (value) => setState(() => usePreviouslySavedKey = value), + title: AppLocalizations.of(context)!.usePreviousKey, + ), + const SizedBox(height: 10) + ], + if (privateKeyOption == 0) EncryptionTextField( + enabled: enabled, + controller: privateKeyPathController, + icon: Icons.description_rounded, + onChanged: (value) { + setState(() => privateKeyPathError = validatePath(context, value)); + onEditValidate(); + }, + label: AppLocalizations.of(context)!.privateKeyPath, + errorText: privateKeyPathError, + ), + if (privateKeyOption == 1) EncryptionTextField( + enabled: enabled == true + ? !usePreviouslySavedKey + : false, + controller: pastePrivateKeyController, + icon: Icons.description_rounded, + onChanged: (value) { + setState(() => pastePrivateKeyError = validatePrivateKey(context, value)); + onEditValidate(); + }, + label: AppLocalizations.of(context)!.pastePrivateKey, + errorText: pastePrivateKeyError, + keyboardType: TextInputType.multiline, + multiline: true, + ), + const SizedBox(height: 20), + if (certKeyValid != null && (privateKeyPathController.text != '' || pastePrivateKeyController.text != '' || usePreviouslySavedKey == true)) ...[ + if (certKeyValid!.validKey != null) ...[ + Status( + valid: certKeyValid!.validKey ?? false, + label: certKeyValid!.validKey == true + ? AppLocalizations.of(context)!.validPrivateKey + : AppLocalizations.of(context)!.invalidPrivateKey, + ), + const SizedBox(height: 10) + ], + if (certKeyValid!.validPair != null && certKeyValid!.validPair == false) ...[ + Status( + valid: false, + label: AppLocalizations.of(context)!.keysNotMatch, + ), + const SizedBox(height: 10) + ], + if (certKeyValid!.keyType != null) ...[ + Status( + valid: true, + label: "${AppLocalizations.of(context)!.keyType}: ${certKeyValid!.keyType}" + ), + const SizedBox(height: 10), + ], + const SizedBox(height: 10) + ] + ], + ); + + case LoadStatus.error: + return SizedBox( + width: double.maxFinite, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon( + Icons.error, + color: Colors.red, + size: 50, + ), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.encryptionSettingsNotLoaded, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ); + + default: + return const SizedBox(); + } + }, + ), + ) ); } } \ No newline at end of file diff --git a/lib/screens/settings/encryption/encryption_functions.dart b/lib/screens/settings/encryption/encryption_functions.dart index c9c0c4b..c7700e4 100644 --- a/lib/screens/settings/encryption/encryption_functions.dart +++ b/lib/screens/settings/encryption/encryption_functions.dart @@ -1,11 +1,11 @@ +import 'package:adguard_home_manager/constants/regexps.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; String? validateDomain(BuildContext context, String domain) { - RegExp regExp = RegExp(r'^([a-z0-9|-]+\.)*[a-z0-9|-]+\.[a-z]+$'); - if (regExp.hasMatch(domain)) { + if (Regexps.domain.hasMatch(domain)) { return null; } else { @@ -23,8 +23,7 @@ String? validatePort(BuildContext context, String value) { } String? validateCertificate(BuildContext context, String cert) { - final regExp = RegExp(r'(-{3,}(\bBEGIN CERTIFICATE\b))|(-{3,}-{3,}(\END CERTIFICATE\b)-{3,})', multiLine: true); - if (regExp.hasMatch(cert.replaceAll('\n', ''))) { + if (Regexps.certificate.hasMatch(cert.replaceAll('\n', ''))) { return null; } else { @@ -33,8 +32,7 @@ String? validateCertificate(BuildContext context, String cert) { } String? validatePrivateKey(BuildContext context, String cert) { - final regExp = RegExp(r'(-{3,}(\bBEGIN\b).*(PRIVATE KEY\b))|(-{3,}-{3,}(\bEND\b).*(PRIVATE KEY\b)-{3,})', multiLine: true); - if (regExp.hasMatch(cert.replaceAll('\n', ''))) { + if (Regexps.privateKey.hasMatch(cert.replaceAll('\n', ''))) { return null; } else { @@ -43,8 +41,7 @@ String? validatePrivateKey(BuildContext context, String cert) { } String? validatePath(BuildContext context, String cert) { - final regExp = RegExp(r'^(\/{0,1}(?!\/))[A-Za-z0-9\/\-_]+(\.([a-zA-Z]+))?$'); - if (regExp.hasMatch(cert)) { + if (Regexps.path.hasMatch(cert)) { return null; } else { diff --git a/lib/screens/settings/encryption/error_message.dart b/lib/screens/settings/encryption/error_message.dart new file mode 100644 index 0000000..01dc221 --- /dev/null +++ b/lib/screens/settings/encryption/error_message.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class ErrorMessageEncryption extends StatelessWidget { + final String errorMessage; + + const ErrorMessageEncryption({ + super.key, + required this.errorMessage, + }); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(AppLocalizations.of(context)!.error), + content: Text(errorMessage), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.close) + ) + ], + ) + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/settings/encryption/master_switch.dart b/lib/screens/settings/encryption/master_switch.dart index af68536..474c0f9 100644 --- a/lib/screens/settings/encryption/master_switch.dart +++ b/lib/screens/settings/encryption/master_switch.dart @@ -6,10 +6,10 @@ class EncryptionMasterSwitch extends StatelessWidget { final void Function(bool) onChange; const EncryptionMasterSwitch({ - Key? key, + super.key, required this.value, required this.onChange - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -58,7 +58,6 @@ class EncryptionMasterSwitch extends StatelessWidget { Switch( value: value, onChanged: (value) => onChange(value), - activeColor: Theme.of(context).colorScheme.primary, ), ], ), diff --git a/lib/screens/settings/encryption/reset_settings_modal.dart b/lib/screens/settings/encryption/reset_settings_modal.dart new file mode 100644 index 0000000..4429162 --- /dev/null +++ b/lib/screens/settings/encryption/reset_settings_modal.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class ResetSettingsModal extends StatelessWidget { + final void Function() onConfirm; + + const ResetSettingsModal({ + super.key, + required this.onConfirm, + }); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Column( + children: [ + Icon( + Icons.restore_rounded, + size: 24, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + const SizedBox(height: 16), + Text( + AppLocalizations.of(context)!.resetSettings, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface + ), + ) + ], + ), + content: Text( + AppLocalizations.of(context)!.resetEncryptionSettingsDescription, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.cancel) + ), + TextButton( + onPressed: () { + onConfirm(); + Navigator.pop(context); + }, + child: Text(AppLocalizations.of(context)!.confirm) + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/settings/encryption/status.dart b/lib/screens/settings/encryption/status.dart index eb344ae..0c11f5a 100644 --- a/lib/screens/settings/encryption/status.dart +++ b/lib/screens/settings/encryption/status.dart @@ -8,10 +8,10 @@ class Status extends StatelessWidget { final String label; const Status({ - Key? key, + super.key, required this.valid, required this.label - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/screens/settings/general_settings.dart b/lib/screens/settings/general_settings.dart deleted file mode 100644 index cf27e34..0000000 --- a/lib/screens/settings/general_settings.dart +++ /dev/null @@ -1,102 +0,0 @@ -// ignore_for_file: use_build_context_synchronously - -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; - -import 'package:adguard_home_manager/providers/app_config_provider.dart'; - -class GeneralSettings extends StatelessWidget { - const GeneralSettings({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final appConfigProvider = Provider.of(context); - - Future updateHideZeroValues(bool newStatus) async { - final result = await appConfigProvider.setHideZeroValues(newStatus); - if (result == true) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.settingsUpdatedSuccessfully), - backgroundColor: Colors.green, - ) - ); - } - else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.cannotUpdateSettings), - backgroundColor: Colors.red, - ) - ); - } - } - - Future updateShowNameTimeLogs(bool newStatus) async { - final result = await appConfigProvider.setShowNameTimeLogs(newStatus); - if (result == true) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.settingsUpdatedSuccessfully), - backgroundColor: Colors.green, - ) - ); - } - else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.cannotUpdateSettings), - backgroundColor: Colors.red, - ) - ); - } - } - - return Scaffold( - appBar: AppBar( - title: Text(AppLocalizations.of(context)!.generalSettings) , - ), - body: ListView( - children: [ - CustomListTile( - icon: Icons.exposure_zero_rounded, - title: AppLocalizations.of(context)!.hideZeroValues, - subtitle: AppLocalizations.of(context)!.hideZeroValuesDescription, - trailing: Switch( - value: appConfigProvider.hideZeroValues, - onChanged: updateHideZeroValues, - activeColor: Theme.of(context).colorScheme.primary, - ), - onTap: () => updateHideZeroValues(!appConfigProvider.hideZeroValues), - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - left: 20, - right: 10 - ) - ), - CustomListTile( - icon: Icons.more, - title: AppLocalizations.of(context)!.nameTimeLogs, - subtitle: AppLocalizations.of(context)!.nameTimeLogsDescription, - trailing: Switch( - value: appConfigProvider.showNameTimeLogs, - onChanged: updateShowNameTimeLogs, - activeColor: Theme.of(context).colorScheme.primary, - ), - onTap: () => updateShowNameTimeLogs(!appConfigProvider.showNameTimeLogs), - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - left: 20, - right: 10 - ) - ), - ], - ), - ); - } -} \ No newline at end of file diff --git a/lib/screens/settings/general_settings/general_settings.dart b/lib/screens/settings/general_settings/general_settings.dart new file mode 100644 index 0000000..f32446d --- /dev/null +++ b/lib/screens/settings/general_settings/general_settings.dart @@ -0,0 +1,265 @@ +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/settings.dart'; +import 'package:adguard_home_manager/screens/settings/general_settings/top_items_list/top_items_list_settings.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/functions/desktop_mode.dart'; +import 'package:adguard_home_manager/functions/snackbar.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; + +class GeneralSettings extends StatefulWidget { + final bool splitView; + + const GeneralSettings({ + super.key, + required this.splitView, + }); + + @override + State createState() => _GeneralSettingsState(); +} + +enum AppUpdatesStatus { available, checking, recheck } + +class _GeneralSettingsState extends State { + AppUpdatesStatus appUpdatesStatus = AppUpdatesStatus.recheck; + + @override + Widget build(BuildContext context) { + final appConfigProvider = Provider.of(context); + + final width = MediaQuery.of(context).size.width; + + Future updateSettings({ + required bool newStatus, + required Future Function(bool) function + }) async { + final result = await function(newStatus); + if (!context.mounted) return; + if (result == true) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.settingsUpdatedSuccessfully, + color: Colors.green + ); + } + else { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.cannotUpdateSettings, + color: Colors.red + ); + } + } + + // Future checkUpdatesAvailable() async { + // setState(() => appUpdatesStatus = AppUpdatesStatus.checking); + + // final res = await checkAppUpdates( + // currentBuildNumber: appConfigProvider.getAppInfo!.buildNumber, + // setUpdateAvailable: appConfigProvider.setAppUpdatesAvailable, + // installationSource: appConfigProvider.installationSource, + // isBeta: appConfigProvider.getAppInfo!.version.contains('beta'), + // ); + + // if (!mounted) return; + // if (res != null) { + // setState(() => appUpdatesStatus = AppUpdatesStatus.available); + // } + // else { + // setState(() => appUpdatesStatus = AppUpdatesStatus.recheck); + // } + // } + + // Widget generateAppUpdateStatus() { + // if (appUpdatesStatus == AppUpdatesStatus.available) { + // return IconButton( + // onPressed: appConfigProvider.appUpdatesAvailable != null + // ? () async { + // final link = getAppUpdateDownloadLink(appConfigProvider.appUpdatesAvailable!); + // if (link != null) { + // openUrl(link); + // } + // } + // : null, + // icon: const Icon(Icons.download_rounded), + // tooltip: AppLocalizations.of(context)!.downloadUpdate, + // ); + // } + // else if (appUpdatesStatus == AppUpdatesStatus.checking) { + // return const Padding( + // padding: EdgeInsets.only(right: 16), + // child: SizedBox( + // width: 24, + // height: 24, + // child: CircularProgressIndicator( + // strokeWidth: 3, + // ) + // ), + // ); + // } + // else { + // return IconButton( + // onPressed: checkUpdatesAvailable, + // icon: const Icon(Icons.refresh_rounded), + // tooltip: AppLocalizations.of(context)!.checkUpdates, + // ); + // } + // } + + return Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context)!.generalSettings), + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, + ), + body: SafeArea( + child: ListView( + children: [ + SectionLabel(label: AppLocalizations.of(context)!.home), + CustomListTile( + icon: Icons.exposure_zero_rounded, + title: AppLocalizations.of(context)!.hideZeroValues, + subtitle: AppLocalizations.of(context)!.hideZeroValuesDescription, + trailing: Switch( + value: appConfigProvider.hideZeroValues, + onChanged: (value) => updateSettings( + newStatus: value, + function: appConfigProvider.setHideZeroValues + ), + ), + onTap: () => updateSettings( + newStatus: !appConfigProvider.hideZeroValues, + function: appConfigProvider.setHideZeroValues + ), + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + left: 16, + right: 10 + ) + ), + CustomListTile( + icon: Icons.show_chart_rounded, + title: AppLocalizations.of(context)!.combinedChart, + subtitle: AppLocalizations.of(context)!.combinedChartDescription, + trailing: Switch( + value: appConfigProvider.combinedChartHome, + onChanged: (value) => updateSettings( + newStatus: value, + function: appConfigProvider.setCombinedChartHome + ), + ), + onTap: () => updateSettings( + newStatus: !appConfigProvider.combinedChartHome, + function: appConfigProvider.setCombinedChartHome + ), + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + left: 16, + right: 10 + ) + ), + CustomListTile( + icon: Icons.remove_red_eye_rounded, + title: AppLocalizations.of(context)!.hideServerAddress, + subtitle: AppLocalizations.of(context)!.hideServerAddressDescription, + trailing: Switch( + value: appConfigProvider.hideServerAddress, + onChanged: (value) => updateSettings( + newStatus: value, + function: appConfigProvider.setHideServerAddress + ), + ), + onTap: () => updateSettings( + newStatus: !appConfigProvider.hideServerAddress, + function: appConfigProvider.setHideServerAddress + ), + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + left: 16, + right: 10 + ) + ), + CustomListTile( + icon: Icons.reorder_rounded, + title: AppLocalizations.of(context)!.topItemsOrder, + subtitle: AppLocalizations.of(context)!.topItemsOrderDescription, + onTap: () => widget.splitView == true + ? Navigator.of(settingsNavigatorKey.currentContext!).push( + MaterialPageRoute(builder: (ctx) => const TopItemsListSettings()) + ) + : Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const TopItemsListSettings() + ) + ) + ), + SectionLabel(label: AppLocalizations.of(context)!.logs), + CustomListTile( + icon: Icons.timer_rounded, + title: AppLocalizations.of(context)!.timeLogs, + subtitle: AppLocalizations.of(context)!.timeLogsDescription, + trailing: Switch( + value: appConfigProvider.showTimeLogs, + onChanged: (value) => updateSettings( + newStatus: value, + function: appConfigProvider.setshowTimeLogs + ), + ), + onTap: () => updateSettings( + newStatus: !appConfigProvider.showTimeLogs, + function: appConfigProvider.setshowTimeLogs + ), + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + left: 16, + right: 10 + ) + ), + CustomListTile( + icon: Icons.more, + title: AppLocalizations.of(context)!.ipLogs, + subtitle: AppLocalizations.of(context)!.ipLogsDescription, + trailing: Switch( + value: appConfigProvider.showIpLogs, + onChanged: (value) => updateSettings( + newStatus: value, + function: appConfigProvider.setShowIpLogs + ), + ), + onTap: () => updateSettings( + newStatus: !appConfigProvider.showIpLogs, + function: appConfigProvider.setShowIpLogs + ), + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + left: 16, + right: 10 + ) + ), + // if (!(Platform.isAndroid || Platform.isIOS) || (Platform.isAndroid && (appConfigProvider.installationSource == InstallationAppReferrer.androidManually ))) ...[ + // SectionLabel(label: AppLocalizations.of(context)!.application), + // CustomListTile( + // icon: Icons.system_update_rounded, + // title: AppLocalizations.of(context)!.appUpdates, + // subtitle: appConfigProvider.appUpdatesAvailable != null + // ? AppLocalizations.of(context)!.updateAvailable + // : AppLocalizations.of(context)!.usingLatestVersion, + // trailing: generateAppUpdateStatus() + // ) + // ] + ], + ), + ) + ); + } +} \ No newline at end of file diff --git a/lib/screens/settings/general_settings/top_items_list/reorderable_top_items_home.dart b/lib/screens/settings/general_settings/top_items_list/reorderable_top_items_home.dart new file mode 100644 index 0000000..332a64b --- /dev/null +++ b/lib/screens/settings/general_settings/top_items_list/reorderable_top_items_home.dart @@ -0,0 +1,314 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_reorderable_list/flutter_reorderable_list.dart' as reorderable_list; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; + +import 'package:adguard_home_manager/constants/enums.dart'; + +class _ItemData { + final HomeTopItems title; + final Key key; + + const _ItemData({ + required this.title, + required this.key + }); +} + +enum DraggingMode { + iOS, + android, +} + +class ReorderableTopItemsHome extends StatefulWidget { + final List persistHomeTopItems; + final void Function(List value) setPersistHomeTopItems; + + const ReorderableTopItemsHome({ + super.key, + required this.persistHomeTopItems, + required this.setPersistHomeTopItems, + }); + + @override + State createState() => _ReorderableTopItemsHomeState(); +} + +class _ReorderableTopItemsHomeState extends State { + List homeTopItemsList = []; + List<_ItemData> renderItems = []; + + int _indexOfKey(Key key) { + return renderItems.indexWhere((_ItemData d) => d.key == key); + } + + bool _reorderCallback(Key item, Key newPosition) { + int draggingIndex = _indexOfKey(item); + int newPositionIndex = _indexOfKey(newPosition); + + final draggedItem = renderItems[draggingIndex]; + + final List reorderedItems = reorderEnumItems(draggingIndex, newPositionIndex); + + setState(() { + renderItems.removeAt(draggingIndex); + renderItems.insert(newPositionIndex, draggedItem); + homeTopItemsList = reorderedItems; + }); + + return true; + } + + void _reorderDone(Key item) { + renderItems[_indexOfKey(item)]; + widget.setPersistHomeTopItems(homeTopItemsList); + } + + List reorderEnumItems(int oldIndex, int newIndex) { + final List list = [...homeTopItemsList]; + final HomeTopItems item = list.removeAt(oldIndex); + list.insert(newIndex, item); + return list; + } + + @override + void initState() { + homeTopItemsList = widget.persistHomeTopItems; + renderItems = widget.persistHomeTopItems.asMap().entries.map( + (e) => _ItemData( + key: ValueKey(e.key), + title: e.value, + ) + ).toList(); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + final draggingMode = Platform.isAndroid + ? DraggingMode.android + : DraggingMode.iOS; + + return SafeArea( + top: false, + bottom: true, + child: Builder( + builder: (context) => CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + ), + SliverList.list( + children: [ + Card( + margin: const EdgeInsets.all(16), + 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)!.topItemsReorderInfo) + ) + ], + ), + ), + ), + if (homeTopItemsList.isEmpty) Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Center( + child: Text( + AppLocalizations.of(context)!.noElementsReorderMessage, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 18, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ), + ), + ], + ), + ), + if (homeTopItemsList.isNotEmpty) reorderable_list.ReorderableList( + onReorder: _reorderCallback, + onReorderDone: _reorderDone, + child: ListView.builder( + primary: false, + shrinkWrap: true, + padding: const EdgeInsets.only(top: 0), + itemBuilder: (context, index) => reorderable_list.ReorderableItem( + key: renderItems[index].key, + childBuilder: (context, state) { + if (draggingMode == DraggingMode.android) { + return reorderable_list.DelayedReorderableListener( + child: _ReorderableTile( + draggingMode: draggingMode, + isFirst: index == 0, + isLast: index == renderItems.length - 1, + state: state, + tileWidget: _TopItemTile(tile: renderItems[index].title), + ), + ); + } + else { + return _ReorderableTile( + draggingMode: draggingMode, + isFirst: index == 0, + isLast: index == renderItems.length - 1, + state: state, + tileWidget: _TopItemTile(tile: renderItems[index].title), + ); + } + }, + ), + itemCount: renderItems.length, + ) + ), + ] + ) + ], + ), + ), + ); + } +} + +class _TopItemTile extends StatelessWidget { + final HomeTopItems tile; + + const _TopItemTile({ + required this.tile, + }); + + @override + Widget build(BuildContext context) { + switch (tile) { + case HomeTopItems.queriedDomains: + return CustomListTile( + title: AppLocalizations.of(context)!.topQueriedDomains, + icon: Icons.install_desktop_outlined, + padding: const EdgeInsets.all(16) + ); + + case HomeTopItems.blockedDomains: + return CustomListTile( + title: AppLocalizations.of(context)!.topBlockedDomains, + icon: Icons.block_rounded, + padding: const EdgeInsets.all(16) + ); + + case HomeTopItems.recurrentClients: + return CustomListTile( + title: AppLocalizations.of(context)!.topClients, + icon: Icons.smartphone_rounded, + padding: const EdgeInsets.all(16) + ); + + case HomeTopItems.topUpstreams: + return CustomListTile( + title: AppLocalizations.of(context)!.topUpstreams, + icon: Icons.upload_file_rounded, + padding: const EdgeInsets.all(16) + ); + + case HomeTopItems.avgUpstreamResponseTime: + return CustomListTile( + title: AppLocalizations.of(context)!.averageUpstreamResponseTime, + icon: Icons.timer_rounded, + padding: const EdgeInsets.all(16) + ); + + default: + return const SizedBox(); + } + } +} + +class _ReorderableTile extends StatelessWidget { + final Widget tileWidget; + final bool isFirst; + final bool isLast; + final reorderable_list.ReorderableItemState state; + final DraggingMode draggingMode; + + const _ReorderableTile({ + required this.tileWidget, + required this.isFirst, + required this.isLast, + required this.state, + required this.draggingMode + }); + + @override + Widget build(BuildContext context) { + BoxDecoration getDecoration() { + if ( + state == reorderable_list.ReorderableItemState.dragProxy || + state == reorderable_list.ReorderableItemState.dragProxyFinished + ) { + return BoxDecoration( + color: Theme.of(context).colorScheme.surface.withOpacity(0.7) + ); + } + else { + bool placeholder = state == reorderable_list.ReorderableItemState.placeholder; + return BoxDecoration( + border: Border( + top: isFirst && !placeholder ? BorderSide( + width: 1, + color: Theme.of(context).colorScheme.primary.withOpacity(0.1) + ) : BorderSide.none, + bottom: isLast && placeholder ? BorderSide.none : BorderSide( + width: 1, + color: Theme.of(context).colorScheme.primary.withOpacity(0.1) + ), + ), + ); + } + } + + return Container( + decoration: getDecoration(), + child: SafeArea( + top: false, + bottom: false, + child: Opacity( + opacity: state == reorderable_list.ReorderableItemState.placeholder ? 0.0 : 1.0, + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: tileWidget + ), + if (draggingMode == DraggingMode.iOS) reorderable_list.ReorderableListener( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Center( + child: Icon( + Icons.reorder, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + ) + ], + ), + ), + ) + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/settings/general_settings/top_items_list/show_hide_top_items_list.dart b/lib/screens/settings/general_settings/top_items_list/show_hide_top_items_list.dart new file mode 100644 index 0000000..3088692 --- /dev/null +++ b/lib/screens/settings/general_settings/top_items_list/show_hide_top_items_list.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/widgets/custom_switch_list_tile.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; + +class ShowHideTopItemsList extends StatelessWidget { + final List enabledHomeTopItems; + final void Function(List) setEnabledHomeTopItems; + + const ShowHideTopItemsList({ + super.key, + required this.enabledHomeTopItems, + required this.setEnabledHomeTopItems, + }); + + @override + Widget build(BuildContext context) { + const padding = EdgeInsets.symmetric(horizontal: 16, vertical: 8); + + void updateValue(HomeTopItems value, bool newStatus) { + if (newStatus == true) { + setEnabledHomeTopItems([ + ...enabledHomeTopItems, + value + ]); + } + else { + setEnabledHomeTopItems(enabledHomeTopItems.where((e) => e != value).toList()); + } + } + + return SafeArea( + top: false, + bottom: true, + child: Builder( + builder: (context) => CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + ), + SliverList.list( + children: [ + const SizedBox(height: 8), + CustomSwitchListTile( + value: enabledHomeTopItems.contains(HomeTopItems.queriedDomains), + onChanged: (v) => updateValue(HomeTopItems.queriedDomains, v), + title: AppLocalizations.of(context)!.topQueriedDomains, + leadingIcon: Icons.install_desktop_outlined, + padding: padding, + ), + CustomSwitchListTile( + value: enabledHomeTopItems.contains(HomeTopItems.blockedDomains), + onChanged: (v) => updateValue(HomeTopItems.blockedDomains, v), + title: AppLocalizations.of(context)!.topBlockedDomains, + leadingIcon: Icons.block_rounded, + padding: padding, + ), + CustomSwitchListTile( + value: enabledHomeTopItems.contains(HomeTopItems.recurrentClients), + onChanged: (v) => updateValue(HomeTopItems.recurrentClients, v), + title: AppLocalizations.of(context)!.topClients, + leadingIcon: Icons.smartphone_rounded, + padding: padding, + ), + CustomSwitchListTile( + value: enabledHomeTopItems.contains(HomeTopItems.topUpstreams), + onChanged: (v) => updateValue(HomeTopItems.topUpstreams, v), + title: AppLocalizations.of(context)!.topUpstreams, + leadingIcon: Icons.upload_file_rounded, + padding: padding, + ), + CustomSwitchListTile( + value: enabledHomeTopItems.contains(HomeTopItems.avgUpstreamResponseTime), + onChanged: (v) => updateValue(HomeTopItems.avgUpstreamResponseTime, v), + title: AppLocalizations.of(context)!.averageUpstreamResponseTime, + leadingIcon: Icons.timer_rounded, + padding: padding, + ), + ] + ) + ], + ), + ) + ); + } +} \ No newline at end of file diff --git a/lib/screens/settings/general_settings/top_items_list/top_items_list_settings.dart b/lib/screens/settings/general_settings/top_items_list/top_items_list_settings.dart new file mode 100644 index 0000000..89b1613 --- /dev/null +++ b/lib/screens/settings/general_settings/top_items_list/top_items_list_settings.dart @@ -0,0 +1,131 @@ +import 'package:flutter/foundation.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/general_settings/top_items_list/show_hide_top_items_list.dart'; +import 'package:adguard_home_manager/screens/settings/general_settings/top_items_list/reorderable_top_items_home.dart'; + +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/functions/desktop_mode.dart'; +import 'package:adguard_home_manager/functions/snackbar.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; + +class TopItemsListSettings extends StatefulWidget { + const TopItemsListSettings({super.key}); + + @override + State createState() => _TopItemsListSettingsState(); +} + +class _TopItemsListSettingsState extends State with TickerProviderStateMixin { + late TabController _tabController; + + List persistHomeTopItemsList = []; + + @override + void initState() { + final appConfigProvider = Provider.of(context, listen: false); + persistHomeTopItemsList = appConfigProvider.homeTopItemsOrder; + + super.initState(); + + _tabController = TabController(length: 2, vsync: this); + } + + @override + Widget build(BuildContext context) { + final appConfigProvider = Provider.of(context); + + final width = MediaQuery.of(context).size.width; + + void saveSettings() async { + final result = await appConfigProvider.setHomeTopItemsOrder(persistHomeTopItemsList); + if (!context.mounted) return; + if (result == true) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.settingsSaved, + color: Colors.green + ); + } + else { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.settingsNotSaved, + color: Colors.red + ); + } + } + + return Scaffold( + body: DefaultTabController( + length: 2, + child: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverAppBar( + pinned: true, + floating: true, + centerTitle: false, + forceElevated: innerBoxIsScrolled, + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, + title: Text(AppLocalizations.of(context)!.topItemsOrder), + actions: [ + IconButton( + onPressed: !listEquals(appConfigProvider.homeTopItemsOrder, persistHomeTopItemsList) + ? () => saveSettings() + : null, + icon: const Icon(Icons.save_rounded), + tooltip: AppLocalizations.of(context)!.save, + ), + const SizedBox(width: 8) + ], + bottom: TabBar( + controller: _tabController, + unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant, + tabs: [ + Tab( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.reorder_rounded), + const SizedBox(width: 8), + Text(AppLocalizations.of(context)!.reorder) + ], + ), + ), + Tab( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.remove_red_eye_rounded), + const SizedBox(width: 8), + Text(AppLocalizations.of(context)!.showHide) + ], + ), + ), + ] + ) + ), + ) + ], + body: TabBarView( + controller: _tabController, + children: [ + ReorderableTopItemsHome( + persistHomeTopItems: persistHomeTopItemsList, + setPersistHomeTopItems: (v) => setState(() => persistHomeTopItemsList = v), + ), + ShowHideTopItemsList( + enabledHomeTopItems: persistHomeTopItemsList, + setEnabledHomeTopItems: (v) => setState(() => persistHomeTopItemsList = v), + ) + ] + ) + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/settings/logs_settings/config_widgets.dart b/lib/screens/settings/logs_settings/config_widgets.dart new file mode 100644 index 0000000..066f779 --- /dev/null +++ b/lib/screens/settings/logs_settings/config_widgets.dart @@ -0,0 +1,212 @@ +import 'package:adguard_home_manager/constants/regexps.dart'; +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 retentionItems; + final String? retentionTime; + final void Function(String?) updateRetentionTime; + final void Function() onClear; + final void Function() onConfirm; + final List ignoredDomainsControllers; + final void Function(List) updateIgnoredDomainsControllers; + final TextEditingController customTimeController; + final String? customTimeError; + final void Function(String) validateCustomTime; + + 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, + required this.customTimeController, + required this.customTimeError, + required this.validateCustomTime, + }); + + @override + Widget build(BuildContext context) { + const Uuid uuid = Uuid(); + + final List dropdownItemTranslation = [ + AppLocalizations.of(context)!.custom, + 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) { + bool error = false; + if (Regexps.domain.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() + ); + } +print(retentionTime); + 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.toString()), + 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(top: 24, left: 16, right: 16), + 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: 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, + ), + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/settings/logs_settings/logs_settings.dart b/lib/screens/settings/logs_settings/logs_settings.dart new file mode 100644 index 0000000..c31304f --- /dev/null +++ b/lib/screens/settings/logs_settings/logs_settings.dart @@ -0,0 +1,246 @@ +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 createState() => _LogsSettingsState(); +} + +class _LogsSettingsState extends State { + final Uuid uuid = const Uuid(); + + bool generalSwitch = false; + bool anonymizeClientIp = false; + String? retentionTime; + List _ignoredDomainsControllers = []; + final _customTimeController = TextEditingController(); + String? _customTimeError = null; + + List retentionItems = [ + "custom", + "21600000", + "86400000", + "604800000", + "2592000000", + "7776000000" + ]; + + LoadStatus loadStatus = LoadStatus.loading; + + void loadData() async { + final serversProvider = Provider.of(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 = retentionItems.contains(data.interval.toString()) ? data.interval.toString() : "custom"; + if (data.interval != null && !retentionItems.contains(data.interval.toString())) { + _customTimeController.text = (data.interval!/3.6e+6).toInt().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); + } + } + + void validateCustomTime(String value) { + try { + final regex = RegExp(r'^\d+$'); + final parsed = int.parse(value); + if (!regex.hasMatch(value)) { + 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); + } + } + + bool checkValidValues() { + if (_ignoredDomainsControllers.where((d) => d.controller.text == "" || d.error == true).isNotEmpty) { + return false; + } + if (retentionTime == "custom" && (_customTimeError != null || _customTimeController.text == "")) { + return false; + } + return true; + } + + @override + void initState() { + loadData(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final serversProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + + final validValues = checkValidValues(); + + void clearQueries() async { + ProcessModal processModal = ProcessModal(); + processModal.open(AppLocalizations.of(context)!.updatingSettings); + + final result = await serversProvider.apiClient2!.clearLogs(); + + processModal.close(); + + if (!context.mounted) return; + + if (result.successful == true) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.logsCleared, + color: Colors.green + ); + } + else { + showSnackbar( + 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 == "custom" ? int.parse(_customTimeController.text)*3.6e+6 : int.parse(retentionTime!) , + "anonymize_client_ip": anonymizeClientIp, + "ignored": _ignoredDomainsControllers.map((e) => e.controller.text).toList() + } + ); + + processModal.close(); + + if (!context.mounted) return; + + if (result.successful == true) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.logsConfigUpdated, + color: Colors.green + ); + } + else { + showSnackbar( + 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), + customTimeController: _customTimeController, + customTimeError: _customTimeError, + validateCustomTime: validateCustomTime, + ); + + case LoadStatus.error: + return ErrorLoadData(text: AppLocalizations.of(context)!.logSettingsNotLoaded,); + + default: + return const SizedBox(); + } + }, + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/settings/safe_search_settings.dart b/lib/screens/settings/safe_search_settings.dart new file mode 100644 index 0000000..c9d5eef --- /dev/null +++ b/lib/screens/settings/safe_search_settings.dart @@ -0,0 +1,291 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:adguard_home_manager/functions/desktop_mode.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/widgets/custom_checkbox_list_tile.dart'; + +import 'package:adguard_home_manager/classes/process_modal.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/providers/status_provider.dart'; +import 'package:adguard_home_manager/functions/snackbar.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; + +class SafeSearchSettingsScreen extends StatefulWidget { + const SafeSearchSettingsScreen({super.key}); + + @override + State createState() => _SafeSearchSettingsScreenState(); +} + +class _SafeSearchSettingsScreenState extends State { + bool generalEnabled = false; + bool bingEnabled = false; + bool duckduckgoEnabled = false; + bool googleEnabled = false; + bool pixabayEnabled = false; + bool yandexEnabled = false; + bool youtubeEnabled = false; + + Future requestSafeSearchSettings() async { + final result = await Provider.of(context, listen: false).getServerStatus(); + if (mounted && result == true) { + final statusProvider = Provider.of(context, listen: false); + if (statusProvider.serverStatus != null) { + setState(() { + generalEnabled = statusProvider.serverStatus!.safeSearchEnabled; + bingEnabled = statusProvider.serverStatus!.safeSeachBing ?? false; + duckduckgoEnabled = statusProvider.serverStatus!.safeSearchDuckduckgo ?? false; + googleEnabled = statusProvider.serverStatus!.safeSearchGoogle ?? false; + pixabayEnabled = statusProvider.serverStatus!.safeSearchPixabay ?? false; + yandexEnabled = statusProvider.serverStatus!.safeSearchYandex ?? false; + youtubeEnabled = statusProvider.serverStatus!.safeSearchYoutube ?? false; + }); + } + } + } + + @override + void initState() { + final statusProvider = Provider.of(context, listen: false); + + if (statusProvider.loadStatus == LoadStatus.loading) { + requestSafeSearchSettings(); + } + else if (statusProvider.loadStatus == LoadStatus.loaded) { + generalEnabled = statusProvider.serverStatus!.safeSearchEnabled; + bingEnabled = statusProvider.serverStatus!.safeSeachBing!; + duckduckgoEnabled = statusProvider.serverStatus!.safeSearchDuckduckgo!; + googleEnabled = statusProvider.serverStatus!.safeSearchGoogle!; + pixabayEnabled = statusProvider.serverStatus!.safeSearchPixabay!; + yandexEnabled = statusProvider.serverStatus!.safeSearchYandex!; + youtubeEnabled = statusProvider.serverStatus!.safeSearchYoutube!; + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + final statusProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + + final width = MediaQuery.of(context).size.width; + + void saveConfig() async { + ProcessModal processModal = ProcessModal(); + processModal.open(AppLocalizations.of(context)!.savingSettings); + + final result = await statusProvider.updateSafeSearchConfig({ + "enabled": generalEnabled, + "bing": bingEnabled, + "duckduckgo": duckduckgoEnabled, + "google": googleEnabled, + "pixabay": pixabayEnabled, + "yandex": yandexEnabled, + "youtube": youtubeEnabled + }); + + processModal.close(); + + if (result == true) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.settingsUpdatedSuccessfully, + color: Colors.green, + labelColor: Colors.white + ); + } + else { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.settingsNotSaved, + color: Colors.red, + labelColor: Colors.white + ); + } + } + + return Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context)!.safeSearchSettings), + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, + centerTitle: false, + actions: [ + IconButton( + onPressed: statusProvider.loadStatus == LoadStatus.loaded + ? () => saveConfig() + : null, + icon: const Icon(Icons.save_rounded), + tooltip: AppLocalizations.of(context)!.save, + ), + const SizedBox(width: 8) + ], + ), + body: SafeArea( + child: Builder( + builder: (context) { + switch (statusProvider.loadStatus) { + case LoadStatus.loading: + return SizedBox( + width: double.maxFinite, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.loadingSafeSearchSettings, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ); + + case LoadStatus.loaded: + return RefreshIndicator( + onRefresh: requestSafeSearchSettings, + child: ListView( + children: [ + Padding( + padding: const EdgeInsets.only( + top: 16, + left: 16, + right: 16, + bottom: 8 + ), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(28), + child: InkWell( + borderRadius: BorderRadius.circular(28), + onTap: () => setState(() => generalEnabled = !generalEnabled), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12 + ), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(28) + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + AppLocalizations.of(context)!.enableSafeSearch, + style: const TextStyle( + fontSize: 18 + ), + ), + ), + Switch( + value: generalEnabled, + onChanged: (value) => setState(() => generalEnabled = value) + ) + ], + ), + ), + ), + ), + ), + CustomCheckboxListTile( + value: bingEnabled, + onChanged: (value) => setState(() => bingEnabled = value), + title: "Bing", + padding: const EdgeInsets.only( + top: 8, left: 40, right: 40, bottom: 8 + ), + disabled: !generalEnabled, + ), + CustomCheckboxListTile( + value: duckduckgoEnabled, + onChanged: (value) => setState(() => duckduckgoEnabled = value), + title: "DuckDuckGo", + padding: const EdgeInsets.only( + top: 8, left: 40, right: 40, bottom: 8 + ), + disabled: !generalEnabled, + ), + CustomCheckboxListTile( + value: googleEnabled, + onChanged: (value) => setState(() => googleEnabled = value), + title: "Google", + padding: const EdgeInsets.only( + top: 8, left: 40, right: 40, bottom: 8 + ), + disabled: !generalEnabled, + ), + CustomCheckboxListTile( + value: pixabayEnabled, + onChanged: (value) => setState(() => pixabayEnabled = value), + title: "Pixabay", + padding: const EdgeInsets.only( + top: 8, left: 40, right: 40, bottom: 8 + ), + disabled: !generalEnabled, + ), + CustomCheckboxListTile( + value: yandexEnabled, + onChanged: (value) => setState(() => yandexEnabled = value), + title: "Yandex", + padding: const EdgeInsets.only( + top: 8, left: 40, right: 40, bottom: 8 + ), + disabled: !generalEnabled, + ), + CustomCheckboxListTile( + value: youtubeEnabled, + onChanged: (value) => setState(() => youtubeEnabled = value), + title: "YouTube", + padding: const EdgeInsets.only( + top: 8, left: 40, right: 40, bottom: 8 + ), + disabled: !generalEnabled, + ), + ], + ), + ); + + case LoadStatus.error: + return SizedBox( + width: double.maxFinite, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon( + Icons.error, + color: Colors.red, + size: 50, + ), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.safeSearchSettingsNotLoaded, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ); + + + default: + return const SizedBox(); + } + }, + ), + ) + ); + } +} \ No newline at end of file diff --git a/lib/screens/settings/server_info/dns_addresses_modal.dart b/lib/screens/settings/server_info/dns_addresses_modal.dart index 2a6fa2f..e5fb87a 100644 --- a/lib/screens/settings/server_info/dns_addresses_modal.dart +++ b/lib/screens/settings/server_info/dns_addresses_modal.dart @@ -1,54 +1,71 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; +import 'package:adguard_home_manager/widgets/list_bottom_sheet.dart'; + class DnsAddressesModal extends StatelessWidget { + final bool isDialog; final List dnsAddresses; const DnsAddressesModal({ - Key? key, + super.key, + required this.isDialog, required this.dnsAddresses, - }) : super(key: key); + }); @override Widget build(BuildContext context) { - return AlertDialog( - title: Column( - children: [ - Icon( - Icons.route_rounded, - color: Theme.of(context).listTileTheme.iconColor - ), - const SizedBox(height: 16), - Text( - AppLocalizations.of(context)!.dnsAddresses, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface + if (isDialog == true) { + return AlertDialog( + title: Column( + children: [ + Icon( + Icons.route_rounded, + color: Theme.of(context).listTileTheme.iconColor ), + const SizedBox(height: 16), + Text( + AppLocalizations.of(context)!.dnsAddresses, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface + ), + ) + ], + ), + content: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 500), + child: SingleChildScrollView( + child: Wrap( + children: dnsAddresses.map((address) => ListTile( + title: Text( + address, + style: TextStyle( + fontWeight: FontWeight.normal, + color: Theme.of(context).listTileTheme.textColor + ), + ), + )).toList(), + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.close) ) ], - ), - content: SizedBox( - height: dnsAddresses.length*56 < 500 - ? dnsAddresses.length*56 : 500, - width: double.minPositive, - child: ListView( - children: dnsAddresses.map((address) => ListTile( - title: Text( - address, - style: TextStyle( - fontWeight: FontWeight.normal, - color: Theme.of(context).listTileTheme.textColor - ), - ), - )).toList(), - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(AppLocalizations.of(context)!.close) - ) - ], - ); + ); + } + else { + return ListBottomSheet( + icon: Icons.route_rounded, + title: AppLocalizations.of(context)!.dnsAddresses, + children: dnsAddresses.map((address) => CustomListTile( + title: address, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + )).toList(), + ); + } } } \ No newline at end of file diff --git a/lib/screens/settings/server_info/server_info.dart b/lib/screens/settings/server_info/server_info.dart index 35abb52..8f5c927 100644 --- a/lib/screens/settings/server_info/server_info.dart +++ b/lib/screens/settings/server_info/server_info.dart @@ -1,4 +1,3 @@ -import 'package:animations/animations.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -6,56 +5,32 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/screens/settings/server_info/dns_addresses_modal.dart'; import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; -import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/functions/desktop_mode.dart'; import 'package:adguard_home_manager/models/server_info.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart'; -class ServerInformation extends StatelessWidget { - const ServerInformation({Key? key}) : super(key: key); +class ServerInformation extends StatefulWidget { + const ServerInformation({super.key}); @override - Widget build(BuildContext context) { - final serversProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - - return ServerInformationWidget( - serversProvider: serversProvider, - appConfigProvider: appConfigProvider, - ); - } + State createState() => _ServerInformationState(); } -class ServerInformationWidget extends StatefulWidget { - final ServersProvider serversProvider; - final AppConfigProvider appConfigProvider; - - const ServerInformationWidget({ - Key? key, - required this.serversProvider, - required this.appConfigProvider, - }) : super(key: key); - - @override - State createState() => _ServerInformationWidgetState(); -} - -class _ServerInformationWidgetState extends State { - ServerInfo serverInfo = ServerInfo(loadStatus: 0); +class _ServerInformationState extends State { + ServerInfo serverInfo = ServerInfo(loadStatus: LoadStatus.loading); void fetchServerInfo() async { - final result = await getServerInfo(server: widget.serversProvider.selectedServer!); - if (mounted) { - if (result['result'] == 'success') { - setState(() { - serverInfo.loadStatus = 1; - serverInfo.data = result['data']; - }); - } - else { - widget.appConfigProvider.addLog(result['log']); - setState(() => serverInfo.loadStatus = 2); - } + final result = await Provider.of(context, listen: false).apiClient2!.getServerInfo(); + if (!mounted) return; + if (result.successful == true) { + setState(() { + serverInfo.data = result.content as ServerInfoData; + serverInfo.loadStatus = LoadStatus.loaded; + }); + } + else { + setState(() => serverInfo.loadStatus = LoadStatus.error); } } @@ -67,120 +42,140 @@ class _ServerInformationWidgetState extends State { @override Widget build(BuildContext context) { - Widget generateBody() { - switch (serverInfo.loadStatus) { - case 0: - return SizedBox( - width: double.maxFinite, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const CircularProgressIndicator(), - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Text( - AppLocalizations.of(context)!.loadingServerInfo, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ) - ], - ), - ); - - case 1: - return ListView( - children: [ - CustomListTile( - title: AppLocalizations.of(context)!.dnsAddresses, - subtitle: AppLocalizations.of(context)!.seeDnsAddresses, - onTap: () { - showModal( - context: context, - builder: (context) => DnsAddressesModal( - dnsAddresses: serverInfo.data!.dnsAddresses - ) - ); - }, - ), - CustomListTile( - title: AppLocalizations.of(context)!.dnsPort, - subtitle: serverInfo.data!.dnsPort.toString(), - ), - CustomListTile( - title: AppLocalizations.of(context)!.httpPort, - subtitle: serverInfo.data!.httpPort.toString(), - ), - CustomListTile( - title: AppLocalizations.of(context)!.protectionEnabled, - subtitle: serverInfo.data!.protectionEnabled == true - ? AppLocalizations.of(context)!.yes - : AppLocalizations.of(context)!.no, - ), - CustomListTile( - title: AppLocalizations.of(context)!.dhcpAvailable, - subtitle: serverInfo.data!.dhcpAvailable == true - ? AppLocalizations.of(context)!.yes - : AppLocalizations.of(context)!.no, - ), - CustomListTile( - title: AppLocalizations.of(context)!.serverRunning, - subtitle: serverInfo.data!.running == true - ? AppLocalizations.of(context)!.yes - : AppLocalizations.of(context)!.no, - ), - CustomListTile( - title: AppLocalizations.of(context)!.serverVersion, - subtitle: serverInfo.data!.version, - ), - CustomListTile( - title: AppLocalizations.of(context)!.serverLanguage, - subtitle: serverInfo.data!.language, - ), - ] - ); - - case 2: - return SizedBox( - width: double.maxFinite, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon( - Icons.error, - color: Colors.red, - size: 50, - ), - const SizedBox(height: 30), - Text( - AppLocalizations.of(context)!.serverInfoNotLoaded, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ) - ], - ), - ); - - default: - return const SizedBox(); - } - } - + final width = MediaQuery.of(context).size.width; return Scaffold( appBar: AppBar( title: Text(AppLocalizations.of(context)!.serverInformation), + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, + centerTitle: false, ), - body: generateBody() + body: SafeArea( + child: Builder( + builder: (context) { + switch (serverInfo.loadStatus) { + case LoadStatus.loading: + return SizedBox( + width: double.maxFinite, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 30), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + AppLocalizations.of(context)!.loadingServerInfo, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ) + ], + ), + ); + + case LoadStatus.loaded: + return ListView( + children: [ + CustomListTile( + title: AppLocalizations.of(context)!.dnsAddresses, + subtitle: AppLocalizations.of(context)!.seeDnsAddresses, + onTap: () { + if (width > 700) { + showDialog( + context: context, + builder: (context) => DnsAddressesModal( + dnsAddresses: serverInfo.data!.dnsAddresses, + isDialog: true, + ) + ); + } + else { + showModalBottomSheet( + context: context, + builder: (context) => DnsAddressesModal( + dnsAddresses: serverInfo.data!.dnsAddresses, + isDialog: false, + ), + isScrollControlled: true, + backgroundColor: Colors.transparent, + useSafeArea: true + ); + } + }, + ), + CustomListTile( + title: AppLocalizations.of(context)!.dnsPort, + subtitle: serverInfo.data!.dnsPort.toString(), + ), + CustomListTile( + title: AppLocalizations.of(context)!.httpPort, + subtitle: serverInfo.data!.httpPort.toString(), + ), + CustomListTile( + title: AppLocalizations.of(context)!.protectionEnabled, + subtitle: serverInfo.data!.protectionEnabled == true + ? AppLocalizations.of(context)!.yes + : AppLocalizations.of(context)!.no, + ), + CustomListTile( + title: AppLocalizations.of(context)!.dhcpAvailable, + subtitle: serverInfo.data!.dhcpAvailable == true + ? AppLocalizations.of(context)!.yes + : AppLocalizations.of(context)!.no, + ), + CustomListTile( + title: AppLocalizations.of(context)!.serverRunning, + subtitle: serverInfo.data!.running == true + ? AppLocalizations.of(context)!.yes + : AppLocalizations.of(context)!.no, + ), + CustomListTile( + title: AppLocalizations.of(context)!.serverVersion, + subtitle: serverInfo.data!.version, + ), + if (serverInfo.data!.language != "") CustomListTile( + title: AppLocalizations.of(context)!.serverLanguage, + subtitle: serverInfo.data!.language, + ), + ] + ); + + case LoadStatus.error: + return SizedBox( + width: double.maxFinite, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon( + Icons.error, + color: Colors.red, + size: 50, + ), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.serverInfoNotLoaded, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ); + + default: + return const SizedBox(); + } + }, + ), + ) ); } } \ No newline at end of file diff --git a/lib/screens/settings/settings.dart b/lib/screens/settings/settings.dart index 0031768..6c388cf 100644 --- a/lib/screens/settings/settings.dart +++ b/lib/screens/settings/settings.dart @@ -1,222 +1,354 @@ + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:flutter_web_browser/flutter_web_browser.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/screens/settings/server_info/server_info.dart'; import 'package:adguard_home_manager/screens/settings/encryption/encryption.dart'; +import 'package:adguard_home_manager/screens/settings/logs_settings/logs_settings.dart'; import 'package:adguard_home_manager/screens/settings/access_settings/access_settings.dart'; import 'package:adguard_home_manager/screens/settings/customization/customization.dart'; import 'package:adguard_home_manager/screens/settings/dhcp/dhcp.dart'; -import 'package:adguard_home_manager/widgets/section_label.dart'; +import 'package:adguard_home_manager/screens/settings/statistics_settings/statistics_settings.dart'; +import 'package:adguard_home_manager/screens/settings/safe_search_settings.dart'; +import 'package:adguard_home_manager/screens/settings/update_server/update_screen.dart'; import 'package:adguard_home_manager/screens/settings/dns/dns.dart'; import 'package:adguard_home_manager/screens/settings/dns_rewrites/dns_rewrites.dart'; -import 'package:adguard_home_manager/screens/settings/appbar.dart'; import 'package:adguard_home_manager/screens/servers/servers.dart'; import 'package:adguard_home_manager/screens/settings/advanced_setings.dart'; -import 'package:adguard_home_manager/screens/settings/general_settings.dart'; +import 'package:adguard_home_manager/screens/settings/general_settings/general_settings.dart'; +import 'package:adguard_home_manager/widgets/custom_settings_tile.dart'; +import 'package:adguard_home_manager/widgets/section_label.dart'; import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; -import 'package:adguard_home_manager/constants/strings.dart'; +import 'package:adguard_home_manager/functions/desktop_mode.dart'; +import 'package:adguard_home_manager/functions/open_url.dart'; import 'package:adguard_home_manager/constants/urls.dart'; +import 'package:adguard_home_manager/providers/status_provider.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; +final settingsNavigatorKey = GlobalKey(); + class Settings extends StatelessWidget { - const Settings({Key? key}) : super(key: key); + const Settings({super.key}); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth > 900) { + return Row( + children: [ + const Expanded( + flex: 1, + child: _SettingsWidget( + twoColumns: true, + ) + ), + Expanded( + flex: 2, + child: Navigator( + key: settingsNavigatorKey, + onGenerateRoute: (settings) => MaterialPageRoute(builder: (ctx) => const SizedBox()), + ), + ) + ], + ); + } + else { + return const _SettingsWidget( + twoColumns: false, + ); + } + }, + ); + } +} +class _SettingsWidget extends StatefulWidget { + final bool twoColumns; + + const _SettingsWidget({ + required this.twoColumns, + }); + + @override + State<_SettingsWidget> createState() => _SettingsWidgetState(); +} + +class _SettingsWidgetState extends State<_SettingsWidget> { + final _scaffoldMessengerKey = GlobalKey(); + + @override + void initState() { + Provider.of(context, listen: false).setSelectedSettingsScreen(screen: null); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final serversProvider = Provider.of(context); + final statusProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + + final width = MediaQuery.of(context).size.width; + + if (!widget.twoColumns && appConfigProvider.selectedSettingsScreen != null) { + appConfigProvider.setSelectedSettingsScreen(screen: null); + } + + return ScaffoldMessenger( + key: widget.twoColumns ? _scaffoldMessengerKey : null, + child: Scaffold( + body: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverAppBar.large( + pinned: true, + floating: true, + centerTitle: false, + forceElevated: innerBoxIsScrolled, + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, + title: Text(AppLocalizations.of(context)!.settings), + ) + ) + ], + body: SafeArea( + top: false, + bottom: false, + child: Builder( + builder: (context) => CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + ), + SliverList.list( + children: [ + if ( + serversProvider.selectedServer != null && + statusProvider.serverStatus != null && + serversProvider.apiClient2 != null + ) ...[ + SectionLabel(label: AppLocalizations.of(context)!.serverSettings), + _SettingsTile( + icon: Icons.search_rounded, + title: AppLocalizations.of(context)!.safeSearch, + subtitle: AppLocalizations.of(context)!.safeSearchSettings, + thisItem: 0, + screenToNavigate: const SafeSearchSettingsScreen(), + twoColumns: widget.twoColumns, + ), + _SettingsTile( + icon: Icons.list_alt_rounded, + title: AppLocalizations.of(context)!.logsSettings, + subtitle: AppLocalizations.of(context)!.logsSettingsDescription, + thisItem: 1, + screenToNavigate: const LogsSettings(), + twoColumns: widget.twoColumns, + ), + _SettingsTile( + icon: Icons.analytics_rounded, + title: AppLocalizations.of(context)!.statisticsSettings, + subtitle: AppLocalizations.of(context)!.statisticsSettingsDescription, + thisItem: 2, + screenToNavigate: const StatisticsSettings(), + twoColumns: widget.twoColumns, + ), + _SettingsTile( + icon: Icons.lock_rounded, + title: AppLocalizations.of(context)!.accessSettings, + subtitle: AppLocalizations.of(context)!.accessSettingsDescription, + thisItem: 3, + screenToNavigate: const AccessSettings(), + twoColumns: widget.twoColumns, + ), + _SettingsTile( + icon: Icons.install_desktop_rounded, + title: AppLocalizations.of(context)!.dhcpSettings, + subtitle: AppLocalizations.of(context)!.dhcpSettingsDescription, + thisItem: 4, + screenToNavigate: const DhcpScreen(), + twoColumns: widget.twoColumns, + ), + _SettingsTile( + icon: Icons.dns_rounded, + title: AppLocalizations.of(context)!.dnsSettings, + subtitle: AppLocalizations.of(context)!.dnsSettingsDescription, + thisItem: 5, + screenToNavigate: DnsSettings( + splitView: widget.twoColumns, + ), + twoColumns: widget.twoColumns, + ), + _SettingsTile( + icon: Icons.security_rounded, + title: AppLocalizations.of(context)!.encryptionSettings, + subtitle: AppLocalizations.of(context)!.encryptionSettingsDescription, + thisItem: 6, + screenToNavigate: const EncryptionSettings(), + twoColumns: widget.twoColumns, + ), + _SettingsTile( + icon: Icons.route_rounded, + title: AppLocalizations.of(context)!.dnsRewrites, + subtitle: AppLocalizations.of(context)!.dnsRewritesDescription, + thisItem: 7, + screenToNavigate: const DnsRewritesScreen(), + twoColumns: widget.twoColumns, + ), + if (serversProvider.updateAvailable.data != null) _SettingsTile( + icon: Icons.system_update_rounded, + title: AppLocalizations.of(context)!.updates, + subtitle: AppLocalizations.of(context)!.updatesDescription, + trailing: serversProvider.updateAvailable.data != null && + serversProvider.updateAvailable.data!.canAutoupdate == true + ? Container( + width: 10, + height: 10, + margin: const EdgeInsets.only(right: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Colors.red + ), + ) + : null, + thisItem: 8, + screenToNavigate: const UpdateScreen(), + twoColumns: widget.twoColumns, + ), + _SettingsTile( + icon: Icons.info_rounded, + title: AppLocalizations.of(context)!.serverInformation, + subtitle: AppLocalizations.of(context)!.serverInformationDescription, + thisItem: 9, + screenToNavigate: const ServerInformation(), + twoColumns: widget.twoColumns, + ), + ], + SectionLabel(label: AppLocalizations.of(context)!.appSettings), + _SettingsTile( + icon: Icons.palette_rounded, + title: AppLocalizations.of(context)!.customization, + subtitle: AppLocalizations.of(context)!.customizationDescription, + thisItem: 10, + screenToNavigate: const Customization(), + twoColumns: widget.twoColumns, + ), + _SettingsTile( + icon: Icons.storage_rounded, + title: AppLocalizations.of(context)!.servers, + subtitle: serversProvider.selectedServer != null + ? statusProvider.serverStatus != null + ? "${AppLocalizations.of(context)!.connectedTo} ${serversProvider.selectedServer!.name}" + : "${AppLocalizations.of(context)!.selectedServer} ${serversProvider.selectedServer!.name}" + : AppLocalizations.of(context)!.noServerSelected, + thisItem: 11, + screenToNavigate: const Servers(), + twoColumns: widget.twoColumns, + ), + _SettingsTile( + icon: Icons.settings, + title: AppLocalizations.of(context)!.generalSettings, + subtitle: AppLocalizations.of(context)!.generalSettingsDescription, + thisItem: 12, + screenToNavigate: GeneralSettings(splitView: widget.twoColumns), + twoColumns: widget.twoColumns, + ), + _SettingsTile( + icon: Icons.build_outlined, + title: AppLocalizations.of(context)!.advancedSettings, + subtitle: AppLocalizations.of(context)!.advancedSetupDescription, + thisItem: 13, + screenToNavigate: const AdvancedSettings(), + twoColumns: widget.twoColumns, + ), + SectionLabel(label: AppLocalizations.of(context)!.aboutApp), + CustomListTile( + title: AppLocalizations.of(context)!.appVersion, + subtitle: appConfigProvider.getAppInfo!.version, + ), + CustomListTile( + title: AppLocalizations.of(context)!.applicationDetails, + subtitle: AppLocalizations.of(context)!.applicationDetailsDescription, + trailing: Icon(Icons.open_in_new_rounded), + onTap: () => openUrl(Urls.appDetailsWebpage), + ), + CustomListTile( + title: AppLocalizations.of(context)!.myOtherApps, + subtitle: AppLocalizations.of(context)!.myOtherAppsDescription, + trailing: Icon(Icons.open_in_new_rounded), + onTap: () => openUrl(Urls.jgeek00AppsWebpage), + ), + SizedBox(height: 16) + ], + ) + ], + ) + ), + ), + ) + ), + ); + } +} + +class _SettingsTile extends StatelessWidget { + final String title; + final String subtitle; + final IconData icon; + final Widget? trailing; + final Widget screenToNavigate; + final int thisItem; + final bool twoColumns; + + const _SettingsTile({ + required this.title, + required this.subtitle, + required this.icon, + this.trailing, + required this.screenToNavigate, + required this.thisItem, + required this.twoColumns + }); @override Widget build(BuildContext context) { final appConfigProvider = Provider.of(context); - final serversProvider = Provider.of(context); - void navigateServers() { - Future.delayed(const Duration(milliseconds: 0), (() { - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => const Servers()) - ); - })); - } - - void openWeb(String url) { - FlutterWebBrowser.openWebPage( - url: url, - customTabsOptions: const CustomTabsOptions( - instantAppsEnabled: true, - showTitle: true, - urlBarHidingEnabled: false, - ), - safariVCOptions: const SafariViewControllerOptions( - barCollapsingEnabled: true, - dismissButtonStyle: SafariViewControllerDismissButtonStyle.close, - modalPresentationCapturesStatusBarAppearance: true, - ) + if (twoColumns) { + return CustomSettingsTile( + title: title, + subtitle: subtitle, + icon: icon, + trailing: trailing, + thisItem: thisItem, + selectedItem: appConfigProvider.selectedSettingsScreen, + onTap: () { + appConfigProvider.setSelectedSettingsScreen(screen: thisItem, notify: true); + Navigator.of(settingsNavigatorKey.currentContext!).pushReplacement( + PageRouteBuilder( + pageBuilder: (context, animation1, animation2) => screenToNavigate, + transitionDuration: Duration.zero, + reverseTransitionDuration: Duration.zero, + ), + ); + }, ); - } - - return Scaffold( - appBar: const SettingsAppBar(), - body: ListView( - children: [ - if (serversProvider.selectedServer != null) ...[ - SectionLabel(label: AppLocalizations.of(context)!.serverSettings), - CustomListTile( - icon: Icons.lock_rounded, - title: AppLocalizations.of(context)!.accessSettings, - subtitle: AppLocalizations.of(context)!.accessSettingsDescription, - onTap: () => { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const AccessSettings() - ) - ) - }, - ), - CustomListTile( - icon: Icons.install_desktop_rounded, - title: AppLocalizations.of(context)!.dhcpSettings, - subtitle: AppLocalizations.of(context)!.dhcpSettingsDescription, - onTap: () => { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const Dhcp() - ) - ) - }, - ), - CustomListTile( - icon: Icons.dns_rounded, - title: AppLocalizations.of(context)!.dnsSettings, - subtitle: AppLocalizations.of(context)!.dnsSettingsDescription, - onTap: () => { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const DnsSettings() - ) - ) - }, - ), - CustomListTile( - icon: Icons.security_rounded, - title: AppLocalizations.of(context)!.encryptionSettings, - subtitle: AppLocalizations.of(context)!.encryptionSettingsDescription, - onTap: () => { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const EncryptionSettings() - ) - ) - }, - ), - CustomListTile( - icon: Icons.route_rounded, - title: AppLocalizations.of(context)!.dnsRewrites, - subtitle: AppLocalizations.of(context)!.dnsRewritesDescription, - onTap: () => { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const DnsRewrites() - ) - ) - }, - ), - CustomListTile( - icon: Icons.info_rounded, - title: AppLocalizations.of(context)!.serverInformation, - subtitle: AppLocalizations.of(context)!.serverInformationDescription, - onTap: () => { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const ServerInformation() - ) - ) - }, - ), - ], - SectionLabel(label: AppLocalizations.of(context)!.appSettings), - CustomListTile( - icon: Icons.palette_rounded, - title: AppLocalizations.of(context)!.customization, - subtitle: AppLocalizations.of(context)!.customizationDescription, - onTap: () => Navigator.push(context, MaterialPageRoute( - builder: (context) => const Customization() - )) - ), - CustomListTile( - icon: Icons.storage_rounded, - title: AppLocalizations.of(context)!.servers, - subtitle: serversProvider.selectedServer != null - ? serversProvider.serverStatus.data != null - ? "${AppLocalizations.of(context)!.connectedTo} ${serversProvider.selectedServer!.name}" - : "${AppLocalizations.of(context)!.selectedServer} ${serversProvider.selectedServer!.name}" - : AppLocalizations.of(context)!.noServerSelected, - onTap: navigateServers, - ), - CustomListTile( - icon: Icons.settings, - title: AppLocalizations.of(context)!.generalSettings, - subtitle: AppLocalizations.of(context)!.generalSettingsDescription, - onTap: () => { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const GeneralSettings() - ) - ) - }, - ), - CustomListTile( - icon: Icons.build_outlined, - title: AppLocalizations.of(context)!.advancedSettings, - subtitle: AppLocalizations.of(context)!.advancedSetupDescription, - onTap: () => { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const AdvancedSettings() - ) - ) - }, - ), - SectionLabel(label: AppLocalizations.of(context)!.aboutApp), - CustomListTile( - title: AppLocalizations.of(context)!.appVersion, - subtitle: appConfigProvider.getAppInfo!.version, - ), - CustomListTile( - title: AppLocalizations.of(context)!.createdBy, - subtitle: Strings.createdBy, - ), - Padding( - padding: const EdgeInsets.all(15), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - IconButton( - onPressed: () => openWeb(Urls.playStore), - icon: SvgPicture.asset( - 'assets/resources/google-play.svg', - color: Theme.of(context).colorScheme.onSurfaceVariant, - width: 30, - height: 30, - ), - tooltip: AppLocalizations.of(context)!.visitGooglePlay, - ), - IconButton( - onPressed: () => openWeb(Urls.gitHub), - icon: SvgPicture.asset( - 'assets/resources/github.svg', - color: Theme.of(context).colorScheme.onSurfaceVariant, - width: 30, - height: 30, - ), - tooltip: AppLocalizations.of(context)!.gitHub, - ), - ], - ), - ) - ], - ), - ); + } + else { + return CustomListTile( + title: title, + subtitle: subtitle, + icon: icon, + trailing: trailing, + onTap: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => screenToNavigate) + ); + }, + ); + } } } \ No newline at end of file diff --git a/lib/screens/settings/statistics_settings/statistics_settings.dart b/lib/screens/settings/statistics_settings/statistics_settings.dart new file mode 100644 index 0000000..84439b1 --- /dev/null +++ b/lib/screens/settings/statistics_settings/statistics_settings.dart @@ -0,0 +1,338 @@ +import 'package:adguard_home_manager/constants/regexps.dart'; +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 createState() => _StatisticsSettingsState(); +} + +class _StatisticsSettingsState extends State { + final Uuid uuid = const Uuid(); + LoadStatus _loadStatus = LoadStatus.loading; + bool _generalSwitch = false; + final List _retentionItems = [ + "custom", + "86400000", + "604800000", + "2592000000", + "7776000000" + ]; + final _customTimeController = TextEditingController(); + String? _customTimeError; + String? _retentionTime; + List _ignoredDomainsControllers = []; + + void loadData() async { + final serversProvider = Provider.of(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(context); + final appConfigProvider = Provider.of(context); + + final List 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) { + bool error = false; + if (Regexps.domain.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 ?? _retentionItems[0]), + "ignored": _ignoredDomainsControllers.map((e) => e.controller.text).toList() + } + ); + + processModal.close(); + + if (!context.mounted) return; + + if (result.successful == true) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.statisticsConfigUpdated, + color: Colors.green + ); + } + else { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.statisticsConfigNotUpdated, + 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(); + } + }, + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/settings/theme_modal.dart b/lib/screens/settings/theme_modal.dart index 4d8f2fe..fac867e 100644 --- a/lib/screens/settings/theme_modal.dart +++ b/lib/screens/settings/theme_modal.dart @@ -14,10 +14,10 @@ class ThemeModal extends StatefulWidget { final int selectedTheme; const ThemeModal({ - Key? key, + super.key, required this.statusBarHeight, required this.selectedTheme, - }) : super(key: key); + }); @override State createState() => _ThemeModalState(); diff --git a/lib/screens/settings/update_server/autoupdate_unavailable.dart b/lib/screens/settings/update_server/autoupdate_unavailable.dart new file mode 100644 index 0000000..1ba4ae7 --- /dev/null +++ b/lib/screens/settings/update_server/autoupdate_unavailable.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class AutoUpdateUnavailableModal extends StatelessWidget { + const AutoUpdateUnavailableModal({super.key}); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Column( + children: [ + Icon( + Icons.error_rounded, + size: 24, + color: Theme.of(context).listTileTheme.iconColor + ), + const SizedBox(height: 16), + Text( + AppLocalizations.of(context)!.autoupdateUnavailable, + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface + ), + ) + ], + ), + content: Text( + AppLocalizations.of(context)!.autoupdateUnavailableDescription, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface + ), + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.close) + ), + ], + ) + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/settings/update_server/update.dart b/lib/screens/settings/update_server/update.dart new file mode 100644 index 0000000..de951b7 --- /dev/null +++ b/lib/screens/settings/update_server/update.dart @@ -0,0 +1,219 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:adguard_home_manager/screens/settings/update_server/autoupdate_unavailable.dart'; +import 'package:animations/animations.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:html/parser.dart' as html; +import 'package:markdown/markdown.dart' as md; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/providers/app_config_provider.dart'; +import 'package:adguard_home_manager/functions/snackbar.dart'; +import 'package:adguard_home_manager/classes/process_modal.dart'; +import 'package:adguard_home_manager/functions/open_url.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; + +class UpdateScreen extends StatelessWidget { + const UpdateScreen({super.key}); + + @override + Widget build(BuildContext context) { + final serversProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + + void showAutoUpdateUnavailableModal() { + showModal( + context: context, + builder: (context) => const AutoUpdateUnavailableModal() + ); + } + + void update() async { + ProcessModal processModal = ProcessModal(); + processModal.open(AppLocalizations.of(context)!.requestingUpdate); + + final result = await serversProvider.apiClient2!.requestUpdateServer(); + + processModal.close(); + + if (result.successful == true) { + serversProvider.recheckPeriodServerUpdated(); + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.requestStartUpdateSuccessful, + color: Colors.green, + labelColor: Colors.white, + ); + } + else { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.requestStartUpdateFailed, + color: Colors.red, + labelColor: Colors.white, + ); + } + } + + Widget headerPortrait() { + return Column( + children: [ + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (Navigator.canPop(context)) IconButton( + icon: Icon( + Icons.arrow_back, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + onPressed: () => Navigator.pop(context), + ), + if (!Navigator.canPop(context)) const SizedBox(), + IconButton( + icon: Icon( + Icons.refresh_rounded, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + tooltip: AppLocalizations.of(context)!.checkUpdates, + onPressed: () => serversProvider.checkServerUpdatesAvailable( + server: serversProvider.selectedServer!, + ) + ), + ], + ), + Padding( + padding: const EdgeInsets.only( + top: 8, bottom: 16, left: 16, right: 16 + ), + child: Column( + children: [ + serversProvider.updateAvailable.loadStatus == LoadStatus.loading + ? const Column( + children: [ + CircularProgressIndicator(), + SizedBox(height: 4) + ], + ) + : Icon( + serversProvider.updateAvailable.data!.canAutoupdate == true + ? Icons.system_update_rounded + : Icons.system_security_update_good_rounded, + size: 40, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(height: 16), + Text( + serversProvider.updateAvailable.loadStatus == LoadStatus.loading + ? AppLocalizations.of(context)!.checkingUpdates + : serversProvider.updateAvailable.data!.canAutoupdate == true + ? AppLocalizations.of(context)!.updateAvailable + : AppLocalizations.of(context)!.serverUpdated, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.w400 + ), + ), + const SizedBox(height: 40), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (serversProvider.updateAvailable.loadStatus == LoadStatus.loaded) Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + serversProvider.updateAvailable.data!.canAutoupdate == true + ? AppLocalizations.of(context)!.newVersion + : AppLocalizations.of(context)!.currentVersion, + style: const TextStyle( + fontSize: 16, + ), + ), + const SizedBox(height: 4), + Text( + serversProvider.updateAvailable.data!.canAutoupdate == true + ? serversProvider.updateAvailable.data!.newVersion ?? 'N/A' + : serversProvider.updateAvailable.data!.currentVersion, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ) + ], + ), + if (serversProvider.updateAvailable.loadStatus != LoadStatus.loaded) const SizedBox(), + FilledButton.icon( + icon: const Icon(Icons.download_rounded), + label: Text(AppLocalizations.of(context)!.updateNow), + onPressed: serversProvider.updatingServer == true + ? null + : serversProvider.updateAvailable.data!.canAutoupdate == true + ? serversProvider.updateAvailable.data!.canAutoupdate != null && serversProvider.updateAvailable.data!.canAutoupdate == true + ? () => update() + : () => showAutoUpdateUnavailableModal() + : null + ) + ], + ), + ], + ), + ), + ], + ); + } + + final SafeArea? changelog; + if (serversProvider.updateAvailable.loadStatus == LoadStatus.loaded && serversProvider.updateAvailable.data!.changelog != null) { + changelog = SafeArea( + child: ListView( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + "Changelog ${serversProvider.updateAvailable.data!.canAutoupdate == true + ? serversProvider.updateAvailable.data!.newVersion + : serversProvider.updateAvailable.data!.currentVersion}", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Html( + data: html.parse(md.markdownToHtml(serversProvider.updateAvailable.data!.changelog!)).outerHtml, + onLinkTap: (url, context, attributes) => url != null ? openUrl(url) : null, + ) + ) + ], + ), + ); + } else { + changelog = null; + } + + return Scaffold( + body: Column( + children: [ + Container( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + child: SafeArea( + child: headerPortrait() + ) + ), + const SizedBox(height: 16), + changelog != null + ? Expanded(child: changelog) + : const SizedBox(), + ] + ) + ); + } +} \ No newline at end of file diff --git a/lib/screens/settings/update_server/update_screen.dart b/lib/screens/settings/update_server/update_screen.dart new file mode 100644 index 0000000..ae894f8 --- /dev/null +++ b/lib/screens/settings/update_server/update_screen.dart @@ -0,0 +1,337 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:html/parser.dart' as html; +import 'package:markdown/markdown.dart' as md; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/classes/process_modal.dart'; +import 'package:adguard_home_manager/functions/snackbar.dart'; +import 'package:adguard_home_manager/functions/open_url.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; + +const _minExent = 64.0; +const _maxExent = 240.0; + +const _iconMaxBottomPositionExent = 130.0; +const _iconMinBottomPositionExent = 34.0; + +const _textMaxBottomPositionExent = 70.0; +const _textMinBottomPositionExent = 16.0; + +const _versionTextMaxBottomPositionExent = 30.0; +const _versionTextMinBottomPositionExent = 0.0; + +const _textMaxFontSize = 24.0; +const _textMinFontSize = 22.0; + +const _iconSafetyMargin = 15.0; + +const _iconSize = 45.0; + +class UpdateScreen extends StatefulWidget { + const UpdateScreen({super.key}); + + @override + State createState() => _UpdateScreenState(); +} + +class _UpdateScreenState extends State { + final _scrollController = ScrollController(); + bool _isScrolled = false; + + String? _htmlChangelog; + + void processChangelog() async { + final serversProvider = Provider.of(context, listen: false); + if (serversProvider.updateAvailable.data?.changelog == null) return; + final markdownResult = await compute(md.markdownToHtml, serversProvider.updateAvailable.data!.changelog!); + final htmlParsedResult = await compute(html.parse, markdownResult); + setState(() => _htmlChangelog = htmlParsedResult.outerHtml); + } + + @override + void initState() { + final serversProvider = Provider.of(context, listen: false); + _scrollController.addListener(() { + final newValue = _scrollController.offset > 20; + if (!( + serversProvider.updatingServer == false && + serversProvider.updateAvailable.data!.canAutoupdate != null && + serversProvider.updateAvailable.data!.canAutoupdate == true + )) return; + if (_isScrolled == newValue) return; + setState(() => _isScrolled = newValue); + }); + + processChangelog(); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + final serversProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + + void update() async { + ProcessModal processModal = ProcessModal(); + processModal.open(AppLocalizations.of(context)!.requestingUpdate); + + final result = await serversProvider.apiClient2!.requestUpdateServer(); + + processModal.close(); + + if (!context.mounted) return; + if (result.successful == true) { + serversProvider.recheckPeriodServerUpdated(); + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.requestStartUpdateSuccessful, + color: Colors.green, + labelColor: Colors.white, + ); + } + else { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.requestStartUpdateFailed, + color: Colors.red, + labelColor: Colors.white, + ); + } + } + + return Scaffold( + body: SafeArea( + top: false, + child: Stack( + children: [ + CustomScrollView( + controller: _scrollController, + slivers: [ + SliverPersistentHeader( + pinned: true, + delegate: _Header( + onRefresh: () => serversProvider.checkServerUpdatesAvailable( + server: serversProvider.selectedServer!, + ), + viewPaddingTop: MediaQuery.of(context).viewPadding.top + ) + ), + SliverList.list( + children: [ + const SizedBox(height: 16), + if (_htmlChangelog != null) Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + "Changelog ${serversProvider.updateAvailable.data!.canAutoupdate == true + ? serversProvider.updateAvailable.data!.newVersion + : serversProvider.updateAvailable.data!.currentVersion}", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ), + ), + if (_htmlChangelog != null) Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Html( + data: _htmlChangelog, + onLinkTap: (url, context, attributes) => url != null ? openUrl(url) : null, + ) + ), + if (_htmlChangelog == null) Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 32), + child: Column( + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.loadingChangelog, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 18, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ) + ], + ) + ), + ] + ) + ], + ), + if ( + serversProvider.updatingServer == false && + serversProvider.updateAvailable.data!.canAutoupdate != null && + serversProvider.updateAvailable.data!.canAutoupdate == true + ) AnimatedPositioned( + right: 20, + bottom: _isScrolled ? -70 : 20, + duration: const Duration(milliseconds: 200), + curve: Curves.ease, + child: FloatingActionButton( + onPressed: () => update(), + tooltip: AppLocalizations.of(context)!.updateNow, + child: const Icon(Icons.download_rounded), + ), + ) + ], + ), + ), + ); + } +} + +class _Header extends SliverPersistentHeaderDelegate { + final void Function() onRefresh; + final double viewPaddingTop; + + const _Header({ + required this.onRefresh, + required this.viewPaddingTop, + }); + + @override + Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { + final serversProvider = Provider.of(context); + + final iconMaxBottomPositionExent = _iconMaxBottomPositionExent + viewPaddingTop; + final iconMinBottomPositionExent = _iconMinBottomPositionExent + viewPaddingTop; + final textMaxBottomPositionExent = _textMaxBottomPositionExent + viewPaddingTop; + final textMinBottomPositionExent = _textMinBottomPositionExent + viewPaddingTop; + final versionTextMaxBottomPositionExent = _versionTextMaxBottomPositionExent + viewPaddingTop; + final versionTextMinBottomPositionExent = _versionTextMinBottomPositionExent + viewPaddingTop; + + final iconPercentage = shrinkOffset.clamp(0, _maxExent-_minExent-_iconSafetyMargin)/(_maxExent-_minExent-_iconSafetyMargin); + final textPercentage = shrinkOffset.clamp(0, _maxExent-_minExent)/(_maxExent-_minExent); + + final textFontSize = _textMinFontSize + (_textMaxFontSize-_textMinFontSize)*(1-textPercentage); + final mainText = _textMinBottomPositionExent + (textMaxBottomPositionExent-textMinBottomPositionExent)*(1-textPercentage); + final versionText = _versionTextMinBottomPositionExent + (versionTextMaxBottomPositionExent-versionTextMinBottomPositionExent)*(1-textPercentage); + + final iconBottom = _iconMinBottomPositionExent + (iconMaxBottomPositionExent-iconMinBottomPositionExent)*(1-iconPercentage); + + return LayoutBuilder( + builder: (context, constraints) => Stack( + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + ), + ), + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceTint.withOpacity(0.075), + ), + child: Align( + alignment: Alignment.topLeft, + child: SafeArea( + bottom: false, + child: Stack( + fit: StackFit.expand, + alignment: Alignment.center, + children: [ + if (Navigator.of(context).canPop()) Positioned( + top: 8, + left: 0, + child: BackButton( + onPressed: () => Navigator.pop(context), + ), + ), + Positioned( + top: 8, + right: 0, + child: IconButton( + onPressed: onRefresh, + icon: const Icon(Icons.refresh_rounded), + tooltip: AppLocalizations.of(context)!.refresh, + ) + ), + Positioned( + bottom: iconBottom, + left: (constraints.maxWidth/2)-(_iconSize/2), + child: Opacity( + opacity: 1-iconPercentage, + child: serversProvider.updateAvailable.loadStatus == LoadStatus.loading + ? const Column( + children: [ + CircularProgressIndicator(), + SizedBox(height: 4) + ], + ) + : Icon( + serversProvider.updateAvailable.data!.canAutoupdate == true + ? Icons.system_update_rounded + : Icons.system_security_update_good_rounded, + size: _iconSize, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + Positioned( + bottom: mainText, + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: constraints.maxWidth-100 + ), + child: Text( + serversProvider.updateAvailable.loadStatus == LoadStatus.loading + ? AppLocalizations.of(context)!.checkingUpdates + : serversProvider.updateAvailable.data!.canAutoupdate == true + ? AppLocalizations.of(context)!.updateAvailable + : AppLocalizations.of(context)!.serverUpdated, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: textFontSize, + fontWeight: FontWeight.w400 + ), + ), + ) + ), + Positioned( + bottom: versionText, + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: constraints.maxWidth-100 + ), + child: Opacity( + opacity: 1-iconPercentage, + child: Text( + serversProvider.updateAvailable.data!.canAutoupdate == true + ? "${AppLocalizations.of(context)!.newVersion}: ${serversProvider.updateAvailable.data!.newVersion ?? 'N/A'}" + : "${AppLocalizations.of(context)!.installedVersion}: ${serversProvider.updateAvailable.data!.currentVersion}", + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ), + ), + ) + ) + ], + ), + ), + ), + ), + ], + ), + ); + } + + @override + double get maxExtent => _maxExent + viewPaddingTop; + + @override + double get minExtent => _minExent + viewPaddingTop; + + @override + bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) => false; +} \ No newline at end of file diff --git a/lib/screens/top_items/top_items.dart b/lib/screens/top_items/top_items.dart deleted file mode 100644 index 1ddf272..0000000 --- a/lib/screens/top_items/top_items.dart +++ /dev/null @@ -1,301 +0,0 @@ -// ignore_for_file: use_build_context_synchronously - -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:percent_indicator/percent_indicator.dart'; -import 'package:provider/provider.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -import 'package:adguard_home_manager/screens/home/top_items_options_modal.dart'; -import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; - -import 'package:adguard_home_manager/models/applied_filters.dart'; -import 'package:adguard_home_manager/functions/copy_clipboard.dart'; -import 'package:adguard_home_manager/providers/logs_provider.dart'; -import 'package:adguard_home_manager/functions/snackbar.dart'; -import 'package:adguard_home_manager/functions/number_format.dart'; -import 'package:adguard_home_manager/functions/block_unblock_domain.dart'; -import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; - -class TopItemsScreen extends StatefulWidget { - final String type; - final String title; - final bool? isClient; - final List> data; - - const TopItemsScreen({ - Key? key, - required this.type, - required this.title, - this.isClient, - required this.data, - }) : super(key: key); - - @override - State createState() => _TopItemsScreenState(); -} - -class _TopItemsScreenState extends State { - bool searchActive = false; - final TextEditingController searchController = TextEditingController(); - - List> data = []; - List> screenData = []; - - void search(String value) { - List> newValues = widget.data.where((item) => item.keys.toList()[0].contains(value)).toList(); - setState(() => screenData = newValues); - } - - @override - void initState() { - data = widget.data; - screenData = widget.data; - super.initState(); - } - - @override - Widget build(BuildContext context) { - final serversProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - final logsProvider = Provider.of(context); - - int total = 0; - for (var element in data) { - total = total + int.parse(element.values.toList()[0].toString()); - } - - bool? getIsBlocked() { - if (widget.type == 'topBlockedDomains') { - return true; - } - else if (widget.type == 'topQueriedDomains') { - return false; - } - else { - return null; - } - } - - void changeBlockStatus(String status, String domain) async { - final result = await blockUnblock(context, domain, status); - showSnacbkar( - context: context, - appConfigProvider: appConfigProvider, - label: result['message'], - color: result['success'] == true ? Colors.green : Colors.red - ); - } - - void openOptionsModal(String domain, String type) { - showDialog( - context: context, - builder: (context) => TopItemsOptionsModal( - isBlocked: getIsBlocked(), - changeStatus: (String status) => changeBlockStatus(status, domain), - copyToClipboard: () => copyToClipboard( - context: context, - value: domain, - successMessage: AppLocalizations.of(context)!.domainCopiedClipboard - ), - type: type, - ) - ); - } - - return Scaffold( - appBar: AppBar( - title: searchActive == true - ? Padding( - padding: const EdgeInsets.only(bottom: 3), - child: TextFormField( - controller: searchController, - onChanged: search, - decoration: InputDecoration( - hintText: AppLocalizations.of(context)!.search, - hintStyle: const TextStyle( - fontWeight: FontWeight.normal, - fontSize: 18 - ), - border: InputBorder.none, - ), - style: const TextStyle( - fontWeight: FontWeight.normal, - fontSize: 18 - ), - ), - ) - : Text(widget.title), - leading: searchActive == true ? - IconButton( - onPressed: () => setState(() { - searchActive = false; - searchController.text = ''; - screenData = data; - }), - icon: const Icon(Icons.arrow_back), - tooltip: AppLocalizations.of(context)!.exitSearch, - ) : null, - actions: [ - if (searchActive == false) IconButton( - onPressed: () => setState(() => searchActive = true), - icon: const Icon(Icons.search), - tooltip: AppLocalizations.of(context)!.search, - ), - if (searchActive == true) IconButton( - onPressed: () => setState(() { - searchController.text = ''; - screenData = data; - }), - icon: const Icon(Icons.clear_rounded), - tooltip: AppLocalizations.of(context)!.clearSearch, - ), - const SizedBox(width: 10) - ], - bottom: PreferredSize( - preferredSize: const Size(double.maxFinite, 1), - child: Container( - width: double.maxFinite, - height: 1, - decoration: BoxDecoration( - color: searchActive == true - ? Colors.grey.withOpacity(0.5) - : Colors.transparent - ), - ), - ), - ), - body: RefreshIndicator( - onRefresh: () async { - final result = await getServerStatus(serversProvider.selectedServer!); - if (result['result'] == 'success') { - serversProvider.setServerStatusData(result['data']); - } - else { - appConfigProvider.addLog(result['log']); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.serverStatusNotRefreshed), - backgroundColor: Colors.red, - ) - ); - } - }, - child: screenData.isNotEmpty - ? ListView.builder( - itemCount: screenData.length, - itemBuilder: (context, index) { - String? name; - if (widget.isClient != null && widget.isClient == true) { - try { - name = serversProvider.serverStatus.data!.clients.firstWhere((c) => c.ids.contains(screenData[index].keys.toList()[0])).name; - } catch (e) { - // ---- // - } - } - - return CustomListTile( - onTap: () { - if (widget.type == 'topQueriedDomains' || widget.type == 'topBlockedDomains') { - logsProvider.setDomainText(screenData[index].keys.toList()[0]); - logsProvider.setSelectedClients(null); - logsProvider.setAppliedFilters( - AppliedFiters( - selectedResultStatus: 'all', - domainText: screenData[index].keys.toList()[0], - clients: null - ) - ); - appConfigProvider.setSelectedScreen(2); - Navigator.pop(context); - } - else if (widget.type == 'topClients') { - logsProvider.setDomainText(null); - logsProvider.setSelectedClients([screenData[index].keys.toList()[0]]); - logsProvider.setAppliedFilters( - AppliedFiters( - selectedResultStatus: 'all', - domainText: null, - clients: [screenData[index].keys.toList()[0]] - ) - ); - appConfigProvider.setSelectedScreen(2); - Navigator.pop(context); - } - }, - onLongPress: () => openOptionsModal( - screenData[index].keys.toList()[0], - widget.type - ), - title: screenData[index].keys.toList()[0], - trailing: Text( - screenData[index].values.toList()[0].toString(), - style: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant - ), - ), - subtitleWidget: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (name != null) ...[ - Text( - name, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.onSurface - ), - ), - const SizedBox(height: 5), - ], - Row( - children: [ - SizedBox( - width: 50, - child: Text( - "${doubleFormat((screenData[index].values.toList()[0]/total*100), Platform.localeName)}%", - style: TextStyle( - color: Theme.of(context).listTileTheme.textColor - ), - ), - ), - const SizedBox(width: 10), - Flexible( - child: LinearPercentIndicator( - animation: true, - lineHeight: 4, - animationDuration: 500, - curve: Curves.easeOut, - percent: screenData[index].values.toList()[0]/total, - barRadius: const Radius.circular(5), - progressColor: Theme.of(context).colorScheme.primary, - backgroundColor: Theme.of(context).colorScheme.surfaceVariant, - ), - ), - const SizedBox(width: 10), - ], - ), - ], - ) - ); - } - ) - : Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Text( - AppLocalizations.of(context)!.noItemsSearch, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ), - ) - ), - ); - } -} \ No newline at end of file diff --git a/lib/services/api_client.dart b/lib/services/api_client.dart new file mode 100644 index 0000000..ce3e033 --- /dev/null +++ b/lib/services/api_client.dart @@ -0,0 +1,918 @@ +import 'dart:convert'; + +import 'package:sentry_flutter/sentry_flutter.dart'; + +import 'package:adguard_home_manager/models/blocked_services.dart'; +import 'package:adguard_home_manager/models/querylog_config.dart'; +import 'package:adguard_home_manager/models/statistics_config.dart'; +import 'package:adguard_home_manager/models/dns_info.dart'; +import 'package:adguard_home_manager/models/encryption.dart'; +import 'package:adguard_home_manager/models/dhcp.dart'; +import 'package:adguard_home_manager/models/rewrite_rules.dart'; +import 'package:adguard_home_manager/models/filtering.dart'; +import 'package:adguard_home_manager/models/filtering_status.dart'; +import 'package:adguard_home_manager/models/server_info.dart'; +import 'package:adguard_home_manager/models/logs.dart'; +import 'package:adguard_home_manager/models/clients.dart'; +import 'package:adguard_home_manager/models/clients_allowed_blocked.dart'; +import 'package:adguard_home_manager/models/server.dart'; +import 'package:adguard_home_manager/models/server_status.dart'; +import 'package:adguard_home_manager/classes/http_client.dart'; + +class ApiResponse { + final bool successful; + final dynamic content; + final int? statusCode; + + const ApiResponse({ + required this.successful, + this.content, + this.statusCode, + }); +} + +class ApiClientV2 { + final Server server; + + ApiClientV2({ + required this.server + }); + + Future getServerVersion() async { + final result = await HttpRequestClient.get(urlPath: '/status', server: server); + if (result.successful == true) { + try { + return ApiResponse( + successful: true, + content: jsonDecode(result.body!)['version'] + ); + } on FormatException { + return const ApiResponse(successful: false); + } catch (e, stackTrace) { + Sentry.captureException(e, stackTrace: stackTrace); + return const ApiResponse(successful: false); + } + } + else { + return const ApiResponse(successful: false); + } + } + + Future getServerStatus() async { + final results = await Future.wait([ + HttpRequestClient.get(urlPath: "/stats", server: server), + HttpRequestClient.get(urlPath: "/status", server: server), + HttpRequestClient.get(urlPath: "/filtering/status", server: server), + HttpRequestClient.get(urlPath: "/safesearch/status", server: server), + HttpRequestClient.get(urlPath: "/safebrowsing/status", server: server), + HttpRequestClient.get(urlPath: "/parental/status", server: server), + HttpRequestClient.get(urlPath: "/clients", server: server), + ]); + if ( + results.map((e) => e.successful).every((e) => e == true) && + results.map((e) => e.body).every((e) => e != null) + ) { + try { + final Map mappedData = { + 'stats': jsonDecode(results[0].body!), + 'clients': jsonDecode(results[6].body!)['clients'], + 'status': jsonDecode(results[1].body!), + 'filtering': jsonDecode(results[2].body!), + 'safeSearch': jsonDecode(results[3].body!), + 'safeBrowsingEnabled': jsonDecode(results[4].body!), + 'parentalControlEnabled': jsonDecode(results[5].body!), + }; + return ApiResponse( + successful: true, + content: ServerStatus.fromJson(mappedData) + ); + } on FormatException { + return const ApiResponse(successful: false); + } catch (e, stackTrace) { + Sentry.captureException(e, stackTrace: stackTrace); + return const ApiResponse(successful: false); + } + } + else { + return const ApiResponse(successful: false); + } + } + + Future updateFiltering({ + required bool enable + }) async { + final result = await HttpRequestClient.post( + urlPath: "/filtering/config", + server: server, + body: { + 'enabled': enable + } + ); + return ApiResponse( + successful: result.successful, + ); + } + + Future updateSafeBrowsing({ + required bool enable + }) async { + final result = await HttpRequestClient.post( + urlPath: enable == true + ? "/safebrowsing/enable" + : "/safebrowsing/disable", + server: server, + ); + return ApiResponse( + successful: result.successful, + ); + } + + Future updateParentalControl({ + required bool enable + }) async { + final result = await HttpRequestClient.post( + urlPath: enable == true + ? "/parental/enable" + : "/parental/disable", + server: server, + ); + return ApiResponse( + successful: result.successful, + ); + } + + Future updateGeneralProtection({ + required bool enable, + int? time, + }) async { + final result = await HttpRequestClient.post( + urlPath: "/protection", + server: server, + body: { + 'enabled': enable, + 'duration': time + } + ); + return ApiResponse( + successful: result.successful, + ); + } + + Future getClients() async { + final results = await Future.wait([ + HttpRequestClient.get(urlPath: "/clients", server: server), + HttpRequestClient.get(urlPath: "/access/list", server: server), + ]); + if ( + results.map((e) => e.successful).every((e) => e == true) && + results.map((e) => e.body).every((e) => e != null) + ) { + try { + final clients = Clients.fromJson(jsonDecode(results[0].body!)); + clients.clientsAllowedBlocked = ClientsAllowedBlocked.fromJson(jsonDecode(results[1].body!)); + return ApiResponse( + successful: true, + content: clients + ); + } catch (e, stackTrace) { + Sentry.captureException( + e, + stackTrace: stackTrace, + hint: Hint.withMap({ "statusCode": results.map((e) => e.statusCode.toString()) }) + ); + return const ApiResponse(successful: false); + } + } + else { + return const ApiResponse(successful: false); + } + } + + Future requestAllowedBlockedClientsHosts({ + required Map?> body + }) async { + final result = await HttpRequestClient.post( + urlPath: "/access/set", + server: server, + body: body + ); + if (result.statusCode == 400) { + return const ApiResponse( + successful: false, + content: "client_another_list" + ); + } + return ApiResponse(successful: result.successful); + } + + Future getLogs({ + int? count, + int? offset, + DateTime? olderThan, + String? responseStatus, + String? search + }) async { + final result = await HttpRequestClient.get( + urlPath: '/querylog?${count != null ? 'limit=$count' : ''}${offset != null ? '&offset=$offset' : ''}${olderThan != null ? '&older_than=${olderThan.toIso8601String()}' : ''}${responseStatus != null ? '&response_status=$responseStatus' : ''}${search != null ? '&search=$search' : ''}', + server: server + ); + if (result.successful == true) { + try { + return ApiResponse( + successful: true, + content: LogsData.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 getFilteringRules() async { + final result = await HttpRequestClient.get(urlPath: '/filtering/status', server: server); + if (result.successful == true) { + try { + return ApiResponse( + successful: true, + content: FilteringStatus.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 postFilteringRules({ + required Map> data, + }) async { + final result = await HttpRequestClient.post( + urlPath: '/filtering/set_rules', + server: server, + body: data + ); + return ApiResponse(successful: result.successful); + } + + Future postAddClient({ + required Map data, + }) async { + final result = await HttpRequestClient.post( + urlPath: '/clients/add', + server: server, + body: data + ); + return ApiResponse(successful: result.successful); + } + + Future postUpdateClient({ + required Map data, + }) async { + final result = await HttpRequestClient.post( + urlPath: '/clients/update', + server: server, + body: data + ); + return ApiResponse(successful: result.successful); + } + + Future postDeleteClient({ + required String name, + }) async { + final result = await HttpRequestClient.post( + urlPath: '/clients/delete', + server: server, + body: {'name': name}, + ); + return ApiResponse(successful: result.successful); + } + + Future getFiltering() async { + final results = await Future.wait([ + HttpRequestClient.get(urlPath: '/filtering/status', server: server), + HttpRequestClient.get(urlPath: '/blocked_services/list', server: server), + ]); + if (results[0].successful == true && results[0].body != null) { + try { + return ApiResponse( + successful: true, + content: Filtering.fromJson({ + ...jsonDecode(results[0].body!), + "blocked_services": results[1].body != null + ? jsonDecode(results[1].body!) + : [] + }) + ); + } catch (e, stackTrace) { + Sentry.captureException( + e, + stackTrace: stackTrace, + hint: Hint.withMap({ "statusCode": results.map((e) => e.statusCode.toString()) }) + ); + return const ApiResponse(successful: false); + } + } + else { + return const ApiResponse(successful: false); + } + } + + Future setCustomRules({ + required List rules, + }) async { + final result = await HttpRequestClient.post( + urlPath: '/filtering/set_rules', + server: server, + body: {'rules': rules}, + ); + return ApiResponse(successful: result.successful); + } + + Future addFilteringList({ + required Map data, + }) async { + final result = await HttpRequestClient.post( + urlPath: '/filtering/add_url', + server: server, + body: data, + ); + return ApiResponse( + successful: result.successful, + content: result.body, + statusCode: result.statusCode + ); + } + + Future updateFilterList({ + required Map data, + }) async { + final result = await HttpRequestClient.post( + urlPath: '/filtering/set_url', + server: server, + body: data, + ); + return ApiResponse(successful: result.successful); + } + + Future deleteFilterList({ + required Map data, + }) async { + final result = await HttpRequestClient.post( + urlPath: '/filtering/remove_url', + server: server, + body: data, + ); + return ApiResponse(successful: result.successful); + } + + Future getServerInfo() async { + final result = await HttpRequestClient.get(urlPath: "/status", server: server); + if (result.successful) { + try { + return ApiResponse( + successful: true, + content: ServerInfoData.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 updateLists() async { + final results = await Future.wait([ + HttpRequestClient.post( + urlPath: '/filtering/refresh', + server: server, + body: {'whitelist': true}, + ), + HttpRequestClient.post( + urlPath: '/filtering/refresh', + server: server, + body: {'whitelist': false}, + ), + ]); + if ( + results.map((e) => e.successful).every((e) => e == true) && + results.map((e) => e.body).every((e) => e != null) + ) { + try { + final clients = Clients.fromJson(jsonDecode(results[0].body!)); + clients.clientsAllowedBlocked = ClientsAllowedBlocked.fromJson(jsonDecode(results[1].body!)); + return ApiResponse( + successful: true, + content: {'updated': jsonDecode(results[0].body!)['updated']+jsonDecode(results[1].body!)['updated']} + ); + } catch (e, stackTrace) { + Sentry.captureException( + e, + stackTrace: stackTrace, + hint: Hint.withMap({ "statusCode": results.map((e) => e.statusCode.toString()) }) + ); + return const ApiResponse(successful: false); + } + } + else { + return const ApiResponse(successful: false); + } + } + + Future checkHostFiltered({ + required String host + }) async { + final result = await HttpRequestClient.get(urlPath: '/filtering/check_host?name=$host', server: server); + if (result.successful) { + return ApiResponse( + successful: true, + content: jsonDecode(result.body!) + ); + } + else { + return const ApiResponse(successful: false); + } + } + + Future requestChangeUpdateFrequency({ + required Map data, + }) async { + final result = await HttpRequestClient.post( + urlPath: '/filtering/config', + server: server, + body: data, + ); + return ApiResponse(successful: result.successful); + } + + Future setBlockedServices({ + required List data, + }) async { + final result = await HttpRequestClient.post( + urlPath: '/blocked_services/set', + server: server, + body: data, + ); + return ApiResponse(successful: result.successful); + } + + Future getDhcpData() async { + final results = await Future.wait([ + HttpRequestClient.get(urlPath: '/dhcp/interfaces', server: server), + HttpRequestClient.get(urlPath: '/dhcp/status', server: server), + ]); + if ( + results.map((e) => e.successful).every((e) => e == true) && + results.map((e) => e.body).every((e) => e != null) + ) { + try { + List interfaces = List.from(jsonDecode(results[0].body!).entries.map((entry) => NetworkInterface.fromJson(entry.value))); + return ApiResponse( + successful: true, + content: DhcpModel( + networkInterfaces: interfaces, + dhcpStatus: jsonDecode(results[1].body!)['message'] != null + ? null + : DhcpStatus.fromJson(jsonDecode(results[1].body!)) + ) + ); + } catch (e, stackTrace) { + Sentry.captureException( + e, + stackTrace: stackTrace, + hint: Hint.withMap({ "statusCode": results.map((e) => e.statusCode.toString()) }) + ); + return const ApiResponse(successful: false); + } + } + else { + return const ApiResponse(successful: false); + } + } + + Future saveDhcpConfig({ + required Map data, + }) async { + final result = await HttpRequestClient.post( + urlPath: '/dhcp/set_config', + server: server, + body: data, + ); + return ApiResponse(successful: result.successful); + } + + Future resetDhcpConfig() async { + final result = await HttpRequestClient.post( + urlPath: '/dhcp/reset', + server: server, + body: {}, + ); + return ApiResponse(successful: result.successful); + } + + Future deleteStaticLease({ + required Map data + }) async { + final result = await HttpRequestClient.post( + urlPath: '/dhcp/remove_static_lease', + server: server, + body: data, + ); + return ApiResponse(successful: result.successful); + } + + Future createStaticLease({ + required Map data + }) async { + final result = await HttpRequestClient.post( + urlPath: '/dhcp/add_static_lease', + server: server, + body: data, + ); + if (result.statusCode == 400 && result.body != null && result.body!.contains('static lease already exists')) { + return const ApiResponse( + successful: false, + content: "already_exists", + statusCode: 400 + ); + } + if (result.statusCode == 400 && result.body != null && result.body!.contains('server is unconfigured')) { + return const ApiResponse( + successful: false, + content: "server_not_configured", + statusCode: 400 + ); + } + return ApiResponse(successful: result.successful); + } + + Future restoreAllLeases() async { + final result = await HttpRequestClient.post( + urlPath: '/dhcp/reset_leases', + server: server, + body: {}, + ); + return ApiResponse(successful: result.successful); + } + + Future getDnsRewriteRules() async { + final result = await HttpRequestClient.get(urlPath: '/rewrite/list', server: server); + if (result.successful) { + try { + final List data = List.from( + jsonDecode(result.body!).map((item) => RewriteRules.fromJson(item)) + ); + return ApiResponse( + successful: true, + content: data + ); + } 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 deleteDnsRewriteRule({ + required Map data, + }) async { + final result = await HttpRequestClient.post( + urlPath: '/rewrite/delete', + server: server, + body: data, + ); + return ApiResponse(successful: result.successful); + } + + Future addDnsRewriteRule({ + required Map data, + }) async { + final result = await HttpRequestClient.post( + urlPath: '/rewrite/add', + server: server, + body: data, + ); + return ApiResponse(successful: result.successful); + } + + Future getQueryLogInfo() async { + final result = await HttpRequestClient.get(urlPath: '/querylog/config', server: server); + if (result.successful) { + try { + return ApiResponse( + successful: true, + content: QueryLogConfig.fromJson(jsonDecode(result.body!)) + ); + } catch (e, stackTrace) { + Sentry.captureException( + e, + stackTrace: stackTrace, + hint: Hint.withMap({ "statusCode": result.statusCode.toString() }) + ); + return const ApiResponse(successful: false); + } + } + else { + return const ApiResponse(successful: false); + } + } + + Future updateQueryLogParameters({ + required Map data, + }) async { + final result = await HttpRequestClient.put( + urlPath: '/querylog/config/update', + server: server, + body: data, + ); + return ApiResponse(successful: result.successful); + } + + Future clearLogs() async { + final result = await HttpRequestClient.post( + urlPath: '/querylog_clear', + server: server, + body: {}, + ); + return ApiResponse(successful: result.successful); + } + + Future getDnsInfo() async { + final result = await HttpRequestClient.get(urlPath: '/dns_info', server: server); + if (result.successful) { + try { + return ApiResponse( + successful: true, + content: DnsInfo.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 setDnsConfig({ + required Map data, + }) async { + final result = await HttpRequestClient.post( + urlPath: '/dns_config', + server: server, + body: data, + ); + if (result.statusCode == 400) { + return ApiResponse( + successful: result.successful, + content: "data_not_valid", + statusCode: result.statusCode + ); + } + return ApiResponse(successful: result.successful); + } + + Future getEncryptionSettings() async { + final result = await HttpRequestClient.get(urlPath: '/tls/status', server: server); + if (result.successful) { + try { + return ApiResponse( + successful: true, + content: EncryptionData.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 getBlockedServices() async { + final result = await HttpRequestClient.get(urlPath: '/blocked_services/all', server: server); + if (result.successful) { + try { + return ApiResponse( + successful: true, + content: List.from( + BlockedServicesFromApi.fromJson(jsonDecode(result.body!)).blockedServices + ) + ); + } 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 checkEncryptionSettings({ + required Map data, + }) async { + final result = await HttpRequestClient.post( + urlPath: '/tls/validate', + server: server, + body: data, + ); + try { + return ApiResponse( + successful: result.successful, + content: result.body != null ? EncryptionValidationResult( + isObject: true, + encryptionValidation: EncryptionValidation.fromJson(jsonDecode(result.body!)) + ) : null + ); + } on FormatException { + return ApiResponse( + successful: result.successful, + content: result.body != null ? EncryptionValidationResult( + isObject: false, + message: result.body + ) : null + ); + } catch (e, stackTrace) { + Sentry.captureException( + e, + stackTrace: stackTrace, + hint: Hint.withMap({ "statusCode": result.statusCode.toString() }) + ); + return const ApiResponse(successful: false); + } + } + + Future saveEncryptionSettings({ + required Map data, + }) async { + final result = await HttpRequestClient.post( + urlPath: '/tls/configure', + server: server, + body: data, + ); + return ApiResponse( + successful: result.successful, + content: result.body + ); + } + + Future resetDnsCache() async { + final result = await HttpRequestClient.post( + urlPath: '/cache_clear', + server: server, + ); + return ApiResponse(successful: result.successful); + } + + Future checkServerUpdates() async { + final results = await Future.wait([ + HttpRequestClient.post(urlPath: '/version.json', server: server, body: { "recheck_now": true }), + HttpRequestClient.get(urlPath: '/status', server: server), + ]); + if ( + results.map((e) => e.successful).every((e) => e == true) && + results.map((e) => e.body).every((e) => e != null) + ) { + try { + final Map obj = { + ...jsonDecode(results[0].body!), + 'current_version': ServerInfoData.fromJson(jsonDecode(results[1].body!)).version + }; + return ApiResponse( + successful: true, + content: obj + ); + } catch (e, stackTrace) { + Sentry.captureException( + e, + stackTrace: stackTrace, + hint: Hint.withMap({ "statusCode": results.map((e) => e.statusCode.toString()) }) + ); + return const ApiResponse(successful: false); + } + } + else { + return const ApiResponse(successful: false); + } + } + + Future requestUpdateServer() async { + final result = await HttpRequestClient.post( + urlPath: '/update', + server: server, + ); + return ApiResponse(successful: result.successful); + } + + Future updateSafeSearchSettings({ + required Map body + }) async { + final result = await HttpRequestClient.put( + urlPath: '/safesearch/settings', + server: server, + body: body + ); + return ApiResponse(successful: result.successful); + } + + Future updateRewriteRule({ + required Map body + }) async { + final result = await HttpRequestClient.put( + urlPath: '/rewrite/update', + server: server, + body: body + ); + return ApiResponse(successful: result.successful); + } + + Future testUpstreamDns({ + required Map body + }) async { + final result = await HttpRequestClient.post( + urlPath: '/test_upstream_dns', + server: server, + body: body + ); + return ApiResponse( + successful: result.successful, + content: result.body != null ? jsonDecode(result.body!) : null + ); + } + + Future 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 updateStatisticsSettings({ + required Map body + }) async { + final result = await HttpRequestClient.put( + urlPath: '/stats/config/update', + server: server, + body: body + ); + return ApiResponse(successful: result.successful); + } +} \ No newline at end of file diff --git a/lib/services/auth.dart b/lib/services/auth.dart new file mode 100644 index 0000000..68e2717 --- /dev/null +++ b/lib/services/auth.dart @@ -0,0 +1,86 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:sentry_flutter/sentry_flutter.dart'; + +import 'package:adguard_home_manager/classes/http_client.dart'; +import 'package:adguard_home_manager/models/server.dart'; + +enum AuthStatus { + success, + invalidCredentials, + manyAttepts, + serverError, + socketException, + timeoutException, + handshakeException, + unknown +} + +class ServerAuth { + static Future login(Server server) async { + try { + final body = { + "name": server.user, + "password": server.password + }; + final connectionString = "${server.connectionMethod}://${server.domain}${server.port != null ? ':${server.port}' : ""}${server.path ?? ""}/control/login"; + HttpClient httpClient = HttpClient(); + HttpClientRequest request = await httpClient.postUrl(Uri.parse(connectionString)); + request.headers.set('content-type', 'application/json'); + request.headers.contentLength = utf8.encode(jsonEncode(body)).length; + request.add(utf8.encode(json.encode(body))); + HttpClientResponse response = await request.close().timeout(const Duration(seconds: 10)); + httpClient.close(); + if (response.statusCode == 200) { + return AuthStatus.success; + } + else if (response.statusCode == 400 || response.statusCode == 401 || response.statusCode == 403) { + return AuthStatus.invalidCredentials; + } + else if (response.statusCode == 429) { + return AuthStatus.manyAttepts; + } + else if (response.statusCode == 500) { + return AuthStatus.serverError; + } + else { + return AuthStatus.unknown; + } + } on SocketException { + return AuthStatus.socketException; + } on TimeoutException { + return AuthStatus.timeoutException; + } on HandshakeException { + return AuthStatus.handshakeException; + } catch (e, stackTrace) { + Sentry.captureException(e, stackTrace: stackTrace); + return AuthStatus.unknown; + } + } + + static Future loginHA(Server server) async { + try { + final result = await HttpRequestClient.get(urlPath: "/status", server: server); + if (result.successful) { + return AuthStatus.success; + } + else if (result.statusCode == 401 || result.statusCode == 403) { + return AuthStatus.invalidCredentials; + } + else { + return AuthStatus.unknown; + } + } on SocketException { + return AuthStatus.socketException; + } on TimeoutException { + return AuthStatus.timeoutException; + } on HandshakeException { + return AuthStatus.handshakeException; + } catch (e, stackTrace) { + Sentry.captureException(e, stackTrace: stackTrace); + return AuthStatus.unknown; + } + } +} \ No newline at end of file diff --git a/lib/services/database.dart b/lib/services/database.dart deleted file mode 100644 index af9d8fb..0000000 --- a/lib/services/database.dart +++ /dev/null @@ -1,136 +0,0 @@ -import 'package:sqflite/sqflite.dart'; - -Future> loadDb(bool acceptsDynamicTheme) async { - List>? servers; - List>? appConfig; - - Future upgradeDbToV2(Database db) async { - await db.execute("ALTER TABLE appConfig ADD COLUMN overrideSslCheck NUMERIC"); - await db.execute("UPDATE appConfig SET overrideSslCheck = 0"); - - await db.transaction((txn) async{ - await txn.rawQuery( - 'SELECT * FROM appConfig', - ); - }); - } - - Future upgradeDbToV3(Database db) async { - await db.execute("ALTER TABLE appConfig ADD COLUMN hideZeroValues NUMERIC"); - await db.execute("UPDATE appConfig SET hideZeroValues = 0"); - - await db.transaction((txn) async{ - await txn.rawQuery( - 'SELECT * FROM appConfig', - ); - }); - } - - Future upgradeDbToV4(Database db) async { - await db.execute("ALTER TABLE servers ADD COLUMN runningOnHa INTEGER"); - await db.execute("UPDATE servers SET runningOnHa = 0"); - - await db.transaction((txn) async{ - await txn.rawQuery( - 'SELECT * FROM servers', - ); - }); - } - - Future upgradeDbToV5(Database db) async { - await db.execute("ALTER TABLE appConfig ADD COLUMN useDynamicColor NUMERIC"); - await db.execute("ALTER TABLE appConfig ADD COLUMN staticColor NUMERIC"); - await db.execute("ALTER TABLE appConfig ADD COLUMN useThemeColorForStatus NUMERIC"); - await db.execute("UPDATE appConfig SET useDynamicColor = ${acceptsDynamicTheme == true ? 1 : 0}, staticColor = 0, useThemeColorForStatus = 0"); - - await db.transaction((txn) async{ - await txn.rawQuery( - 'SELECT * FROM appConfig', - ); - }); - } - - Future upgradeDbToV6(Database db) async { - await db.execute("ALTER TABLE appConfig ADD COLUMN showNameTimeLogs NUMERIC"); - await db.execute("UPDATE appConfig SET showNameTimeLogs = 0"); - - await db.transaction((txn) async{ - await txn.rawQuery( - 'SELECT * FROM appConfig', - ); - }); - } - - Future upgradeDbToV7(Database db) async { - await db.execute("ALTER TABLE appConfig ADD COLUMN doNotRememberVersion TEXT"); - - await db.transaction((txn) async{ - await txn.rawQuery( - 'SELECT * FROM appConfig', - ); - }); - } - - Database db = await openDatabase( - 'adguard_home_manager.db', - version: 7, - onCreate: (Database db, int version) async { - await db.execute("CREATE TABLE servers (id TEXT PRIMARY KEY, name TEXT, connectionMethod TEXT, domain TEXT, path TEXT, port INTEGER, user TEXT, password TEXT, defaultServer INTEGER, authToken TEXT, runningOnHa INTEGER)"); - await db.execute("CREATE TABLE appConfig (theme NUMERIC, overrideSslCheck NUMERIC, hideZeroValues NUMERIC, useDynamicColor NUMERIC, staticColor NUMERIC, useThemeColorForStatus NUMERIC, showNameTimeLogs NUMERIC, doNotRememberVersion TEXT)"); - await db.execute("INSERT INTO appConfig (theme, overrideSslCheck, hideZeroValues, useDynamicColor, staticColor, useThemeColorForStatus, showNameTimeLogs) VALUES (0, 0, 0, ${acceptsDynamicTheme == true ? 1 : 0}, 0, 0, 0)"); - }, - onUpgrade: (Database db, int oldVersion, int newVersion) async { - if (oldVersion == 1) { - await upgradeDbToV2(db); - await upgradeDbToV3(db); - await upgradeDbToV4(db); - await upgradeDbToV5(db); - await upgradeDbToV6(db); - await upgradeDbToV7(db); - } - if (oldVersion == 2) { - await upgradeDbToV3(db); - await upgradeDbToV4(db); - await upgradeDbToV5(db); - await upgradeDbToV6(db); - await upgradeDbToV7(db); - } - if (oldVersion == 3) { - await upgradeDbToV4(db); - await upgradeDbToV5(db); - await upgradeDbToV6(db); - await upgradeDbToV7(db); - } - if (oldVersion == 4) { - await upgradeDbToV5(db); - await upgradeDbToV6(db); - await upgradeDbToV7(db); - } - if (oldVersion == 5) { - await upgradeDbToV6(db); - await upgradeDbToV7(db); - } - if (oldVersion == 6) { - await upgradeDbToV7(db); - } - }, - onOpen: (Database db) async { - await db.transaction((txn) async{ - servers = await txn.rawQuery( - 'SELECT * FROM servers', - ); - }); - await db.transaction((txn) async{ - appConfig = await txn.rawQuery( - 'SELECT * FROM appConfig', - ); - }); - } - ); - - return { - "servers": servers, - "appConfig": appConfig![0], - "dbInstance": db, - }; -} \ No newline at end of file diff --git a/lib/services/db/database.dart b/lib/services/db/database.dart new file mode 100644 index 0000000..fe09d16 --- /dev/null +++ b/lib/services/db/database.dart @@ -0,0 +1,42 @@ +import 'package:sqflite/sqflite.dart'; + +Future> loadDb() async { + List>? servers; + + Database db = await openDatabase( + 'adguard_home_manager.db', + version: 11, + onCreate: (Database db, int version) async { + await db.execute( + """ + CREATE TABLE + servers ( + id TEXT PRIMARY KEY, + name TEXT, + connectionMethod TEXT, + domain TEXT, + path TEXT, + port INTEGER, + user TEXT, + password TEXT, + defaultServer INTEGER, + authToken TEXT, + runningOnHa INTEGER + ) + """ + ); + }, + onOpen: (Database db) async { + await db.transaction((txn) async{ + servers = await txn.rawQuery( + 'SELECT * FROM servers', + ); + }); + } + ); + + return { + "servers": servers, + "dbInstance": db, + }; +} \ No newline at end of file diff --git a/lib/services/db/queries.dart b/lib/services/db/queries.dart new file mode 100644 index 0000000..d81d187 --- /dev/null +++ b/lib/services/db/queries.dart @@ -0,0 +1,114 @@ +import 'package:sqflite/sqflite.dart'; + +import 'package:adguard_home_manager/functions/conversions.dart'; +import 'package:adguard_home_manager/models/server.dart'; + +Future saveServerQuery(Database db, Server server) async { + try { + return await db.transaction((txn) async { + await txn.insert( + 'servers', + { + 'id': server.id, + 'name': server.name, + 'connectionMethod': server.connectionMethod, + 'domain': server.domain, + 'path': server.path, + 'port': server.port, + 'user': server.user, + 'password': server.password, + 'defaultServer': convertFromBoolToInt(server.defaultServer), + 'authToken': server.authToken, + 'runningOnHa': convertFromBoolToInt(server.runningOnHa) + } + ); + return null; + }); + } catch (e) { + return e; + } +} + +Future editServerQuery(Database db, Server server) async { + try { + return await db.transaction((txn) async { + await txn.update( + 'servers', + { + 'id': server.id, + 'name': server.name, + 'connectionMethod': server.connectionMethod, + 'domain': server.domain, + 'path': server.path, + 'port': server.port, + 'user': server.user, + 'password': server.password, + 'defaultServer': convertFromBoolToInt(server.defaultServer), + 'authToken': server.authToken, + 'runningOnHa': convertFromBoolToInt(server.runningOnHa) + }, + where: 'id = ?', + whereArgs: [server.id] + ); + return null; + }); + } catch (e) { + return e; + } +} + + +Future removeServerQuery(Database db, String id) async { + try { + return await db.transaction((txn) async { + await txn.delete( + 'servers', + where: 'id = ?', + whereArgs: [id] + ); + return true; + }); + } catch (e) { + return false; + } +} + +Future setDefaultServerQuery(Database db, String id) async { + try { + return await db.transaction((txn) async { + await txn.update( + 'servers', + {'defaultServer': '0'}, + where: 'defaultServer = ?', + whereArgs: [1] + ); + await txn.update( + 'servers', + {'defaultServer': '1'}, + where: 'id = ?', + whereArgs: [id] + ); + return null; + }); + } catch (e) { + return e; + } +} + +Future updateConfigQuery({ + required Database db, + required String column, + required dynamic value +}) async { + try { + return await db.transaction((txn) async { + await txn.update( + 'appConfig', + { column: value }, + ); + return true; + }); + } catch (e) { + return false; + } +} \ No newline at end of file diff --git a/lib/services/external_requests.dart b/lib/services/external_requests.dart new file mode 100644 index 0000000..5f6ca58 --- /dev/null +++ b/lib/services/external_requests.dart @@ -0,0 +1,55 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; + +import 'package:adguard_home_manager/models/github_release.dart'; +import 'package:adguard_home_manager/constants/urls.dart'; +import 'package:adguard_home_manager/services/api_client.dart'; + +class ExternalRequests { + static Future getReleasesGitHub() async { + try { + final response = await http.get(Uri.parse(Urls.getReleasesGitHub)); + if (response.statusCode == 200) { + return ApiResponse( + successful: true, + content: List.from( + jsonDecode(response.body).map((entry) => GitHubRelease.fromJson(entry)) + ) + ); + } + else { + return const ApiResponse(successful: false); + } + } catch (e) { + return const ApiResponse(successful: false); + } + } + + static Future getReleaseData({ + // If releaseTag is null gets latest release + String? releaseTag + }) async { + try { + final response = await http.get( + Uri.parse( + releaseTag != null + ? "${Urls.adGuardHomeReleasesTags}/$releaseTag" + : Urls.getLatestReleaseGitHub + ) + ); + if (response.statusCode == 200) { + return ApiResponse( + successful: true, + content: GitHubRelease.fromJson(jsonDecode(response.body)), + statusCode: response.statusCode + ); + } + else { + return const ApiResponse(successful: false); + } + } catch (e) { + return const ApiResponse(successful: false); + } + } +} \ No newline at end of file diff --git a/lib/services/http_requests.dart b/lib/services/http_requests.dart index 5e249e9..2d001f2 100644 --- a/lib/services/http_requests.dart +++ b/lib/services/http_requests.dart @@ -4,12 +4,13 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:sentry_flutter/sentry_flutter.dart'; + import 'package:adguard_home_manager/models/blocked_services.dart'; import 'package:adguard_home_manager/models/dhcp.dart'; import 'package:adguard_home_manager/models/dns_info.dart'; import 'package:adguard_home_manager/models/encryption.dart'; import 'package:adguard_home_manager/models/filtering.dart'; -import 'package:adguard_home_manager/models/github_release.dart'; import 'package:adguard_home_manager/models/logs.dart'; import 'package:adguard_home_manager/models/filtering_status.dart'; import 'package:adguard_home_manager/models/app_log.dart'; @@ -19,11 +20,10 @@ import 'package:adguard_home_manager/models/server_status.dart'; import 'package:adguard_home_manager/models/clients.dart'; import 'package:adguard_home_manager/models/clients_allowed_blocked.dart'; import 'package:adguard_home_manager/models/server.dart'; -import 'package:adguard_home_manager/constants/urls.dart'; Future> apiRequest({ - required Server server, + required Server server, required String method, required String urlPath, dynamic body, @@ -35,7 +35,9 @@ Future> apiRequest({ HttpClient httpClient = HttpClient(); if (method == 'get') { HttpClientRequest request = await httpClient.getUrl(Uri.parse(connectionString)); - request.headers.set('Authorization', 'Basic ${server.authToken}'); + if (server.authToken != null) { + request.headers.set('Authorization', 'Basic ${server.authToken}'); + } HttpClientResponse response = overrideTimeout == true ? await request.close() : await request.close().timeout(const Duration(seconds: 10)); @@ -58,9 +60,26 @@ Future> apiRequest({ }; } } - else if (method == 'post') { - HttpClientRequest request = await httpClient.postUrl(Uri.parse(connectionString)); - request.headers.set('Authorization', 'Basic ${server.authToken}'); + else if (method == 'post' || method == 'put') { + HttpClientRequest? request; + if (method == 'post') { + request = await httpClient.postUrl(Uri.parse(connectionString)); + } + else if (method == 'put') { + request = await httpClient.putUrl(Uri.parse(connectionString)); + } + else { + return { + 'hasResponse': false, + 'error': true, + 'statusCode': null, + 'body': 'Invalid method [selected $method]' + }; + } + + if (server.authToken != null) { + request.headers.set('Authorization', 'Basic ${server.authToken}'); + } request.headers.set('content-type', 'application/json'); request.add(utf8.encode(json.encode(body))); HttpClientResponse response = overrideTimeout == true @@ -243,801 +262,956 @@ Future loginHA(Server server) async { } } -Future getServerStatus(Server server) async { - final result = await Future.wait([ - apiRequest(server: server, method: 'get', urlPath: '/stats', type: 'server_status'), - apiRequest(server: server, method: 'get', urlPath: '/status', type: 'server_status'), - apiRequest(server: server, method: 'get', urlPath: '/filtering/status', type: 'server_status'), - apiRequest(server: server, method: 'get', urlPath: '/safesearch/status', type: 'server_status'), - apiRequest(server: server, method: 'get', urlPath: '/safebrowsing/status', type: 'server_status'), - apiRequest(server: server, method: 'get', urlPath: '/parental/status', type: 'server_status'), - apiRequest(server: server, method: 'get', urlPath: '/clients', type: 'server_status'), - ]); +class ApiClient { + final Server server; - if ( - result[0]['hasResponse'] == true && - result[1]['hasResponse'] == true && - result[2]['hasResponse'] == true && - result[3]['hasResponse'] == true && - result[4]['hasResponse'] == true && - result[5]['hasResponse'] == true && - result[6]['hasResponse'] == true - ) { - if ( - result[0]['statusCode'] == 200 && - result[1]['statusCode'] == 200 && - result[2]['statusCode'] == 200 && - result[3]['statusCode'] == 200 && - result[4]['statusCode'] == 200 && - result[5]['statusCode'] == 200 && - result[6]['statusCode'] == 200 - ) { - final Map mappedData = { - 'stats': jsonDecode(result[0]['body']), - 'clients': jsonDecode(result[6]['body'])['clients'], - 'generalEnabled': jsonDecode(result[1]['body']), - 'filtering': jsonDecode(result[2]['body']), - 'safeSearchEnabled': jsonDecode(result[3]['body']), - 'safeBrowsingEnabled': jsonDecode(result[4]['body']), - 'parentalControlEnabled': jsonDecode(result[5]['body']), - }; - return { - 'result': 'success', - 'data': ServerStatusData.fromJson(mappedData) - }; - } - else { - return { - 'result': 'error', - 'log': AppLog( - type: 'get_server_status', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result.map((res) => res['statusCode']).toString(), - resBody: result.map((res) => res['body']).toString() - ) - }; - } - } - else { - return { - 'result': 'error', - 'log': AppLog( - type: 'get_server_status', - dateTime: DateTime.now(), - message: 'no_response', - statusCode: result.map((res) => res['statusCode'] ?? 'null').toString(), - resBody: result.map((res) => res['body'] ?? 'null').toString() - ) - }; - } -} + ApiClient({ + required this.server + }); -Future updateFiltering(Server server, bool enable) async { - final result = await apiRequest( - urlPath: '/filtering/config', - method: 'post', - server: server, - body: { - 'enabled': enable - }, - type: 'update_filtering' - ); - - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return {'result': 'success'}; - } - else { - return { - 'result': 'error', - 'log': AppLog( - type: 'update_filtering', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'] - ) - }; - } - } - else { - return result; - } -} - -Future updateSafeSearch(Server server, bool enable) async { - final result = enable == true - ? await apiRequest( - urlPath: '/safesearch/enable', - method: 'post', - server: server, - type: 'enable_safe_search' - ) - : await apiRequest( - urlPath: '/safesearch/disable', - method: 'post', + Future getServerVersion() async { + try { + final result = await apiRequest( server: server, - type: 'disable_safe_search' + method: 'get', + urlPath: '/status', + type: 'get_server_version' ); - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return {'result': 'success'}; - } - else { + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200 && result['body'] != null) { + return { + 'result': 'success', + 'data': jsonDecode(result['body'])['version'] + }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'get_server_version', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'] + ) + }; + } + } + else { + return result; + } + } catch (e) { + Sentry.captureException(e); return { 'result': 'error', 'log': AppLog( - type: 'safe_search', + type: 'get_server_version', dateTime: DateTime.now(), message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'] + resBody: e.toString() ) }; } } - else { - return result; - } -} -Future updateSafeBrowsing(Server server, bool enable) async { - final result = enable == true - ? await apiRequest( - urlPath: '/safebrowsing/enable', - method: 'post', - server: server, - type: 'enable_safe_browsing' - ) - : await apiRequest( - urlPath: '/safebrowsing/disable', - method: 'post', - server: server, - type: 'disable_safe_browsing' - ); + Future getServerStatus() async { + try { + final result = await Future.wait([ + apiRequest(server: server, method: 'get', urlPath: '/stats', type: 'server_status'), + apiRequest(server: server, method: 'get', urlPath: '/status', type: 'server_status'), + apiRequest(server: server, method: 'get', urlPath: '/filtering/status', type: 'server_status'), + apiRequest(server: server, method: 'get', urlPath: '/safesearch/status', type: 'server_status'), + apiRequest(server: server, method: 'get', urlPath: '/safebrowsing/status', type: 'server_status'), + apiRequest(server: server, method: 'get', urlPath: '/parental/status', type: 'server_status'), + apiRequest(server: server, method: 'get', urlPath: '/clients', type: 'server_status'), + ]); - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return {'result': 'success'}; - } - else { + if ( + result[0]['hasResponse'] == true && + result[1]['hasResponse'] == true && + result[2]['hasResponse'] == true && + result[3]['hasResponse'] == true && + result[4]['hasResponse'] == true && + result[5]['hasResponse'] == true && + result[6]['hasResponse'] == true + ) { + if ( + result[0]['statusCode'] == 200 && + result[1]['statusCode'] == 200 && + result[2]['statusCode'] == 200 && + result[3]['statusCode'] == 200 && + result[4]['statusCode'] == 200 && + result[5]['statusCode'] == 200 && + result[6]['statusCode'] == 200 + ) { + final Map mappedData = { + 'stats': jsonDecode(result[0]['body']), + 'clients': jsonDecode(result[6]['body'])['clients'], + 'status': jsonDecode(result[1]['body']), + 'filtering': jsonDecode(result[2]['body']), + 'safeSearch': jsonDecode(result[3]['body']), + 'safeBrowsingEnabled': jsonDecode(result[4]['body']), + 'parentalControlEnabled': jsonDecode(result[5]['body']), + }; + return { + 'result': 'success', + 'data': ServerStatus.fromJson(mappedData) + }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'get_server_status', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result.map((res) => res['statusCode']).toString(), + resBody: result.map((res) => res['body']).toString() + ) + }; + } + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'get_server_status', + dateTime: DateTime.now(), + message: 'no_response', + statusCode: result.map((res) => res['statusCode'] ?? 'null').toString(), + resBody: result.map((res) => res['body'] ?? 'null').toString() + ) + }; + } + } catch (e) { + Sentry.captureException(e); return { - 'result': 'error', - 'log': AppLog( - type: 'safe_browsing', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'] - ) - }; + 'result': 'error', + 'log': AppLog( + type: 'get_server_status', + dateTime: DateTime.now(), + message: 'no_response', + resBody: e.toString() + ) + }; } } - else { - return result; - } -} -Future updateParentalControl(Server server, bool enable) async { - final result = enable == true - ? await apiRequest( - urlPath: '/parental/enable', - method: 'post', - server: server, - type: 'enable_parental_control' - ) - : await apiRequest( - urlPath: '/parental/disable', - method: 'post', - server: server, - type: 'disable_parental_control' - ); - - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return {'result': 'success'}; - } - else { - return { - 'result': 'error', - 'log': AppLog( - type: 'parental_control', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'] - ) - }; - } - } - else { - return result; - } -} - -Future updateGeneralProtection(Server server, bool enable) async { + Future updateFiltering({ + required bool enable, + }) async { final result = await apiRequest( - urlPath: '/dns_config', - method: 'post', - server: server, - body: { - 'protection_enabled': enable - }, - type: 'general_protection' - ); + urlPath: '/filtering/config', + method: 'post', + server: server, + body: { + 'enabled': enable + }, + type: 'update_filtering' + ); - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return {'result': 'success'}; + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return {'result': 'success'}; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'update_filtering', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'] + ) + }; + } } else { - return { - 'result': 'error', - 'log': AppLog( - type: 'general_protection', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'] + return result; + } + } + + Future updateSafeSearchLegacy(bool enable) async { + final result = enable == true + ? await apiRequest( + urlPath: '/safesearch/enable', + method: 'post', + server: server, + type: 'enable_safe_search' ) - }; - } - } - else { - return result; - } -} + : await apiRequest( + urlPath: '/safesearch/disable', + method: 'post', + server: server, + type: 'disable_safe_search' + ); -Future getClients(Server server) async { - final result = await Future.wait([ - apiRequest(server: server, method: 'get', urlPath: '/clients', type: 'get_clients'), - apiRequest(server: server, method: 'get', urlPath: '/access/list', type: 'get_clients'), - ]); - - if (result[0]['hasResponse'] == true && result[1]['hasResponse'] == true) { - if (result[0]['statusCode'] == 200 && result[1]['statusCode'] == 200) { - final clients = ClientsData.fromJson(jsonDecode(result[0]['body'])); - clients.clientsAllowedBlocked = ClientsAllowedBlocked.fromJson(jsonDecode(result[1]['body'])); - return { - 'result': 'success', - 'data': clients - }; + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return {'result': 'success'}; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'safe_search', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'] + ) + }; + } } else { + return result; + } + } + + Future updateSafeBrowsing(bool enable) async { + final result = enable == true + ? await apiRequest( + urlPath: '/safebrowsing/enable', + method: 'post', + server: server, + type: 'enable_safe_browsing' + ) + : await apiRequest( + urlPath: '/safebrowsing/disable', + method: 'post', + server: server, + type: 'disable_safe_browsing' + ); + + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return {'result': 'success'}; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'safe_browsing', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'] + ) + }; + } + } + else { + return result; + } + } + + Future updateParentalControl(bool enable) async { + final result = enable == true + ? await apiRequest( + urlPath: '/parental/enable', + method: 'post', + server: server, + type: 'enable_parental_control' + ) + : await apiRequest( + urlPath: '/parental/disable', + method: 'post', + server: server, + type: 'disable_parental_control' + ); + + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return {'result': 'success'}; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'parental_control', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'] + ) + }; + } + } + else { + return result; + } + } + + Future updateGeneralProtection({ + required bool enable, + int? time + }) async { + final result = await apiRequest( + urlPath: '/protection', + method: 'post', + server: server, + body: { + 'enabled': enable, + 'duration': time + }, + type: 'general_protection' + ); + + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return {'result': 'success'}; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'general_protection', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'] + ) + }; + } + } + else { + return result; + } + } + + Future updateGeneralProtectionLegacy(bool enable) async { + final result = await apiRequest( + urlPath: '/dns_config', + method: 'post', + server: server, + body: { + 'protection_enabled': enable + }, + type: 'general_protection' + ); + + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return {'result': 'success'}; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'general_protection', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'] + ) + }; + } + } + else { + return result; + } + } + + Future getClients() async { + try { + final result = await Future.wait([ + apiRequest(server: server, method: 'get', urlPath: '/clients', type: 'get_clients'), + apiRequest(server: server, method: 'get', urlPath: '/access/list', type: 'get_clients'), + ]); + + if (result[0]['hasResponse'] == true && result[1]['hasResponse'] == true) { + if (result[0]['statusCode'] == 200 && result[1]['statusCode'] == 200) { + final clients = Clients.fromJson(jsonDecode(result[0]['body'])); + clients.clientsAllowedBlocked = ClientsAllowedBlocked.fromJson(jsonDecode(result[1]['body'])); + return { + 'result': 'success', + 'data': clients + }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'get_clients', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result.map((res) => res['statusCode'] ?? 'null').toString(), + resBody: result.map((res) => res['body'] ?? 'null').toString(), + ) + }; + } + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'get_clients', + dateTime: DateTime.now(), + message: 'no_response', + statusCode: result.map((res) => res['statusCode'] ?? 'null').toString(), + resBody: result.map((res) => res['body'] ?? 'null').toString(), + ) + }; + } + } catch (e) { + Sentry.captureException(e); return { 'result': 'error', 'log': AppLog( type: 'get_clients', dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result.map((res) => res['statusCode'] ?? 'null').toString(), - resBody: result.map((res) => res['body'] ?? 'null').toString(), + message: 'no_response', + resBody: e.toString() ) }; } } - else { - return { - 'result': 'error', - 'log': AppLog( - type: 'get_clients', - dateTime: DateTime.now(), - message: 'no_response', - statusCode: result.map((res) => res['statusCode'] ?? 'null').toString(), - resBody: result.map((res) => res['body'] ?? 'null').toString(), - ) - }; - } -} -Future requestAllowedBlockedClientsHosts(Server server, Map?> body) async { - final result = await apiRequest( - urlPath: '/access/set', - method: 'post', - server: server, - body: body, - type: 'get_clients' - ); + Future requestAllowedBlockedClientsHosts(Map?> body) async { + final result = await apiRequest( + urlPath: '/access/set', + method: 'post', + server: server, + body: body, + type: 'get_clients' + ); - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return {'result': 'success'}; - } - if (result['statusCode'] == 400) { - return { - 'result': 'error', - 'message': 'client_another_list' - }; + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return {'result': 'success'}; + } + if (result['statusCode'] == 400) { + return { + 'result': 'error', + 'message': 'client_another_list' + }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'get_clients', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'] + ) + }; + } } else { - return { - 'result': 'error', - 'log': AppLog( - type: 'get_clients', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'] - ) - }; + return result; } } - else { - return result; - } -} -Future getLogs({ - required Server server, - required int count, - int? offset, - DateTime? olderThan, - String? responseStatus, - String? search -}) async { - final result = await apiRequest( - server: server, - method: 'get', - urlPath: '/querylog?limit=$count${offset != null ? '&offset=$offset' : ''}${olderThan != null ? '&older_than=${olderThan.toIso8601String()}' : ''}${responseStatus != null ? '&response_status=$responseStatus' : ''}${search != null ? '&search=$search' : ''}', - type: 'get_logs' - ); - - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return { - 'result': 'success', - 'data': LogsData.fromJson(jsonDecode(result['body'])) - }; - } - else { + Future getLogs({ + required int count, + int? offset, + DateTime? olderThan, + String? responseStatus, + String? search + }) async { + try { + final result = await apiRequest( + server: server, + method: 'get', + urlPath: '/querylog?limit=$count${offset != null ? '&offset=$offset' : ''}${olderThan != null ? '&older_than=${olderThan.toIso8601String()}' : ''}${responseStatus != null ? '&response_status=$responseStatus' : ''}${search != null ? '&search=$search' : ''}', + type: 'get_logs' + ); + + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return { + 'result': 'success', + 'data': LogsData.fromJson(jsonDecode(result['body'])) + }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'get_logs', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'] + ) + }; + } + } + else { + return result; + } + } catch (e) { + Sentry.captureException(e); return { 'result': 'error', 'log': AppLog( type: 'get_logs', dateTime: DateTime.now(), message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'] + resBody: e.toString() ) }; } } - else { - return result; - } -} -Future getFilteringRules({ - required Server server, -}) async { - final result = await apiRequest( - server: server, - method: 'get', - urlPath: '/filtering/status', - type: 'get_filtering_rules' - ); - - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return { - 'result': 'success', - 'data': FilteringStatus.fromJson(jsonDecode(result['body'])) - }; - } - else { + Future getFilteringRules() async { + try { + final result = await apiRequest( + server: server, + method: 'get', + urlPath: '/filtering/status', + type: 'get_filtering_rules' + ); + + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return { + 'result': 'success', + 'data': FilteringStatus.fromJson(jsonDecode(result['body'])) + }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'get_filtering_rules', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'] + ) + }; + } + } + else { + return result; + } + } catch (e) { + Sentry.captureException(e); return { 'result': 'error', 'log': AppLog( type: 'get_filtering_rules', dateTime: DateTime.now(), message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'] + resBody: e.toString() ) }; } } - else { - return result; - } -} -Future postFilteringRules({ - required Server server, - required Map> data, -}) async { - final result = await apiRequest( - urlPath: '/filtering/set_rules', - method: 'post', - server: server, - body: data, - type: 'post_filering_rules' - ); - - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return {'result': 'success'}; - } - else { - return { - 'result': 'error', - 'log': AppLog( - type: 'post_filtering_rules', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'] - ) - }; - } - } - else { - return result; - } -} - -Future postAddClient({ - required Server server, - required Map data, -}) async { - final result = await apiRequest( - urlPath: '/clients/add', - method: 'post', - server: server, - body: data, - type: 'add_client' - ); - - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return {'result': 'success'}; - } - else { - return { - 'result': 'error', - 'log': AppLog( - type: 'add_client', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'] - ) - }; - } - } - else { - return result; - } -} - -Future postUpdateClient({ - required Server server, - required Map data, -}) async { - final result = await apiRequest( - urlPath: '/clients/update', - method: 'post', - server: server, - body: data, - type: 'update_client' - ); - - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return {'result': 'success'}; - } - else { - return { - 'result': 'error', - 'log': AppLog( - type: 'update_client', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'] - ) - }; - } - } - else { - return result; - } -} - -Future postDeleteClient({ - required Server server, - required String name, -}) async { - final result = await apiRequest( - urlPath: '/clients/delete', - method: 'post', - server: server, - body: {'name': name}, - type: 'remove_client' - ); - - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return {'result': 'success'}; - } - else { - return { - 'result': 'error', - 'log': AppLog( - type: 'remove_client', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'] - ) - }; - } - } - else { - return result; - } -} - -Future getFiltering({ - required Server server, -}) async { - final result = await Future.wait([ - apiRequest( - urlPath: '/filtering/status', - method: 'get', + Future postFilteringRules({ + required Map> data, + }) async { + final result = await apiRequest( + urlPath: '/filtering/set_rules', + method: 'post', server: server, - type: 'get_filtering_status' - ), - apiRequest( - urlPath: '/blocked_services/list', - method: 'get', - server: server, - type: 'get_filtering_status' - ), - ]); + body: data, + type: 'post_filering_rules' + ); - if (result[0]['hasResponse'] == true && result[0]['hasResponse'] == true) { - if (result[0]['statusCode'] == 200 && result[0]['statusCode'] == 200) { - return { - 'result': 'success', - 'data': FilteringData.fromJson({ - ...jsonDecode(result[0]['body']), - "blocked_services": jsonDecode(result[1]['body']), - }) - }; + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return {'result': 'success'}; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'post_filtering_rules', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'] + ) + }; + } } else { + return result; + } + } + + Future postAddClient({ + required Map data, + }) async { + final result = await apiRequest( + urlPath: '/clients/add', + method: 'post', + server: server, + body: data, + type: 'add_client' + ); + + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return {'result': 'success'}; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'add_client', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'] + ) + }; + } + } + else { + return result; + } + } + + Future postUpdateClient({ + required Map data, + }) async { + final result = await apiRequest( + urlPath: '/clients/update', + method: 'post', + server: server, + body: data, + type: 'update_client' + ); + + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return {'result': 'success'}; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'update_client', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'] + ) + }; + } + } + else { + return result; + } + } + + Future postDeleteClient({ + required String name, + }) async { + final result = await apiRequest( + urlPath: '/clients/delete', + method: 'post', + server: server, + body: {'name': name}, + type: 'remove_client' + ); + + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return {'result': 'success'}; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'remove_client', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'] + ) + }; + } + } + else { + return result; + } + } + + Future getFiltering() async { + try { + final result = await Future.wait([ + apiRequest( + urlPath: '/filtering/status', + method: 'get', + server: server, + type: 'get_filtering_status' + ), + apiRequest( + urlPath: '/blocked_services/list', + method: 'get', + server: server, + type: 'get_filtering_status' + ), + ]); + + if (result[0]['hasResponse'] == true && result[0]['hasResponse'] == true) { + if (result[0]['statusCode'] == 200 && result[0]['statusCode'] == 200) { + return { + 'result': 'success', + 'data': Filtering.fromJson({ + ...jsonDecode(result[0]['body']), + "blocked_services": result[1]['body'] != null + ? jsonDecode(result[1]['body']) + : [] + }) + }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'get_filtering_status', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result.map((res) => res['statusCode'] ?? 'null').toString(), + resBody: result.map((res) => res['body'] ?? 'null').toString(), + ) + }; + } + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'get_filtering_status', + dateTime: DateTime.now(), + message: 'no_response', + statusCode: result.map((res) => res['statusCode'] ?? 'null').toString(), + resBody: result.map((res) => res['body'] ?? 'null').toString(), + ) + }; + } + } catch (e) { + Sentry.captureException(e); return { 'result': 'error', 'log': AppLog( type: 'get_filtering_status', dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result.map((res) => res['statusCode'] ?? 'null').toString(), - resBody: result.map((res) => res['body'] ?? 'null').toString(), + message: 'no_response', + resBody: e.toString(), ) }; } } - else { - return { - 'result': 'error', - 'log': AppLog( - type: 'get_filtering_status', - dateTime: DateTime.now(), - message: 'no_response', - statusCode: result.map((res) => res['statusCode'] ?? 'null').toString(), - resBody: result.map((res) => res['body'] ?? 'null').toString(), - ) - }; - } -} -Future setCustomRules({ - required Server server, - required List rules, -}) async { - final result = await apiRequest( - urlPath: '/filtering/set_rules', - method: 'post', - server: server, - body: {'rules': rules}, - type: 'set_custom_rules' - ); - - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return {'result': 'success'}; - } - else { - return { - 'result': 'error', - 'log': AppLog( - type: 'set_custom_rules', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'] - ) - }; - } - } - else { - return result; - } - -} - -Future addFilteringList({ - required Server server, - required Map data, -}) async { - final result = await apiRequest( - urlPath: '/filtering/add_url', - method: 'post', - server: server, - body: data, - type: 'add_filtering_url' - ); - - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return { - 'result': 'success', - 'data': result['body'] - }; - } - else { - return { - 'result': 'error', - 'log': AppLog( - type: 'add_filtering_url', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'] - ) - }; - } - } - else { - return result; - } -} - -Future updateFilterList({ - required Server server, - required Map data, -}) async { - final result = await apiRequest( - urlPath: '/filtering/set_url', - method: 'post', - server: server, - body: data, - type: 'update_filter_list' - ); - - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return {'result': 'success'}; - } - else { - return { - 'result': 'error', - 'log': AppLog( - type: 'update_filter_list', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'] - ) - }; - } - } - else { - return result; - } -} - -Future deleteFilterList({ - required Server server, - required Map data, -}) async { - final result = await apiRequest( - urlPath: '/filtering/remove_url', - method: 'post', - server: server, - body: data, - type: 'delete_filter_list' - ); - - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return {'result': 'success'}; - } - else { - return { - 'result': 'error', - 'log': AppLog( - type: 'delete_filter_list', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'] - ) - }; - } - } - else { - return result; - } -} - -Future getServerInfo({ - required Server server, -}) async { - final result = await apiRequest( - urlPath: '/status', - method: 'get', - server: server, - type: 'server_info' - ); - - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return { - 'result': 'success', - 'data': ServerInfoData.fromJson(jsonDecode(result['body'])) - }; - } - else { - return { - 'result': 'error', - 'log': AppLog( - type: 'server_info', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'] - ) - }; - } - } - else { - return result; - } -} - -Future updateLists({ - required Server server, -}) async { - final result = await Future.wait([ - apiRequest( - urlPath: '/filtering/refresh', + Future setCustomRules({ + required List rules, + }) async { + final result = await apiRequest( + urlPath: '/filtering/set_rules', method: 'post', server: server, - body: {'whitelist': true}, - type: 'update_lists', - overrideTimeout: true - ), - apiRequest( - urlPath: '/filtering/refresh', + body: {'rules': rules}, + type: 'set_custom_rules' + ); + + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return {'result': 'success'}; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'set_custom_rules', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'] + ) + }; + } + } + else { + return result; + } + + } + + Future addFilteringList({ + required Map data, + }) async { + final result = await apiRequest( + urlPath: '/filtering/add_url', method: 'post', server: server, - body: {'whitelist': false}, - type: 'update_lists', - overrideTimeout: true - ), - ]); + body: data, + type: 'add_filtering_url' + ); - if (result[0]['hasResponse'] == true && result[1]['hasResponse'] == true) { - if (result[0]['statusCode'] == 200 && result[1]['statusCode'] == 200) { - return { - 'result': 'success', - 'data': {'updated': jsonDecode(result[0]['body'])['updated']+jsonDecode(result[1]['body'])['updated']} - }; + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return { + 'result': 'success', + 'data': result['body'] + }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'add_filtering_url', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'] + ) + }; + } + } + else { + return result; + } + } + + Future updateFilterList({ + required Map data, + }) async { + final result = await apiRequest( + urlPath: '/filtering/set_url', + method: 'post', + server: server, + body: data, + type: 'update_filter_list' + ); + + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return {'result': 'success'}; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'update_filter_list', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'] + ) + }; + } + } + else { + return result; + } + } + + Future deleteFilterList({ + required Map data, + }) async { + final result = await apiRequest( + urlPath: '/filtering/remove_url', + method: 'post', + server: server, + body: data, + type: 'delete_filter_list' + ); + + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return {'result': 'success'}; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'delete_filter_list', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'] + ) + }; + } + } + else { + return result; + } + } + + Future getServerInfo() async { + final result = await apiRequest( + urlPath: '/status', + method: 'get', + server: server, + type: 'server_info' + ); + + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return { + 'result': 'success', + 'data': ServerInfoData.fromJson(jsonDecode(result['body'])) + }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'server_info', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'] + ) + }; + } + } + else { + return result; + } + } + + Future updateLists() async { + final result = await Future.wait([ + apiRequest( + urlPath: '/filtering/refresh', + method: 'post', + server: server, + body: {'whitelist': true}, + type: 'update_lists', + overrideTimeout: true + ), + apiRequest( + urlPath: '/filtering/refresh', + method: 'post', + server: server, + body: {'whitelist': false}, + type: 'update_lists', + overrideTimeout: true + ), + ]); + + if (result[0]['hasResponse'] == true && result[1]['hasResponse'] == true) { + if (result[0]['statusCode'] == 200 && result[1]['statusCode'] == 200) { + return { + 'result': 'success', + 'data': {'updated': jsonDecode(result[0]['body'])['updated']+jsonDecode(result[1]['body'])['updated']} + }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'update_lists', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result.map((res) => res['statusCode'] ?? 'null').toString(), + resBody: result.map((res) => res['body'] ?? 'null').toString(), + ) + }; + } } else { return { @@ -1045,914 +1219,1044 @@ Future updateLists({ 'log': AppLog( type: 'update_lists', dateTime: DateTime.now(), - message: 'error_code_not_expected', + message: [result[0]['message'], result[1]['message']].toString(), statusCode: result.map((res) => res['statusCode'] ?? 'null').toString(), resBody: result.map((res) => res['body'] ?? 'null').toString(), ) }; } } - else { - return { - 'result': 'error', - 'log': AppLog( - type: 'update_lists', - dateTime: DateTime.now(), - message: [result[0]['message'], result[1]['message']].toString(), - statusCode: result.map((res) => res['statusCode'] ?? 'null').toString(), - resBody: result.map((res) => res['body'] ?? 'null').toString(), - ) - }; - } -} -Future checkHostFiltered({ - required Server server, - required String host, -}) async { - final result = await apiRequest( - urlPath: '/filtering/check_host?name=$host', - method: 'get', - server: server, - type: 'check_host_filtered' - ); - - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return { - 'result': 'success', - 'data': jsonDecode(result['body']) - }; - } - else { - return { - 'result': 'error', - 'log': AppLog( - type: 'update_lists', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'], - ) - }; - } - } - else { - return result; - } -} - -Future requestChangeUpdateFrequency({ - required Server server, - required Map data, -}) async { - final result = await apiRequest( - urlPath: '/filtering/config', - method: 'post', - server: server, - body: data, - type: 'change_update_frequency' - ); - - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return {'result': 'success'}; - } - else { - return { - 'result': 'error', - 'log': AppLog( - type: 'change_update_frequency', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'], - ) - }; - } - } - else { - return result; - } -} - -Future setBlockedServices({ - required Server server, - required List data, -}) async { - final result = await apiRequest( - urlPath: '/blocked_services/set', - method: 'post', - server: server, - body: data, - type: 'update_blocked_services' - ); - - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return {'result': 'success'}; - } - else { - return { - 'result': 'error', - 'log': AppLog( - type: 'update_blocked_services', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'], - ) - }; - } - } - else { - return result; - } -} - -Future getDhcpData({ - required Server server, -}) async { - final result = await Future.wait([ - apiRequest( - urlPath: '/dhcp/interfaces', + Future checkHostFiltered({ + required String host, + }) async { + final result = await apiRequest( + urlPath: '/filtering/check_host?name=$host', method: 'get', server: server, - type: 'get_dhcp_data' - ), - apiRequest( - urlPath: '/dhcp/status', + type: 'check_host_filtered' + ); + + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return { + 'result': 'success', + 'data': jsonDecode(result['body']) + }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'update_lists', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'], + ) + }; + } + } + else { + return result; + } + } + + Future requestChangeUpdateFrequency({ + required Map data, + }) async { + final result = await apiRequest( + urlPath: '/filtering/config', + method: 'post', + server: server, + body: data, + type: 'change_update_frequency' + ); + + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return {'result': 'success'}; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'change_update_frequency', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'], + ) + }; + } + } + else { + return result; + } + } + + Future setBlockedServices({ + required List data, + }) async { + final result = await apiRequest( + urlPath: '/blocked_services/set', + method: 'post', + server: server, + body: data, + type: 'update_blocked_services' + ); + + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return {'result': 'success'}; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'update_blocked_services', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'], + ) + }; + } + } + else { + return result; + } + } + + Future getDhcpData() async { + try { + final result = await Future.wait([ + apiRequest( + urlPath: '/dhcp/interfaces', + method: 'get', + server: server, + type: 'get_dhcp_data' + ), + apiRequest( + urlPath: '/dhcp/status', + method: 'get', + server: server, + type: 'get_dhcp_data' + ), + ]); + + if (result[0]['hasResponse'] == true && result[1]['hasResponse'] == true) { + if (result[0]['statusCode'] == 200 && result[1]['statusCode'] == 200) { + List interfaces = List.from(jsonDecode(result[0]['body']).entries.map((entry) => NetworkInterface.fromJson(entry.value))); + + return { + 'result': 'success', + 'data': DhcpModel( + networkInterfaces: interfaces, + dhcpStatus: DhcpStatus.fromJson(jsonDecode(result[1]['body'])) + ) + }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'get_dhcp_data', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result.map((res) => res['statusCode'] ?? 'null').toString(), + resBody: result.map((res) => res['body'] ?? 'null').toString(), + ) + }; + } + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'get_dhpc_data', + dateTime: DateTime.now(), + message: [result[0]['log'].message, result[1]['log'].message].toString(), + statusCode: result.map((res) => res['statusCode'] ?? 'null').toString(), + resBody: result.map((res) => res['body'] ?? 'null').toString(), + ) + }; + } + } catch (e) { + Sentry.captureException(e); + return { + 'result': 'error', + 'log': AppLog( + type: 'get_dhpc_data', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + resBody: e.toString(), + ) + }; + } + } + + Future saveDhcpConfig({ + required Map data, + }) async { + final result = await apiRequest( + urlPath: '/dhcp/set_config', + method: 'post', + server: server, + body: data, + type: 'save_dhcp_config' + ); + + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return {'result': 'success'}; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'save_dhcp_config', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'], + ) + }; + } + } + else { + return result; + } + } + + Future resetDhcpConfig() async { + final result = await apiRequest( + urlPath: '/dhcp/reset', + method: 'post', + server: server, + body: {}, + type: 'reset_dhcp_config' + ); + + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return {'result': 'success'}; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'reset_dhcp_config', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'], + ) + }; + } + } + else { + return result; + } + } + + Future deleteStaticLease({ + required Map data + }) async { + final result = await apiRequest( + urlPath: '/dhcp/remove_static_lease', + method: 'post', + server: server, + body: data, + type: 'remove_static_lease' + ); + + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return {'result': 'success'}; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'remove_static_lease', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'], + ) + }; + } + } + else { + return result; + } + } + + Future createStaticLease({ + required Map data + }) async { + final result = await apiRequest( + urlPath: '/dhcp/add_static_lease', + method: 'post', + server: server, + body: data, + type: 'add_static_lease' + ); + + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return {'result': 'success'}; + } + else if (result['statusCode'] == 400 && result['body'].contains('static lease already exists')) { + return { + 'result': 'error', + 'message': 'already_exists', + 'log': AppLog( + type: 'add_static_lease', + dateTime: DateTime.now(), + message: 'already_exists', + statusCode: result['statusCode'].toString(), + resBody: result['body'], + ) + }; + } + else if (result['statusCode'] == 400 && result['body'].contains('server is unconfigured')) { + return { + 'result': 'error', + 'message': 'server_not_configured', + 'log': AppLog( + type: 'add_static_lease', + dateTime: DateTime.now(), + message: 'server_not_configured', + statusCode: result['statusCode'].toString(), + resBody: result['body'], + ) + }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'add_static_lease', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'], + ) + }; + } + } + else { + return result; + } + } + + Future restoreAllLeases() async { + final result = await apiRequest( + urlPath: '/dhcp/reset_leases', + method: 'post', + server: server, + body: {}, + type: 'restore_all_leases' + ); + + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return {'result': 'success'}; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'restore_all_leases', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'], + ) + }; + } + } + else { + return result; + } + } + + Future getDnsRewriteRules() async { + final result = await apiRequest( + urlPath: '/rewrite/list', method: 'get', server: server, - type: 'get_dhcp_data' - ), - ]); + type: 'get_dns_rewrite_rules' + ); - if (result[0]['hasResponse'] == true && result[1]['hasResponse'] == true) { - if (result[0]['statusCode'] == 200 && result[1]['statusCode'] == 200) { - List interfaces = List.from(jsonDecode(result[0]['body']).entries.map((entry) => NetworkInterface.fromJson(entry.value))); + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + final List data = List.from( + jsonDecode(result['body']).map((item) => RewriteRules.fromJson(item)) + ); - return { - 'result': 'success', - 'data': DhcpData( - networkInterfaces: interfaces, - dhcpStatus: DhcpStatus.fromJson(jsonDecode(result[1]['body'])) - ) - }; + return { + 'result': 'success', + 'data': data + }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'get_dns_rewrite_rules', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'], + ) + }; + } } else { - return { - 'result': 'error', - 'log': AppLog( - type: 'get_dhcp_data', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result.map((res) => res['statusCode'] ?? 'null').toString(), - resBody: result.map((res) => res['body'] ?? 'null').toString(), - ) - }; + return result; } } - else { - return { - 'result': 'error', - 'log': AppLog( - type: 'get_dhpc_data', - dateTime: DateTime.now(), - message: [result[0]['log'].message, result[1]['log'].message].toString(), - statusCode: result.map((res) => res['statusCode'] ?? 'null').toString(), - resBody: result.map((res) => res['body'] ?? 'null').toString(), - ) - }; - } -} -Future saveDhcpConfig({ - required Server server, - required Map data, -}) async { - final result = await apiRequest( - urlPath: '/dhcp/set_config', - method: 'post', - server: server, - body: data, - type: 'save_dhcp_config' - ); + Future deleteDnsRewriteRule({ + required Map data, + }) async { + final result = await apiRequest( + urlPath: '/rewrite/delete', + method: 'post', + server: server, + body: data, + type: 'delete_dns_rewrite_rule' + ); - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return {'result': 'success'}; + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return { 'result': 'success' }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'delete_dns_rewrite_rule', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'], + ) + }; + } } else { - return { - 'result': 'error', - 'log': AppLog( - type: 'save_dhcp_config', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'], - ) - }; + return result; } } - else { - return result; - } -} -Future resetDhcpConfig({ - required Server server, -}) async { - final result = await apiRequest( - urlPath: '/dhcp/reset', - method: 'post', - server: server, - body: {}, - type: 'reset_dhcp_config' - ); + Future addDnsRewriteRule({ + required Map data, + }) async { + final result = await apiRequest( + urlPath: '/rewrite/add', + method: 'post', + server: server, + body: data, + type: 'add_dns_rewrite_rule' + ); - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return {'result': 'success'}; + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return { 'result': 'success' }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'add_dns_rewrite_rule', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'], + ) + }; + } } else { - return { - 'result': 'error', - 'log': AppLog( - type: 'reset_dhcp_config', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'], - ) - }; + return result; } } - else { - return result; - } -} -Future deleteStaticLease({ - required Server server, - required Map data -}) async { - final result = await apiRequest( - urlPath: '/dhcp/remove_static_lease', - method: 'post', - server: server, - body: data, - type: 'remove_static_lease' - ); + Future getQueryLogInfo() async { + final result = await apiRequest( + urlPath: '/querylog/config', + method: 'get', + server: server, + type: 'get_query_log_info' + ); - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return {'result': 'success'}; + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return { + 'result': 'success', + 'data': jsonDecode(result['body']) + }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'get_query_log_info', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'], + ) + }; + } } else { - return { - 'result': 'error', - 'log': AppLog( - type: 'remove_static_lease', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'], - ) - }; + return result; } } - else { - return result; - } -} -Future createStaticLease({ - required Server server, - required Map data -}) async { - final result = await apiRequest( - urlPath: '/dhcp/add_static_lease', - method: 'post', - server: server, - body: data, - type: 'add_static_lease' - ); + Future getQueryLogInfoLegacy() async { + final result = await apiRequest( + urlPath: '/querylog_info', + method: 'get', + server: server, + type: 'get_query_log_info' + ); - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return {'result': 'success'}; - } - else if (result['statusCode'] == 400 && result['body'].contains('static lease already exists')) { - return { - 'result': 'error', - 'message': 'already_exists', - 'log': AppLog( - type: 'add_static_lease', - dateTime: DateTime.now(), - message: 'already_exists', - statusCode: result['statusCode'].toString(), - resBody: result['body'], - ) - }; - } - else if (result['statusCode'] == 400 && result['body'].contains('server is unconfigured')) { - return { - 'result': 'error', - 'message': 'server_not_configured', - 'log': AppLog( - type: 'add_static_lease', - dateTime: DateTime.now(), - message: 'server_not_configured', - statusCode: result['statusCode'].toString(), - resBody: result['body'], - ) - }; + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return { + 'result': 'success', + 'data': jsonDecode(result['body']) + }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'get_query_log_info', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'], + ) + }; + } } else { - return { - 'result': 'error', - 'log': AppLog( - type: 'add_static_lease', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'], - ) - }; + return result; } } - else { - return result; - } -} -Future restoreAllLeases({ - required Server server, -}) async { - final result = await apiRequest( - urlPath: '/dhcp/reset_leases', - method: 'post', - server: server, - body: {}, - type: 'restore_all_leases' - ); + Future updateQueryLogParameters({ + required Map data, + }) async { + final result = await apiRequest( + urlPath: '/querylog/config/update', + method: 'put', + server: server, + body: data, + type: 'update_query_log_config' + ); - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return {'result': 'success'}; + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return { 'result': 'success' }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'update_query_log_config', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'], + ) + }; + } } else { - return { - 'result': 'error', - 'log': AppLog( - type: 'restore_all_leases', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'], - ) - }; + return result; } } - else { - return result; - } -} -Future getDnsRewriteRules({ - required Server server, -}) async { - final result = await apiRequest( - urlPath: '/rewrite/list', - method: 'get', - server: server, - type: 'get_dns_rewrite_rules' - ); + Future updateQueryLogParametersLegacy({ + required Map data, + }) async { + final result = await apiRequest( + urlPath: '/querylog_config', + method: 'post', + server: server, + body: data, + type: 'update_query_log_config' + ); - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - final List data = List.from( - jsonDecode(result['body']).map((item) => RewriteRulesData.fromJson(item)) - ); - - return { - 'result': 'success', - 'data': data - }; + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return { 'result': 'success' }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'update_query_log_config', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'], + ) + }; + } } else { - return { - 'result': 'error', - 'log': AppLog( - type: 'get_dns_rewrite_rules', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'], - ) - }; + return result; } } - else { - return result; - } -} -Future deleteDnsRewriteRule({ - required Server server, - required Map data, -}) async { - final result = await apiRequest( - urlPath: '/rewrite/delete', - method: 'post', - server: server, - body: data, - type: 'delete_dns_rewrite_rule' - ); + Future clearLogs() async { + final result = await apiRequest( + urlPath: '/querylog_clear', + method: 'post', + server: server, + body: {}, + type: 'clear_query_logs' + ); - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return { 'result': 'success' }; + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return { 'result': 'success' }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'clear_query_logs', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'], + ) + }; + } } else { - return { - 'result': 'error', - 'log': AppLog( - type: 'delete_dns_rewrite_rule', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'], - ) - }; + return result; } } - else { - return result; - } -} -Future addDnsRewriteRule({ - required Server server, - required Map data, -}) async { - final result = await apiRequest( - urlPath: '/rewrite/add', - method: 'post', - server: server, - body: data, - type: 'add_dns_rewrite_rule' - ); + Future getDnsInfo() async { + final result = await apiRequest( + urlPath: '/dns_info', + method: 'get', + server: server, + type: 'get_dns_info' + ); - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return { 'result': 'success' }; + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return { + 'result': 'success' , + 'data': DnsInfo.fromJson(jsonDecode(result['body'])) + }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'get_dns_info', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'], + ) + }; + } } else { - return { - 'result': 'error', - 'log': AppLog( - type: 'add_dns_rewrite_rule', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'], - ) - }; + return result; } } - else { - return result; - } -} -Future getQueryLogInfo({ - required Server server, -}) async { - final result = await apiRequest( - urlPath: '/querylog_info', - method: 'get', - server: server, - type: 'get_query_log_info' - ); + Future setDnsConfig({ + required Map data, + }) async { + final result = await apiRequest( + urlPath: '/dns_config', + method: 'post', + server: server, + body: data, + type: 'set_dns_config' + ); - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return { - 'result': 'success', - 'data': jsonDecode(result['body']) - }; + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return { 'result': 'success' }; + } + if (result['statusCode'] == 400) { + return { + 'result': 'error', + 'log': AppLog( + type: 'set_dns_config', + dateTime: DateTime.now(), + message: 'data_not_valid', + statusCode: result['statusCode'].toString(), + resBody: result['body'], + ) + }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'set_dns_config', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'], + ) + }; + } } else { - return { - 'result': 'error', - 'log': AppLog( - type: 'get_query_log_info', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'], - ) - }; + return result; } } - else { - return result; - } -} -Future updateQueryLogParameters({ - required Server server, - required Map data, -}) async { - final result = await apiRequest( - urlPath: '/querylog_config', - method: 'post', - server: server, - body: data, - type: 'update_query_log_config' - ); + Future getEncryptionSettings() async { + final result = await apiRequest( + urlPath: '/tls/status', + method: 'get', + server: server, + type: 'get_encryption_settings' + ); - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return { 'result': 'success' }; + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return { + 'result': 'success', + 'data': EncryptionData.fromJson(jsonDecode(result['body'])) + }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'get_encryption_settings', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'], + ) + }; + } } else { - return { - 'result': 'error', - 'log': AppLog( - type: 'update_query_log_config', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'], - ) - }; + return result; } } - else { - return result; - } -} -Future clearLogs({ - required Server server, -}) async { - final result = await apiRequest( - urlPath: '/querylog_clear', - method: 'post', - server: server, - body: {}, - type: 'clear_query_logs' - ); + Future getBlockedServices() async { + final result = await apiRequest( + urlPath: '/blocked_services/all', + method: 'get', + server: server, + type: 'get_blocked_services' + ); - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return { 'result': 'success' }; + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return { + 'result': 'success', + 'data': List.from( + BlockedServicesFromApi.fromJson(jsonDecode(result['body'])).blockedServices + ) + }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'get_encryption_settings', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'], + ) + }; + } } else { - return { - 'result': 'error', - 'log': AppLog( - type: 'clear_query_logs', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'], - ) - }; + return result; } } - else { - return result; - } -} -Future getDnsInfo({ - required Server server, -}) async { - final result = await apiRequest( - urlPath: '/dns_info', - method: 'get', - server: server, - type: 'get_dns_info' - ); + Future checkEncryptionSettings({ + required Map data, + }) async { + final result = await apiRequest( + urlPath: '/tls/validate', + method: 'post', + server: server, + body: data, + type: 'check_encryption_settings' + ); - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return { - 'result': 'success' , - 'data': DnsInfoData.fromJson(jsonDecode(result['body'])) - }; + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return { + 'result': 'success', + 'data': jsonDecode(result['body']) + }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'check_encryption_settings', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'], + ) + }; + } } else { - return { - 'result': 'error', - 'log': AppLog( - type: 'get_dns_info', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'], - ) - }; + return result; } } - else { - return result; - } -} -Future setDnsConfig({ - required Server server, - required Map data, -}) async { - final result = await apiRequest( - urlPath: '/dns_config', - method: 'post', - server: server, - body: data, - type: 'set_dns_config' - ); + Future saveEncryptionSettings({ + required Map data, + }) async { + final result = await apiRequest( + urlPath: '/tls/configure', + method: 'post', + server: server, + body: data, + type: 'update_encryption_settings' + ); - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return { 'result': 'success' }; - } - if (result['statusCode'] == 400) { - return { - 'result': 'error', - 'log': AppLog( - type: 'set_dns_config', - dateTime: DateTime.now(), - message: 'data_not_valid', - statusCode: result['statusCode'].toString(), - resBody: result['body'], - ) - }; + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return { 'result': 'success' }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'update_encryption_settings', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'], + ) + }; + } } else { - return { - 'result': 'error', - 'log': AppLog( - type: 'set_dns_config', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'], - ) - }; + return result; } } - else { - return result; - } -} -Future getEncryptionSettings({ - required Server server, -}) async { - final result = await apiRequest( - urlPath: '/tls/status', - method: 'get', - server: server, - type: 'get_encryption_settings' - ); + Future resetDnsCache() async { + final result = await apiRequest( + urlPath: '/cache_clear', + method: 'post', + server: server, + type: 'clear_dns_cache' + ); - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return { - 'result': 'success', - 'data': EncryptionData.fromJson(jsonDecode(result['body'])) - }; + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return { 'result': 'success' }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'clear_dns_cache', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'], + ) + }; + } } else { + return result; + } + } + + Future checkServerUpdates() async { + try { + final result = await Future.wait([ + apiRequest( + urlPath: '/version.json', + method: 'post', + server: server, + type: 'check_server_updates', + body: { + "recheck_now": true + } + ), + apiRequest( + urlPath: '/status', + method: 'get', + server: server, + type: 'check_server_updates', + ), + ]); + + if (result[0]['hasResponse'] == true && result[1]['hasResponse'] == true) { + if (result[0]['statusCode'] == 200 && result[1]['statusCode'] == 200) { + final Map obj = { + ...jsonDecode(result[0]['body']), + 'current_version': ServerInfoData.fromJson(jsonDecode(result[1]['body'])).version + }; + return { + 'result': 'success', + 'data': obj + }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'get_filtering_status', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result.map((res) => res['statusCode'] ?? 'null').toString(), + resBody: result.map((res) => res['body'] ?? 'null').toString(), + ) + }; + } + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'get_filtering_status', + dateTime: DateTime.now(), + message: 'no_response', + statusCode: result.map((res) => res['statusCode'] ?? 'null').toString(), + resBody: result.map((res) => res['body'] ?? 'null').toString(), + ) + }; + } + } catch (e) { + Sentry.captureException(e); return { 'result': 'error', 'log': AppLog( - type: 'get_encryption_settings', + type: 'get_filtering_status', dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'], + message: 'no_response', + resBody: e.toString(), ) }; } } - else { - return result; - } -} -Future getBlockedServices({ - required Server server, -}) async { - final result = await apiRequest( - urlPath: '/blocked_services/all', - method: 'get', - server: server, - type: 'get_blocked_services' - ); + Future requestUpdateServer() async { + final result = await apiRequest( + urlPath: '/update', + method: 'post', + server: server, + type: 'update_server' + ); - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return { - 'result': 'success', - 'data': List.from( - BlockedServicesFromApi.fromJson(jsonDecode(result['body'])).blockedServices - ) - }; + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return { 'result': 'success' }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'update_server', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'], + ) + }; + } } else { - return { - 'result': 'error', - 'log': AppLog( - type: 'get_encryption_settings', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'], - ) - }; + return result; } } - else { - return result; - } -} -Future checkEncryptionSettings({ - required Server server, - required Map data, -}) async { - final result = await apiRequest( - urlPath: '/tls/validate', - method: 'post', - server: server, - body: data, - type: 'check_encryption_settings' - ); + Future updateSafeSearchSettings({ + required Map body + }) async { + final result = await apiRequest( + urlPath: '/safesearch/settings', + method: 'put', + server: server, + type: 'safesearch_settings', + body: body + ); - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return { - 'result': 'success', - 'data': jsonDecode(result['body']) - }; + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return { 'result': 'success' }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'safesearch_settings', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'], + ) + }; + } } else { - return { - 'result': 'error', - 'log': AppLog( - type: 'check_encryption_settings', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'], - ) - }; + return result; } } - else { - return result; - } -} -Future saveEncryptionSettings({ - required Server server, - required Map data, -}) async { - final result = await apiRequest( - urlPath: '/tls/configure', - method: 'post', - server: server, - body: data, - type: 'update_encryption_settings' - ); + Future updateRewriteRule({ + required Map body + }) async { + final result = await apiRequest( + urlPath: '/rewrite/update', + method: 'put', + server: server, + type: 'update_rewrite', + body: body + ); - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return { 'result': 'success' }; + if (result['hasResponse'] == true) { + if (result['statusCode'] == 200) { + return { 'result': 'success' }; + } + else { + return { + 'result': 'error', + 'log': AppLog( + type: 'update_rewrite', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + statusCode: result['statusCode'].toString(), + resBody: result['body'], + ) + }; + } } else { - return { - 'result': 'error', - 'log': AppLog( - type: 'update_encryption_settings', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'], - ) - }; + return result; } } - else { - return result; - } -} - -Future resetDnsCache({ - required Server server, -}) async { - final result = await apiRequest( - urlPath: '/cache_clear', - method: 'post', - server: server, - type: 'clear_dns_cache' - ); - - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200) { - return { 'result': 'success' }; - } - else { - return { - 'result': 'error', - 'log': AppLog( - type: 'clear_dns_cache', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'], - ) - }; - } - } - else { - return result; - } -} - -Future checkAppUpdatesGitHub() async { - try { - HttpClient httpClient = HttpClient(); - HttpClientRequest request = await httpClient.getUrl(Uri.parse(Urls.checkLatestReleaseUrl)); - HttpClientResponse response = await request.close(); - String reply = await response.transform(utf8.decoder).join(); - httpClient.close(); - if (response.statusCode == 200) { - return { - 'result': 'success', - 'hasResponse': true, - 'error': false, - 'statusCode': response.statusCode, - 'body': GitHubRelease.fromJson(jsonDecode(reply)) - }; - } - else { - return { - 'result': 'error', - 'log': AppLog( - type: 'update_encryption_settings', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: response.statusCode.toString(), - resBody: reply, - ) - }; - } - } on SocketException { - return { - 'result': 'no_connection', - 'message': 'SocketException', - 'log': AppLog( - type: 'check_latest_release_github', - dateTime: DateTime.now(), - message: 'SocketException' - ) - }; - } on TimeoutException { - return { - 'result': 'no_connection', - 'message': 'TimeoutException', - 'log': AppLog( - type: 'check_latest_release_github', - dateTime: DateTime.now(), - message: 'TimeoutException' - ) - }; - } on HandshakeException { - return { - 'result': 'ssl_error', - 'message': 'HandshakeException', - 'log': AppLog( - type: 'check_latest_release_github', - dateTime: DateTime.now(), - message: 'HandshakeException' - ) - }; - } catch (e) { - return { - 'result': 'error', - 'message': e.toString(), - 'log': AppLog( - type: 'check_latest_release_github', - dateTime: DateTime.now(), - message: e.toString() - ) - }; - } } \ No newline at end of file diff --git a/lib/widgets/add_server/add_server_functions.dart b/lib/widgets/add_server/add_server_functions.dart new file mode 100644 index 0000000..bbb88fd --- /dev/null +++ b/lib/widgets/add_server/add_server_functions.dart @@ -0,0 +1,118 @@ +import 'package:adguard_home_manager/constants/regexps.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/models/server.dart'; +import 'package:adguard_home_manager/widgets/add_server/add_server_modal.dart'; +import 'package:adguard_home_manager/widgets/version_warning_modal.dart'; + +bool checkDataValid({ + required TextEditingController nameController, + required TextEditingController ipDomainController, + required String? ipDomainError, + required String? pathError, + required String? portError, +}) { + if ( + nameController.text != '' && + ipDomainController.text != '' && + ipDomainError == null && + pathError == null && + portError == null + ) { + return true; + } + else { + return false; + } +} + + +String? validatePort({ + required String? value, + required BuildContext context +}) { + if (value != null && value != '') { + if (int.tryParse(value) != null && int.parse(value) <= 65535) { + return null; + } + else { + return AppLocalizations.of(context)!.invalidPort; + } + } + else { + return null; + } +} + +String? validateSubroute({ + required BuildContext context, + required String? value +}) { + if (value != null && value != '') { + if (Regexps.subroute.hasMatch(value) == true) { + return null; + } + else { + return AppLocalizations.of(context)!.invalidPath; + } + } + else { + return null; + } +} + +String? validateAddress({ + required BuildContext context, + required String? value +}) { + if (value != null && value != '') { + if (Regexps.ipv4Address.hasMatch(value) == true || Regexps.domain.hasMatch(value) == true) { + return null; + } + else { + return AppLocalizations.of(context)!.invalidIpDomain; + } + } + else { + return AppLocalizations.of(context)!.ipDomainNotEmpty; + } +} + +void openServerFormModal({ + required BuildContext context, + required double width, + Server? server, +}) { + showGeneralDialog( + context: context, + barrierColor: width <= 700 + ?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) => AddServerModal( + fullScreen: width <= 700, + server: server, + onUnsupportedVersion: (version) => showDialog( + context: context, + builder: (ctx) => VersionWarningModal( + version: version + ), + barrierDismissible: false + ), + ), + ); +} \ No newline at end of file diff --git a/lib/widgets/add_server/add_server_modal.dart b/lib/widgets/add_server/add_server_modal.dart new file mode 100644 index 0000000..a7fc768 --- /dev/null +++ b/lib/widgets/add_server/add_server_modal.dart @@ -0,0 +1,635 @@ +// ignore_for_file: use_build_context_synchronously +import 'package:provider/provider.dart'; +import 'package:flutter/material.dart'; +import 'package:segmented_button_slide/segmented_button_slide.dart'; +import 'package:uuid/uuid.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/widgets/add_server/unsupported_version_modal.dart'; +import 'package:adguard_home_manager/widgets/add_server/form_text_field.dart'; +import 'package:adguard_home_manager/widgets/section_label.dart'; +import 'package:adguard_home_manager/widgets/custom_switch_list_tile.dart'; +import 'package:adguard_home_manager/widgets/add_server/add_server_functions.dart'; + +import 'package:adguard_home_manager/config/minimum_server_version.dart'; +import 'package:adguard_home_manager/models/server_status.dart'; +import 'package:adguard_home_manager/functions/compare_versions.dart'; +import 'package:adguard_home_manager/services/auth.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; +import 'package:adguard_home_manager/services/api_client.dart'; +import 'package:adguard_home_manager/functions/snackbar.dart'; +import 'package:adguard_home_manager/constants/urls.dart'; +import 'package:adguard_home_manager/functions/open_url.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/providers/status_provider.dart'; +import 'package:adguard_home_manager/functions/base64.dart'; +import 'package:adguard_home_manager/models/app_log.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; +import 'package:adguard_home_manager/models/server.dart'; + +enum ConnectionType { http, https} + +class AddServerModal extends StatefulWidget { + final Server? server; + final bool fullScreen; + final void Function(String version) onUnsupportedVersion; + + const AddServerModal({ + super.key, + this.server, + required this.fullScreen, + required this.onUnsupportedVersion + }); + + @override + State createState() => _AddServerModalState(); +} + +class _AddServerModalState extends State { + final uuid = const Uuid(); + + final TextEditingController nameController = TextEditingController(); + String? nameError; + + ConnectionType connectionType = ConnectionType.http; + + final TextEditingController ipDomainController = TextEditingController(); + String? ipDomainError; + + final TextEditingController pathController = TextEditingController(); + String? pathError; + + final TextEditingController portController = TextEditingController(); + String? portError; + + final TextEditingController userController = TextEditingController(); + + final TextEditingController passwordController = TextEditingController(); + + bool defaultServer = false; + + bool homeAssistant = false; + + bool allDataValid = false; + + bool isConnecting = false; + + @override + void initState() { + if (widget.server != null) { + nameController.text = widget.server!.name; + connectionType = widget.server!.connectionMethod == 'https' ? ConnectionType.https : ConnectionType.http; + ipDomainController.text = widget.server!.domain; + pathController.text = widget.server!.path ?? ''; + portController.text = widget.server!.port != null ? widget.server!.port.toString() : ""; + userController.text = widget.server!.user ?? ""; + passwordController.text = widget.server!.password ?? ""; + defaultServer = widget.server!.defaultServer; + homeAssistant = widget.server!.runningOnHa; + } + setState(() => allDataValid = checkDataValid( + ipDomainController: ipDomainController, + nameController: nameController, + ipDomainError: ipDomainError, + pathError: pathError, + portError: portError + )); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final serversProvider = Provider.of(context, listen: false); + final statusProvider = Provider.of(context, listen: false); + final appConfigProvider = Provider.of(context, listen: false); + + void cancelConnecting() { + if (mounted) { + setState(() => isConnecting = false); + } + else { + isConnecting = false; + } + } + + void validateData() { + setState(() => allDataValid = checkDataValid( + ipDomainController: ipDomainController, + nameController: nameController, + ipDomainError: ipDomainError, + pathError: pathError, + portError: portError + )); + } + + String getErrorMessage(AuthStatus status) { + if (status == AuthStatus.invalidCredentials) return AppLocalizations.of(context)!.invalidUsernamePassword; + if (status == AuthStatus.manyAttepts) return AppLocalizations.of(context)!.tooManyAttempts; + if (status == AuthStatus.socketException || status == AuthStatus.timeoutException) return AppLocalizations.of(context)!.cantReachServer; + if (status == AuthStatus.serverError) return AppLocalizations.of(context)!.serverError; + if (status == AuthStatus.handshakeException) return AppLocalizations.of(context)!.sslError; + return AppLocalizations.of(context)!.unknownError; + } + + void connect() async { + setState(() => isConnecting = true); + + Server serverObj = Server( + id: uuid.v4(), + name: nameController.text, + connectionMethod: connectionType.name, + domain: ipDomainController.text, + port: portController.text != '' ? int.parse(portController.text) : null, + user: userController.text != "" ? userController.text : null, + password: passwordController.text != "" ? passwordController.text : null, + path: pathController.text != "" ? pathController.text : null, + defaultServer: defaultServer, + authToken: homeAssistant == true + ? encodeBase64UserPass(userController.text, passwordController.text) + : null, + runningOnHa: homeAssistant + ); + + final result = homeAssistant == true + ? await ServerAuth.loginHA(serverObj) + : await ServerAuth.login(serverObj); + + // If something goes wrong with the connection + if (result != AuthStatus.success) { + cancelConnecting(); + if (mounted) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: getErrorMessage(result), + color: Colors.red + ); + } + return; + } + + if (serverObj.user != null && serverObj.password != null) { + serverObj.authToken = encodeBase64UserPass(serverObj.user!, serverObj.password!); + } + + statusProvider.setServerStatusLoad(LoadStatus.loading); + final ApiClientV2 apiClient2 = ApiClientV2(server: serverObj); + final serverStatus = await apiClient2.getServerStatus(); + + if (!context.mounted) return; + + // If something goes wrong when fetching server status + if (serverStatus.successful == false) { + statusProvider.setServerStatusLoad(LoadStatus.error); + Navigator.pop(context); + return; + } + + final status = serverStatus.content as ServerStatus; + + // Check if ths server version is compatible + final validVersion = serverVersionIsAhead( + currentVersion: status.serverVersion, + referenceVersion: MinimumServerVersion.stable, + referenceVersionBeta: MinimumServerVersion.beta + ); + if (validVersion == false) { + showDialog( + context: context, + builder: (ctx) => UnsupportedVersionModal( + serverVersion: status.serverVersion, + onClose: () => Navigator.pop(context) + ) + ); + return; + } + + final serverCreated = await serversProvider.createServer(serverObj); + + if (!context.mounted) return; + + // If something goes wrong when saving the connection on the db + if (serverCreated != null) { + setState(() => isConnecting = false); + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.connectionNotCreated, + color: Colors.red + ); + return; + } + + // If everything is successful + statusProvider.setServerStatusData( + data: status + ); + serversProvider.setApiClient2(apiClient2); + statusProvider.setServerStatusLoad(LoadStatus.loaded); + if (status.serverVersion.contains('a') || status.serverVersion.contains('b')) { + Navigator.pop(context); + widget.onUnsupportedVersion(status.serverVersion); + } + else { + Navigator.pop(context); + } + } + + void edit() async { + setState(() => isConnecting = true); + + final Server serverObj = Server( + id: widget.server!.id, + name: nameController.text, + connectionMethod: connectionType.name, + domain: ipDomainController.text, + port: portController.text != '' ? int.parse(portController.text) : null, + user: userController.text != "" ? userController.text : null, + password: passwordController.text != "" ? passwordController.text : null, + defaultServer: defaultServer, + authToken: homeAssistant == true + ? encodeBase64UserPass(userController.text, passwordController.text) + : null, + runningOnHa: homeAssistant + ); + + final result = homeAssistant == true + ? await ServerAuth.loginHA(serverObj) + : await ServerAuth.login(serverObj); + + // If something goes wrong with the connection + if (result != AuthStatus.success) { + cancelConnecting(); + if (mounted) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: getErrorMessage(result), + color: Colors.red + ); + } + return; + } + + if (serverObj.user != null && serverObj.password != null) { + serverObj.authToken = encodeBase64UserPass(serverObj.user!, serverObj.password!); + } + + final ApiClientV2 apiClient2 = ApiClientV2(server: serverObj); + final version = await apiClient2.getServerVersion(); + if (version.successful == false) { + if (mounted) setState(() => isConnecting = false); + return; + } + + // Check if ths server version is compatible + final validVersion = serverVersionIsAhead( + currentVersion: version.content, + referenceVersion: MinimumServerVersion.stable, + referenceVersionBeta: MinimumServerVersion.beta + ); + if (validVersion == false) { + showDialog( + context: context, + builder: (ctx) => UnsupportedVersionModal( + serverVersion: version.content, + onClose: () => Navigator.pop(context) + ) + ); + return; + } + + final serverSaved = await serversProvider.editServer(serverObj); + + if (!mounted) return; + + // If something goes wrong when saving the connection on the db + if (serverSaved != null) { + setState(() => isConnecting = false); + appConfigProvider.addLog( + AppLog( + type: 'save_connection_db', + dateTime: DateTime.now(), + message: serverSaved.toString() + ) + ); + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.connectionNotCreated, + color: Colors.red + ); + return; + } + + // If everything is successful + if ( + version.successful == true && + (version.content.contains('a') || version.content.contains('b')) // alpha or beta + ) { + Navigator.pop(context); + widget.onUnsupportedVersion(version.content); + } + else { + Navigator.pop(context); + } + } + + Widget actions() { + return Row( + children: [ + IconButton( + onPressed: () => openUrl(Urls.connectionInstructions), + icon: const Icon(Icons.help_outline_outlined) + ), + IconButton( + tooltip: widget.server == null + ? AppLocalizations.of(context)!.connect + : AppLocalizations.of(context)!.save, + onPressed: allDataValid == true && isConnecting == false + ? widget.server == null + ? () => connect() + : () => edit() + : null, + icon: isConnecting + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator() + ) + : Icon( + widget.server == null + ? Icons.login_rounded + : Icons.save_rounded + ) + ), + ], + ); + } + + List form() { + return [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + margin: const EdgeInsets.only( + top: 24, + left: 24, + right: 24 + ), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary.withOpacity(0.05), + borderRadius: BorderRadius.circular(30), + border: Border.all( + color: Theme.of(context).colorScheme.primary + ) + ), + child: Text( + "${connectionType.name}://${ipDomainController.text}${portController.text != '' ? ':${portController.text}' : ""}${pathController.text}", + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.w500 + ), + ), + ), + SectionLabel( + label: AppLocalizations.of(context)!.general, + padding: const EdgeInsets.all(24), + ), + FormTextField( + label: AppLocalizations.of(context)!.name, + controller: nameController, + icon: Icons.badge_rounded, + error: nameError, + onChanged: (value) { + if (value != '') { + setState(() => nameError = null); + } + else { + setState(() => nameError = AppLocalizations.of(context)!.nameNotEmpty); + } + validateData(); + }, + isConnecting: isConnecting, + ), + SectionLabel( + label: AppLocalizations.of(context)!.connection, + padding: const EdgeInsets.all(24), + ), + SegmentedButtonSlide( + entries: const [ + SegmentedButtonSlideEntry(label: "HTTP"), + SegmentedButtonSlideEntry(label: "HTTPS"), + ], + selectedEntry: connectionType.index, + onChange: (v) => setState(() => connectionType = ConnectionType.values[v]), + colors: SegmentedButtonSlideColors( + barColor: Theme.of(context).colorScheme.primary.withOpacity(0.2), + backgroundSelectedColor: Theme.of(context).colorScheme.primary, + ), + textOverflow: TextOverflow.ellipsis, + height: 40, + margin: const EdgeInsets.symmetric( + horizontal: 24, + ), + selectedTextStyle: TextStyle( + color: Theme.of(context).colorScheme.onPrimary, + fontWeight: FontWeight.w700 + ), + unselectedTextStyle: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + hoverTextStyle: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + Card( + margin: const EdgeInsets.only( + top: 16, left: 24, right: 24 + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Icon( + Icons.warning_rounded, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 16), + Flexible(child: Text(AppLocalizations.of(context)!.redirectHttpsWarning)) + ], + ), + ), + ), + if (connectionType == ConnectionType.https) Card( + margin: const EdgeInsets.only( + top: 16, left: 24, right: 24 + ), + 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)!.sslWarning)) + ], + ), + ), + ), + const SizedBox(height: 30), + FormTextField( + label: AppLocalizations.of(context)!.ipDomain, + controller: ipDomainController, + icon: Icons.link_rounded, + error: ipDomainError, + keyboardType: TextInputType.url, + onChanged: (v) { + setState(() => ipDomainError = validateAddress(context: context, value: v)); + validateData(); + }, + isConnecting: isConnecting, + ), + const SizedBox(height: 20), + FormTextField( + label: AppLocalizations.of(context)!.path, + controller: pathController, + icon: Icons.route_rounded, + error: pathError, + onChanged: (v) { + setState(() => pathError = validateSubroute(context: context, value: v)); + validateData(); + }, + hintText: AppLocalizations.of(context)!.examplePath, + helperText: AppLocalizations.of(context)!.helperPath, + isConnecting: isConnecting, + ), + const SizedBox(height: 20), + FormTextField( + label: AppLocalizations.of(context)!.port, + controller: portController, + icon: Icons.numbers_rounded, + error: portError, + keyboardType: TextInputType.number, + onChanged: (v) { + setState(() => portError = validatePort(context: context, value: v)); + validateData(); + }, + isConnecting: isConnecting, + ), + SectionLabel( + label: AppLocalizations.of(context)!.authentication, + padding: const EdgeInsets.all(24), + ), + FormTextField( + label: AppLocalizations.of(context)!.username, + controller: userController, + icon: Icons.person_rounded, + isConnecting: isConnecting, + ), + const SizedBox(height: 20), + FormTextField( + label: AppLocalizations.of(context)!.password, + controller: passwordController, + icon: Icons.lock_rounded, + keyboardType: TextInputType.visiblePassword, + obscureText: true, + isConnecting: isConnecting, + ), + SectionLabel( + label: AppLocalizations.of(context)!.other, + padding: const EdgeInsets.only( + top: 32, + left: 24, + bottom: 12 + ), + ), + CustomSwitchListTile( + value: defaultServer, + onChanged: (value) => setState(() => defaultServer = value), + title: AppLocalizations.of(context)!.defaultServer, + disabled: widget.server != null || isConnecting, + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 4 + ), + ), + CustomSwitchListTile( + value: homeAssistant, + onChanged: (value) => setState(() => homeAssistant = value), + title: AppLocalizations.of(context)!.runningHomeAssistant, + disabled: widget.server != null || isConnecting, + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 4 + ), + ), + const SizedBox(height: 20), + ]; + } + + if (widget.fullScreen == true) { + return Dialog.fullscreen( + child: Scaffold( + appBar: AppBar( + leading: CloseButton( + onPressed: () => Navigator.pop(context), + ), + title: widget.server == null + ? Text(AppLocalizations.of(context)!.createConnection) + : Text(AppLocalizations.of(context)!.editConnection), + actions: [ + actions(), + const SizedBox(width: 8) + ], + ), + body: SafeArea( + child: ListView( + children: form() + ), + ), + ), + ); + } + else { + return Dialog( + child: SizedBox( + width: 400, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.clear_rounded) + ), + const SizedBox(width: 8), + Text( + widget.server == null + ? AppLocalizations.of(context)!.createConnection + : AppLocalizations.of(context)!.editConnection, + style: const TextStyle( + fontSize: 20 + ), + ), + ], + ), + actions() + ], + ), + ), + Expanded( + child: ListView( + children: form() + ), + ) + ], + ), + ), + ); + } + } +} \ No newline at end of file diff --git a/lib/widgets/add_server/form_text_field.dart b/lib/widgets/add_server/form_text_field.dart new file mode 100644 index 0000000..72d410b --- /dev/null +++ b/lib/widgets/add_server/form_text_field.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; + +class FormTextField extends StatelessWidget { + final TextEditingController controller; + final String label; + final String? error; + final IconData icon; + final TextInputType? keyboardType; + final Function(String)? onChanged; + final bool? obscureText; + final String? hintText; + final String? helperText; + final bool isConnecting; + + const FormTextField({ + super.key, + required this.label, + required this.controller, + this.error, + required this.icon, + this.keyboardType, + this.onChanged, + this.obscureText, + this.hintText, + this.helperText, + required this.isConnecting + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: TextFormField( + controller: controller, + onChanged: onChanged, + obscureText: obscureText ?? false, + enabled: !isConnecting, + decoration: InputDecoration( + prefixIcon: Icon(icon), + errorText: error, + hintText: hintText, + helperText: helperText, + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + labelText: label, + ), + keyboardType: keyboardType, + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/add_server/unsupported_version_modal.dart b/lib/widgets/add_server/unsupported_version_modal.dart new file mode 100644 index 0000000..3340aad --- /dev/null +++ b/lib/widgets/add_server/unsupported_version_modal.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/config/minimum_server_version.dart'; + +class UnsupportedVersionModal extends StatelessWidget { + final String serverVersion; + final void Function() onClose; + + const UnsupportedVersionModal({ + super.key, + required this.serverVersion, + required this.onClose, + }); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Column( + children: [ + Icon( + Icons.error_rounded, + size: 24, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + const SizedBox(height: 16), + Text( + AppLocalizations.of(context)!.unsupportedServerVersion, + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface + ), + ) + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + AppLocalizations.of(context)!.unsupportedServerVersionMessage, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ), + const SizedBox(height: 16), + Text( + AppLocalizations.of(context)!.yourVersion(serverVersion), + style: const TextStyle( + fontStyle: FontStyle.italic + ), + ), + const SizedBox(height: 4), + Text( + AppLocalizations.of(context)!.minimumRequiredVersion( + serverVersion.contains("b") + ? MinimumServerVersion.beta + : MinimumServerVersion.stable + ), + style: const TextStyle( + fontStyle: FontStyle.italic + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + onClose(); + }, + child: Text(AppLocalizations.of(context)!.close) + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/widgets/add_server_modal.dart b/lib/widgets/add_server_modal.dart deleted file mode 100644 index e6a7c75..0000000 --- a/lib/widgets/add_server_modal.dart +++ /dev/null @@ -1,688 +0,0 @@ -// ignore_for_file: use_build_context_synchronously - -import 'package:provider/provider.dart'; -import 'package:flutter/material.dart'; -import 'package:uuid/uuid.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/functions/encode_base64.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; -import 'package:adguard_home_manager/models/app_log.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; -import 'package:adguard_home_manager/models/server.dart'; - -enum ConnectionType { http, https} - -class AddServerModal extends StatefulWidget { - final Server? server; - - const AddServerModal({ - Key? key, - this.server, - }) : super(key: key); - - @override - State createState() => _AddServerModalState(); -} - -class _AddServerModalState extends State { - final uuid = const Uuid(); - - final TextEditingController nameController = TextEditingController(); - String? nameError; - - ConnectionType connectionType = ConnectionType.http; - - final TextEditingController ipDomainController = TextEditingController(); - String? ipDomainError; - - final TextEditingController pathController = TextEditingController(); - String? pathError; - - final TextEditingController portController = TextEditingController(); - String? portError; - - final TextEditingController userController = TextEditingController(); - String? userError; - - final TextEditingController passwordController = TextEditingController(); - String? passwordError; - - bool defaultServer = false; - - bool homeAssistant = false; - - bool allDataValid = false; - - bool isConnecting = false; - - Widget sectionLabel(String label) { - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 24 - ), - child: Text( - label, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.primary - ), - ), - ); - } - - Widget textField({ - required String label, - required TextEditingController controller, - String? error, - required IconData icon, - TextInputType? keyboardType, - Function(String)? onChanged, - bool? obscureText, - String? hintText, - String? helperText - }) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: TextFormField( - controller: controller, - onChanged: onChanged, - obscureText: obscureText ?? false, - decoration: InputDecoration( - prefixIcon: Icon(icon), - errorText: error, - hintText: hintText, - helperText: helperText, - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - labelText: label, - ), - keyboardType: keyboardType, - ), - ); - } - - void checkDataValid() { - if ( - nameController.text != '' && - ipDomainController.text != '' && - ipDomainError == null && - pathError == null && - portError == null && - userController.text != '' && - passwordController.text != '' - ) { - setState(() { - allDataValid = true; - }); - } - else { - setState(() { - allDataValid = false; - }); - } - } - - - void validatePort(String? value) { - if (value != null && value != '') { - if (int.tryParse(value) != null && int.parse(value) <= 65535) { - setState(() { - portError = null; - }); - } - else { - setState(() { - portError = AppLocalizations.of(context)!.invalidPort; - }); - } - } - else { - setState(() { - portError = null; - }); - } - checkDataValid(); - } - - void validateSubroute(String? value) { - if (value != null && value != '') { - RegExp subrouteRegexp = RegExp(r'^\/\b([A-Za-z0-9_\-~/]*)[^\/|\.|\:]$'); - if (subrouteRegexp.hasMatch(value) == true) { - setState(() { - pathError = null; - }); - } - else { - setState(() { - pathError = AppLocalizations.of(context)!.invalidPath; - }); - } - } - else { - setState(() { - pathError = null; - }); - } - checkDataValid(); - } - - void validateAddress(String? value) { - if (value != null && value != '') { - RegExp ipAddress = RegExp(r'^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$'); - RegExp domain = RegExp(r'^([a-z0-9|-]+\.)*[a-z0-9|-]+\.[a-z]+$'); - if (ipAddress.hasMatch(value) == true || domain.hasMatch(value) == true) { - setState(() { - ipDomainError = null; - }); - } - else { - setState(() { - ipDomainError = AppLocalizations.of(context)!.invalidIpDomain; - }); - } - } - else { - setState(() { - ipDomainError = AppLocalizations.of(context)!.ipDomainNotEmpty; - }); - } - checkDataValid(); - } - - void validateUser(String? value) { - if (value != null && value != '') { - setState(() { - userError = null; - }); - } - else { - setState(() { - userError = AppLocalizations.of(context)!.userNotEmpty; - }); - } - checkDataValid(); - } - - void validatePassword(String? value) { - if (value != null && value != '') { - setState(() { - passwordError = null; - }); - } - else { - setState(() { - passwordError = AppLocalizations.of(context)!.passwordNotEmpty; - }); - } - checkDataValid(); - } - - @override - void initState() { - if (widget.server != null) { - nameController.text = widget.server!.name; - connectionType = widget.server!.connectionMethod == 'https' ? ConnectionType.https : ConnectionType.http; - ipDomainController.text = widget.server!.domain; - pathController.text = widget.server!.path ?? ''; - portController.text = widget.server!.port != null ? widget.server!.port.toString() : ""; - userController.text = widget.server!.user; - passwordController.text = widget.server!.password; - defaultServer = widget.server!.defaultServer; - homeAssistant = widget.server!.runningOnHa; - } - checkDataValid(); - super.initState(); - } - - @override - Widget build(BuildContext context) { - final serversProvider = Provider.of(context, listen: false); - final appConfigProvider = Provider.of(context, listen: false); - - final mediaQuery = MediaQuery.of(context); - - void connect() async { - Server serverObj = Server( - id: uuid.v4(), - name: nameController.text, - connectionMethod: connectionType.name, - domain: ipDomainController.text, - port: portController.text != '' ? int.parse(portController.text) : null, - user: userController.text, - password: passwordController.text, - defaultServer: defaultServer, - authToken: homeAssistant == true - ? encodeBase64UserPass(userController.text, passwordController.text) - : '', - runningOnHa: homeAssistant - ); - setState(() => isConnecting = true); - - final result = homeAssistant == true - ? await loginHA(serverObj) - : await login(serverObj); - - setState(() => isConnecting = false); - - if (result['result'] == 'success') { - serverObj.authToken = encodeBase64UserPass(serverObj.user, serverObj.password); - final serverCreated = await serversProvider.createServer(serverObj); - if (serverCreated == null) { - serversProvider.setServerStatusLoad(0); - final serverStatus = await getServerStatus(serverObj); - if (serverStatus['result'] == 'success') { - serversProvider.setServerStatusData(serverStatus['data']); - serversProvider.setServerStatusLoad(1); - } - else { - appConfigProvider.addLog(serverStatus['log']); - serversProvider.setServerStatusLoad(2); - } - Navigator.pop(context); - } - else { - appConfigProvider.addLog( - AppLog( - type: 'save_connection_db', - dateTime: DateTime.now(), - message: serverCreated.toString() - ) - ); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.connectionNotCreated), - backgroundColor: Colors.red, - ) - ); - } - } - else if (result['result'] == 'invalid_username_password') { - appConfigProvider.addLog(result['log']); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.invalidUsernamePassword), - backgroundColor: Colors.red, - ) - ); - } - else if (result['result'] == 'many_attempts') { - appConfigProvider.addLog(result['log']); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.tooManyAttempts), - backgroundColor: Colors.red, - ) - ); - } - else if (result['result'] == 'no_connection') { - appConfigProvider.addLog(result['log']); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.cantReachServer), - backgroundColor: Colors.red, - ) - ); - } - else if (result['result'] == 'ssl_error') { - appConfigProvider.addLog(result['log']); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.sslError), - backgroundColor: Colors.red, - ) - ); - } - else if (result['result'] == 'server_error') { - appConfigProvider.addLog(result['log']); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.serverError), - backgroundColor: Colors.red, - ) - ); - } - else { - appConfigProvider.addLog(result['log']); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.unknownError), - backgroundColor: Colors.red, - ) - ); - } - } - - void edit() async { - final Server serverObj = Server( - id: widget.server!.id, - name: nameController.text, - connectionMethod: connectionType.name, - domain: ipDomainController.text, - port: portController.text != '' ? int.parse(portController.text) : null, - user: userController.text, - password: passwordController.text, - defaultServer: defaultServer, - authToken: homeAssistant == true - ? encodeBase64UserPass(userController.text, passwordController.text) - : '', - runningOnHa: homeAssistant - ); - - final result = homeAssistant == true - ? await loginHA(serverObj) - : await login(serverObj); - - if (result['result'] == 'success') { - serverObj.authToken = encodeBase64UserPass(serverObj.user, serverObj.password); - final serverSaved = await serversProvider.editServer(serverObj); - if (serverSaved == null) { - Navigator.pop(context); - } - else { - appConfigProvider.addLog( - AppLog( - type: 'edit_connection_db', - dateTime: DateTime.now(), - message: serverSaved.toString() - ) - ); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.connectionNotCreated), - backgroundColor: Colors.red, - ) - ); - } - } - else if (result['result'] == 'invalid_username_password') { - appConfigProvider.addLog(result['log']); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.invalidUsernamePassword), - backgroundColor: Colors.red, - ) - ); - } - else if (result['result'] == 'many_attempts') { - appConfigProvider.addLog(result['log']); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.tooManyAttempts), - backgroundColor: Colors.red, - ) - ); - } - else if (result['result'] == 'no_connection') { - appConfigProvider.addLog(result['log']); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.cantReachServer), - backgroundColor: Colors.red, - ) - ); - } - else if (result['result'] == 'ssl_error') { - appConfigProvider.addLog(result['log']); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.sslError), - backgroundColor: Colors.red, - ) - ); - } - else if (result['result'] == 'server_error') { - appConfigProvider.addLog(result['log']); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.serverError), - backgroundColor: Colors.red, - ) - ); - } - else { - appConfigProvider.addLog(result['log']); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.unknownError), - backgroundColor: Colors.red, - ) - ); - } - } - - return Stack( - children: [ - Scaffold( - appBar: AppBar( - title: Text(AppLocalizations.of(context)!.createConnection), - actions: [ - Padding( - padding: const EdgeInsets.only(right: 10), - child: IconButton( - tooltip: widget.server == null - ? AppLocalizations.of(context)!.connect - : AppLocalizations.of(context)!.save, - onPressed: allDataValid == true - ? widget.server == null - ? () => connect() - : () => edit() - : null, - icon: Icon( - widget.server == null - ? Icons.login_rounded - : Icons.save_rounded - ) - ), - ), - ], - toolbarHeight: 70, - ), - body: ListView( - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - margin: const EdgeInsets.only( - top: 24, - left: 24, - right: 24 - ), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary.withOpacity(0.05), - borderRadius: BorderRadius.circular(30), - border: Border.all( - color: Theme.of(context).colorScheme.primary - ) - ), - child: Text( - "${connectionType.name}://${ipDomainController.text}${portController.text != '' ? ':${portController.text}' : ""}${pathController.text}", - textAlign: TextAlign.center, - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.w500 - ), - ), - ), - sectionLabel(AppLocalizations.of(context)!.general), - textField( - label: AppLocalizations.of(context)!.name, - controller: nameController, - icon: Icons.badge_rounded, - error: nameError, - onChanged: (value) { - if (value != '') { - setState(() => nameError = null); - } - else { - setState(() => nameError = AppLocalizations.of(context)!.nameNotEmpty); - } - checkDataValid(); - } - ), - sectionLabel(AppLocalizations.of(context)!.connection), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: SegmentedButton( - segments: const [ - ButtonSegment( - value: ConnectionType.http, - label: Text("HTTP") - ), - ButtonSegment( - value: ConnectionType.https, - label: Text("HTTPS") - ), - ], - selected: {connectionType}, - onSelectionChanged: (value) => setState(() => connectionType = value.first), - ), - ), - const SizedBox(height: 30), - textField( - label: AppLocalizations.of(context)!.ipDomain, - controller: ipDomainController, - icon: Icons.link_rounded, - error: ipDomainError, - keyboardType: TextInputType.url, - onChanged: validateAddress - ), - const SizedBox(height: 20), - textField( - label: AppLocalizations.of(context)!.path, - controller: pathController, - icon: Icons.route_rounded, - error: pathError, - onChanged: validateSubroute, - hintText: AppLocalizations.of(context)!.examplePath, - helperText: AppLocalizations.of(context)!.helperPath, - ), - const SizedBox(height: 20), - textField( - label: AppLocalizations.of(context)!.port, - controller: portController, - icon: Icons.numbers_rounded, - error: portError, - keyboardType: TextInputType.number, - onChanged: validatePort - ), - sectionLabel(AppLocalizations.of(context)!.authentication), - textField( - label: AppLocalizations.of(context)!.username, - controller: userController, - icon: Icons.person_rounded, - onChanged: validateUser, - error: userError - ), - const SizedBox(height: 20), - textField( - label: AppLocalizations.of(context)!.password, - controller: passwordController, - icon: Icons.lock_rounded, - keyboardType: TextInputType.visiblePassword, - onChanged: validatePassword, - error: passwordError, - obscureText: true - ), - sectionLabel(AppLocalizations.of(context)!.other), - Material( - color: Colors.transparent, - child: InkWell( - onTap: widget.server == null - ? () => setState(() => defaultServer = !defaultServer) - : null, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - AppLocalizations.of(context)!.defaultServer, - style: const TextStyle( - fontSize: 15, - ), - ), - Switch( - value: defaultServer, - onChanged: widget.server == null - ? (value) => setState(() => defaultServer = value) - : null, - activeColor: Theme.of(context).colorScheme.primary, - ) - ], - ), - ), - ), - ), - const SizedBox(height: 20), - Material( - color: Colors.transparent, - child: InkWell( - onTap: () => setState(() => homeAssistant = !homeAssistant), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - AppLocalizations.of(context)!.runningHomeAssistant, - style: const TextStyle( - fontSize: 15, - ), - ), - Switch( - value: homeAssistant, - onChanged: (value) => setState(() => homeAssistant = value), - activeColor: Theme.of(context).colorScheme.primary, - ) - ], - ), - ), - ), - ), - const SizedBox(height: 20), - ], - ), - ), - AnimatedOpacity( - opacity: isConnecting == true ? 1 : 0, - duration: const Duration(milliseconds: 250), - curve: Curves.easeInOut, - child: IgnorePointer( - ignoring: isConnecting == true ? false : true, - child: Scaffold( - backgroundColor: Colors.transparent, - body: Container( - width: mediaQuery.size.width, - height: mediaQuery.size.height, - color: const Color.fromRGBO(0, 0, 0, 0.7), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const CircularProgressIndicator( - color: Colors.white, - ), - const SizedBox(height: 30), - Text( - AppLocalizations.of(context)!.connecting, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.w500, - fontSize: 26 - ), - ) - ], - ), - ), - ), - ), - ) - ], - ); - } -} \ No newline at end of file diff --git a/lib/widgets/bottom_nav_bar.dart b/lib/widgets/bottom_nav_bar.dart index dabc657..8d06a8d 100644 --- a/lib/widgets/bottom_nav_bar.dart +++ b/lib/widgets/bottom_nav_bar.dart @@ -3,25 +3,22 @@ import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/config/app_screens.dart'; -import 'package:adguard_home_manager/providers/logs_provider.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; import 'package:adguard_home_manager/models/app_screen.dart'; class BottomNavBar extends StatelessWidget { - const BottomNavBar({Key? key}) : super(key: key); + const BottomNavBar({super.key}); @override Widget build(BuildContext context) { final serversProvider = Provider.of(context); final appConfigProvider = Provider.of(context); - final logsProvider = Provider.of(context); - List screens = serversProvider.selectedServer != null + List screens = serversProvider.selectedServer != null && serversProvider.apiClient2 != null ? screensServerConnected : screensSelectServer; - String translatedName(String key) { switch (key) { case 'home': @@ -47,28 +44,44 @@ class BottomNavBar extends StatelessWidget { } } + if ((serversProvider.selectedServer == null || serversProvider.apiClient2 == null) && appConfigProvider.selectedScreen > 1) { + appConfigProvider.setSelectedScreen(0); + } + return NavigationBar( - selectedIndex: appConfigProvider.selectedScreen, + selectedIndex: (serversProvider.selectedServer == null || serversProvider.apiClient2 == null) && appConfigProvider.selectedScreen > 1 + ? 0 + : appConfigProvider.selectedScreen, destinations: screens.map((screen) => NavigationDestination( - icon: Icon( - screen.icon, - color: screens[appConfigProvider.selectedScreen] == screen - ? Theme.of(context).colorScheme.onSecondaryContainer - : Theme.of(context).colorScheme.onSurfaceVariant, + icon: Stack( + children: [ + Icon( + screen.icon, + color: screens[appConfigProvider.selectedScreen] == screen + ? Theme.of(context).colorScheme.onSecondaryContainer + : Theme.of(context).colorScheme.onSurfaceVariant, + ), + if ( + screen.name == 'settings' && + serversProvider.updateAvailable.data != null && + serversProvider.updateAvailable.data!.canAutoupdate == true + ) Positioned( + bottom: 0, + right: -12, + child: Container( + width: 10, + height: 10, + margin: const EdgeInsets.only(right: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Colors.red + ), + ), + ) + ], ), label: translatedName(screen.name) )).toList(), - onDestinationSelected: (value) { - // Reset clients tab to 0 when changing screen - if (value != 1) { - appConfigProvider.setSelectedClientsTab(0); - } - // Reset logs filters when changing screen - if (value != 2) { - logsProvider.resetFilters(); - } - appConfigProvider.setSelectedScreen(value); - }, ); } } \ No newline at end of file diff --git a/lib/widgets/combined_line_chart.dart b/lib/widgets/combined_line_chart.dart new file mode 100644 index 0000000..9aceb67 --- /dev/null +++ b/lib/widgets/combined_line_chart.dart @@ -0,0 +1,204 @@ +import 'package:flutter/material.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/screens/home/combined_chart.dart'; + +import 'package:adguard_home_manager/providers/app_config_provider.dart'; + + +class CustomCombinedLineChart extends StatelessWidget { + final CombinedChartData inputData; + final List dates; + final BuildContext context; + final bool daysInterval; + + const CustomCombinedLineChart({ + super.key, + required this.inputData, + required this.context, + required this.dates, + required this.daysInterval + }); + + LineChartData mainData(Map data, ThemeMode selectedTheme) { + String chartDate(DateTime date) { + String twoDigits(int number) => number.toString().padLeft(2, '0'); + + String shortMonth(String month) => month.length > 3 ? month.substring(0, 3) : month; + + String getMonth(int month) { + final List months = [ + AppLocalizations.of(context)!.january, + AppLocalizations.of(context)!.february, + AppLocalizations.of(context)!.march, + AppLocalizations.of(context)!.april, + AppLocalizations.of(context)!.may, + AppLocalizations.of(context)!.june, + AppLocalizations.of(context)!.july, + AppLocalizations.of(context)!.august, + AppLocalizations.of(context)!.september, + AppLocalizations.of(context)!.october, + AppLocalizations.of(context)!.november, + AppLocalizations.of(context)!.december, + ]; + return months[month-1]; + } + + if (daysInterval == true) { + return "${date.day} ${shortMonth(getMonth(date.month))}"; + } + else { + return "${date.day} ${shortMonth(getMonth(date.month))} ${twoDigits(date.hour)}:00"; + } + } + + return LineChartData( + gridData: const FlGridData( + show: false, + drawVerticalLine: false, + ), + titlesData: const FlTitlesData( + show: false, + ), + borderData: FlBorderData( + show: false, + ), + lineBarsData: List.from( + data["lines"].map((item) => LineChartBarData( + spots: item['data'], + color: item['color'], + isCurved: true, + barWidth: 2, + isStrokeCapRound: true, + preventCurveOverShooting: true, + dotData: const FlDotData( + show: false, + ), + belowBarData: BarAreaData( + show: true, + color: item['color'].withOpacity(0.2) + ), + )) + ), + lineTouchData: LineTouchData( + enabled: true, + touchTooltipData: LineTouchTooltipData( + fitInsideHorizontally: true, + getTooltipColor: (touchedSpot) => selectedTheme == ThemeMode.light + ? const Color.fromRGBO(220, 220, 220, 0.9) + : const Color.fromRGBO(35, 35, 35, 0.9), + getTooltipItems: (items) { + return [ + LineTooltipItem( + chartDate(dates[items[0].x.toInt()]), + TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: Theme.of(context).colorScheme.onSurface + ) + ), + ...items.sublist(0, items.length-1).asMap().entries.map((item) => LineTooltipItem( + "${data['lines'][item.key]['label']}: ${item.value.y.toInt().toString()}", + TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: data['lines'][item.key]['color'] + ) + )) + ]; + } + ), + ) + ); + } + + @override + Widget build(BuildContext context) { + final appConfigProvider = Provider.of(context); + + Map formatData(CombinedChartData unformattedData) { + int topPoint = 0; + + List dataLine(List data) { + final List formattedData = []; + + int xPosition = 0; + for (int i = 0; i < data.length; i++) { + if (data[i] > topPoint) { + topPoint = data[i]; + } + formattedData.add( + FlSpot( + xPosition.toDouble(), + data[i].toDouble() + ) + ); + xPosition++; + } + + return formattedData; + } + + List datesLine(int number) { + final List formattedData = []; + + for (int i = 0; i < number; i++) { + formattedData.add( + FlSpot( + i.toDouble(), + 0 + ) + ); + } + + return formattedData; + } + + List> toDraw = []; + toDraw.add({ + "data": dataLine(unformattedData.totalQueries.data), + "color": unformattedData.totalQueries.color, + "label": unformattedData.totalQueries.label + }); + if (unformattedData.blockedFilters != null) { + toDraw.add({ + "data": dataLine(unformattedData.blockedFilters!.data), + "color": unformattedData.blockedFilters!.color, + "label": unformattedData.blockedFilters!.label + }); + } + if (unformattedData.replacedSafeBrowsing != null) { + toDraw.add({ + "data": dataLine(unformattedData.replacedSafeBrowsing!.data), + "color": unformattedData.replacedSafeBrowsing!.color, + "label": unformattedData.replacedSafeBrowsing!.label + }); + } + if (unformattedData.replacedParental != null) { + toDraw.add({ + "data": dataLine(unformattedData.replacedParental!.data), + "color": unformattedData.replacedParental!.color, + "label": unformattedData.replacedParental!.label + }); + } + toDraw.add({ + "data": datesLine(dates.length), + "color": Colors.transparent, + }); + + return { + 'lines': toDraw, + 'topPoint': topPoint + }; + } + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 15), + child: LineChart( + mainData(formatData(inputData), appConfigProvider.selectedTheme) + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/confirm_action_modal.dart b/lib/widgets/confirm_action_modal.dart index d0bb314..4ed9cd7 100644 --- a/lib/widgets/confirm_action_modal.dart +++ b/lib/widgets/confirm_action_modal.dart @@ -8,12 +8,12 @@ class ConfirmActionModal extends StatelessWidget { final void Function() onConfirm; const ConfirmActionModal({ - Key? key, + super.key, required this.icon, required this.title, required this.message, required this.onConfirm - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/custom_checkbox_list_tile.dart b/lib/widgets/custom_checkbox_list_tile.dart new file mode 100644 index 0000000..39c4cdf --- /dev/null +++ b/lib/widgets/custom_checkbox_list_tile.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; + +class CustomCheckboxListTile extends StatelessWidget { + final bool value; + final void Function(bool) onChanged; + final String title; + final String? subtitle; + final bool? disabled; + final EdgeInsets? padding; + + const CustomCheckboxListTile({ + super.key, + required this.value, + required this.onChanged, + required this.title, + this.disabled, + this.subtitle, + this.padding + }); + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: disabled != null && disabled == true + ? null + : () => onChanged(!value), + child: Padding( + padding: padding ?? const EdgeInsets.only( + top: 12, left: 16, right: 18, bottom: 16 + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 16, + color: disabled != null && disabled == true + ? Theme.of(context).colorScheme.onSurface.withOpacity(0.38) + : Theme.of(context).colorScheme.onSurface, + ), + ), + if (subtitle != null) ... [ + const SizedBox(height: 5), + SizedBox( + width: MediaQuery.of(context).size.width-110, + child: Text( + subtitle!, + style: TextStyle( + fontSize: 14, + color: disabled != null && disabled == true + ? Theme.of(context).listTileTheme.textColor!.withOpacity(0.38) + : Theme.of(context).listTileTheme.textColor + ), + ), + ), + ] + ], + ), + ), + const SizedBox(width: 16), + Theme( + data: ThemeData( + unselectedWidgetColor: disabled == true + ? Colors.grey + : Theme.of(context).colorScheme.onSurface, + disabledColor: Colors.grey + ), + child: Checkbox( + value: value, + onChanged: (value) => disabled != null && disabled == true && value != null + ? null + : onChanged(value!), + tristate: false, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + ), + activeColor: disabled == true + ? Colors.grey + : Theme.of(context).colorScheme.primary, + ), + ) + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/custom_list_tile.dart b/lib/widgets/custom_list_tile.dart index 629d7a7..5bf1e42 100644 --- a/lib/widgets/custom_list_tile.dart +++ b/lib/widgets/custom_list_tile.dart @@ -10,9 +10,11 @@ class CustomListTile extends StatelessWidget { final EdgeInsets? padding; final void Function()? onLongPress; final bool? disabled; + final void Function(bool)? onHover; + final Color? color; const CustomListTile({ - Key? key, + super.key, required this.title, this.subtitle, this.subtitleWidget, @@ -21,15 +23,18 @@ class CustomListTile extends StatelessWidget { this.trailing, this.padding, this.onLongPress, - this.disabled - }) : super(key: key); + this.disabled, + this.onHover, + this.color, + }); @override Widget build(BuildContext context) { return Material( - color: Colors.transparent, + color: color ?? Colors.transparent, child: InkWell( onTap: onTap, + onHover: onHover, onLongPress: onLongPress, child: Padding( padding: padding ?? const EdgeInsets.symmetric(horizontal: 16, vertical: 12), @@ -85,7 +90,7 @@ class CustomListTile extends StatelessWidget { ), ), if (trailing != null) ...[ - const SizedBox(width: 10), + const SizedBox(width: 16), trailing! ] ], diff --git a/lib/widgets/custom_list_tile_dialog.dart b/lib/widgets/custom_list_tile_dialog.dart index 1d09760..3faf8d4 100644 --- a/lib/widgets/custom_list_tile_dialog.dart +++ b/lib/widgets/custom_list_tile_dialog.dart @@ -6,11 +6,11 @@ class CustomListTileDialog extends StatelessWidget { final void Function()? onTap; const CustomListTileDialog({ - Key? key, + super.key, required this.title, this.icon, this.onTap - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -29,12 +29,14 @@ class CustomListTileDialog extends StatelessWidget { ), const SizedBox(width: 24), ], - Text( - title, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurface, + Flexible( + child: Text( + title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface, + ), ), ) ], diff --git a/lib/widgets/custom_pie_chart.dart b/lib/widgets/custom_pie_chart.dart new file mode 100644 index 0000000..343ec58 --- /dev/null +++ b/lib/widgets/custom_pie_chart.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:pie_chart/pie_chart.dart'; + +class CustomPieChart extends StatelessWidget { + final Map data; + final List colors; + final Duration? animationDuration; + + const CustomPieChart({ + super.key, + required this.data, + required this.colors, + this.animationDuration = const Duration(milliseconds: 800), + }); + + @override + Widget build(BuildContext context) { + return PieChart( + dataMap: data, + animationDuration: animationDuration, + colorList: colors, + initialAngleInDegree: 270, + chartType: ChartType.ring, + ringStrokeWidth: 12, + legendOptions: const LegendOptions( + showLegends: false + ), + chartValuesOptions: const ChartValuesOptions( + showChartValues: false, + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/custom_radio.dart b/lib/widgets/custom_radio.dart index f8f2a08..5058316 100644 --- a/lib/widgets/custom_radio.dart +++ b/lib/widgets/custom_radio.dart @@ -7,12 +7,12 @@ class CustomRadio extends StatelessWidget { final Color backgroundColor; const CustomRadio({ - Key? key, + super.key, required this.value, required this.groupValue, this.onChange, required this.backgroundColor, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/custom_radio_list_tile.dart b/lib/widgets/custom_radio_list_tile.dart index b800aa3..0469bc9 100644 --- a/lib/widgets/custom_radio_list_tile.dart +++ b/lib/widgets/custom_radio_list_tile.dart @@ -11,14 +11,14 @@ class CustomRadioListTile extends StatelessWidget { final void Function(String) onChanged; const CustomRadioListTile({ - Key? key, + super.key, required this.groupValue, required this.value, required this.radioBackgroundColor, required this.title, this.subtitle, required this.onChanged, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -41,33 +41,35 @@ class CustomRadioListTile extends StatelessWidget { backgroundColor: radioBackgroundColor, ), const SizedBox(width: 24), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: MediaQuery.of(context).size.width-110, - child: Text( - title, - style: TextStyle( - fontSize: 16, - color: Theme.of(context).colorScheme.onSurface - ), - ), - ), - if (subtitle != null) ...[ - const SizedBox(height: 5), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ SizedBox( width: MediaQuery.of(context).size.width-110, child: Text( - subtitle!, + title, style: TextStyle( - color: Theme.of(context).listTileTheme.textColor, - fontSize: 14 + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface ), ), ), - ] - ], + if (subtitle != null) ...[ + const SizedBox(height: 5), + SizedBox( + width: MediaQuery.of(context).size.width-110, + child: Text( + subtitle!, + style: TextStyle( + color: Theme.of(context).listTileTheme.textColor, + fontSize: 14 + ), + ), + ), + ] + ], + ), ), ], ), diff --git a/lib/widgets/custom_radio_toggle.dart b/lib/widgets/custom_radio_toggle.dart index 59cb9e9..2698243 100644 --- a/lib/widgets/custom_radio_toggle.dart +++ b/lib/widgets/custom_radio_toggle.dart @@ -7,12 +7,12 @@ class CustomRadioToggle extends StatelessWidget { final void Function(String) onTap; const CustomRadioToggle({ - Key? key, + super.key, required this.groupSelected, required this.value, required this.label, required this.onTap, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/custom_settings_tile.dart b/lib/widgets/custom_settings_tile.dart new file mode 100644 index 0000000..824b710 --- /dev/null +++ b/lib/widgets/custom_settings_tile.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; + +class CustomSettingsTile extends StatelessWidget { + final String title; + final String? subtitle; + final Widget? subtitleWidget; + final void Function()? onTap; + final IconData? icon; + final Widget? trailing; + final EdgeInsets? padding; + final int thisItem; + final int? selectedItem; + + const CustomSettingsTile({ + super.key, + required this.title, + this.subtitle, + this.subtitleWidget, + this.onTap, + this.icon, + this.trailing, + this.padding, + required this.thisItem, + required this.selectedItem, + }); + + @override + Widget build(BuildContext context) { + Widget tileBody = Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (icon != null) ...[ + Icon( + icon, + size: 24, + color: Theme.of(context).listTileTheme.iconColor, + ), + const SizedBox(width: 16), + ], + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + if (subtitle != null || subtitleWidget != null) ...[ + const SizedBox(height: 5), + if (subtitle == null && subtitleWidget != null) subtitleWidget!, + if (subtitle != null && subtitleWidget == null) Text( + subtitle!, + style: TextStyle( + color: Theme.of(context).listTileTheme.textColor, + fontSize: 14, + fontWeight: FontWeight.w400 + ), + ), + ] + ], + ), + ) + ], + ), + ), + if (trailing != null) ...[ + const SizedBox(width: 10), + trailing! + ] + ], + ); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(28), + child: InkWell( + borderRadius: BorderRadius.circular(28), + onTap: onTap, + child: Container( + width: double.maxFinite, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(28), + color: thisItem == selectedItem + ? Theme.of(context).colorScheme.primaryContainer + : null + ), + child: tileBody + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/custom_switch_list_tile.dart b/lib/widgets/custom_switch_list_tile.dart index fc6a1cd..84e65c5 100644 --- a/lib/widgets/custom_switch_list_tile.dart +++ b/lib/widgets/custom_switch_list_tile.dart @@ -6,15 +6,19 @@ class CustomSwitchListTile extends StatelessWidget { final String title; final String? subtitle; final bool? disabled; + final EdgeInsets? padding; + final IconData? leadingIcon; const CustomSwitchListTile({ - Key? key, + super.key, required this.value, required this.onChanged, required this.title, this.disabled, this.subtitle, - }) : super(key: key); + this.padding, + this.leadingIcon, + }); @override Widget build(BuildContext context) { @@ -25,12 +29,19 @@ class CustomSwitchListTile extends StatelessWidget { ? null : () => onChanged(!value), child: Padding( - padding: const EdgeInsets.only( + padding: padding ?? const EdgeInsets.only( top: 12, left: 16, right: 18, bottom: 16 ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + if (leadingIcon != null) ...[ + Icon( + leadingIcon, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 16), + ], Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -68,7 +79,6 @@ class CustomSwitchListTile extends StatelessWidget { onChanged: disabled != null && disabled == true ? null : onChanged, - activeColor: Theme.of(context).colorScheme.primary, ) ], ), diff --git a/lib/widgets/floating_search_bar.dart b/lib/widgets/floating_search_bar.dart new file mode 100644 index 0000000..bc09058 --- /dev/null +++ b/lib/widgets/floating_search_bar.dart @@ -0,0 +1,110 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/config/globals.dart'; + +class FloatingSearchBar extends StatefulWidget { + final void Function(String) onSearchCompleted; + final RenderBox? searchButtonRenderBox; + final String? existingSearchValue; + final void Function(String)? onSearchFieldUpdated; + final void Function()? onSearchFieldCleared; + + const FloatingSearchBar({ + super.key, + required this.onSearchCompleted, + required this.searchButtonRenderBox, + this.existingSearchValue, + this.onSearchFieldUpdated, + this.onSearchFieldCleared, + }); + + @override + State createState() => _SearchState(); +} + +class _SearchState extends State { + final _searchController = TextEditingController(); + + @override + void initState() { + if (widget.existingSearchValue != null) { + _searchController.text = widget.existingSearchValue!; + } + + super.initState(); + } + + @override + Widget build(BuildContext context) { + final position = widget.searchButtonRenderBox?.localToGlobal(Offset.zero); + final topPadding = MediaQuery.of(globalNavigatorKey.currentContext!).viewPadding.top; + + return GestureDetector( + onTap: () => Navigator.pop(context), + child: Material( + color: Colors.transparent, + child: LayoutBuilder( + builder: (context, constraints) { + final double width = constraints.maxWidth - 32 > 500 ? 500 : constraints.maxWidth - 32; + return Stack( + alignment: Alignment.topCenter, + children: [ + Positioned( + top: position != null ? position.dy - topPadding : topPadding, + child: SizedBox( + width: width, + child: GestureDetector( + onTap: () => {}, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16) + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: TextFormField( + controller: _searchController, + onChanged: widget.onSearchFieldUpdated, + onFieldSubmitted: (v) { + widget.onSearchCompleted(v); + Navigator.pop(context); + }, + autofocus: true, + decoration: InputDecoration( + hintText: AppLocalizations.of(context)!.search, + prefixIcon: const Icon(Icons.search_rounded), + border: InputBorder.none, + filled: true, + fillColor: Colors.grey.withOpacity(0.2), + suffixIcon: _searchController.text != "" + ? IconButton( + onPressed: () { + setState(() => _searchController.text = ""); + if (widget.onSearchFieldCleared != null) { + widget.onSearchFieldCleared!(); + } + }, + icon: const Icon( + Icons.close_rounded, + size: 20, + ), + tooltip: AppLocalizations.of(context)!.clearSearch, + ) + : null, + contentPadding: const EdgeInsets.symmetric(vertical: 12), + ), + ), + ), + ), + ), + ), + ) + ], + ); + } + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/layout.dart b/lib/widgets/layout.dart new file mode 100644 index 0000000..119bee1 --- /dev/null +++ b/lib/widgets/layout.dart @@ -0,0 +1,293 @@ +import 'package:flutter/material.dart'; +import 'package:animations/animations.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/widgets/system_ui_overlay_style.dart'; + +import 'package:adguard_home_manager/providers/app_config_provider.dart'; +import 'package:adguard_home_manager/config/app_screens.dart'; +import 'package:adguard_home_manager/config/sizes.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; + +class Layout extends StatefulWidget { + const Layout({ + super.key, + }); + + @override + State createState() => _LayoutState(); +} + +class _LayoutState extends State with WidgetsBindingObserver { + bool _drawerExpanded = true; + + void _goBranch(int index) { + Provider.of(context, listen: false).setSelectedScreen(index); + } + + @override + void initState() { + WidgetsBinding.instance.addObserver(this); + + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + // if (kDebugMode) return; // Don't check for app updates on debug mode + // final appConfigProvider = Provider.of(context, listen: false); + // final result = await checkAppUpdates( + // currentBuildNumber: appConfigProvider.getAppInfo!.buildNumber, + // installationSource: appConfigProvider.installationSource, + // setUpdateAvailable: appConfigProvider.setAppUpdatesAvailable, + // isBeta: appConfigProvider.getAppInfo!.version.contains('beta'), + // ); + // if (result != null && appConfigProvider.doNotRememberVersion != result.tagName && mounted) { + // await showDialog( + // context: context, + // builder: (context) => UpdateModal( + // gitHubRelease: result, + // onDownload: (link, version) => openUrl(link), + // ), + // ); + // } + }); + } + + @override + Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width; + + final serversProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + + final screens = serversProvider.selectedServer != null && serversProvider.apiClient2 != null + ? screensServerConnected + : screensSelectServer; + + String translatedName(String key) { + switch (key) { + case 'home': + return AppLocalizations.of(context)!.home; + + case 'settings': + return AppLocalizations.of(context)!.settings; + + case 'connect': + return AppLocalizations.of(context)!.connect; + + case 'clients': + return AppLocalizations.of(context)!.clients; + + case 'logs': + return AppLocalizations.of(context)!.logs; + + case 'filters': + return AppLocalizations.of(context)!.filters; + + default: + return ''; + } + } + + if (width > desktopBreakpoint) { + return OverlayStyle( + child: Material( + child: Row( + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 250), + curve: Curves.ease, + width: _drawerExpanded ? 250 : 90, + child: ListView( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 16 + ), + child: IconButton( + onPressed: () => setState(() => _drawerExpanded = !_drawerExpanded), + icon: const Icon(Icons.menu_open_rounded), + tooltip: _drawerExpanded == true + ? AppLocalizations.of(context)!.closeMenu + : AppLocalizations.of(context)!.openMenu, + ), + ), + ], + ), + ...screens.asMap().entries.map( + (s) => DrawerTile( + icon: s.value.icon, + title: translatedName(s.value.name), + isSelected: appConfigProvider.selectedScreen == s.key, + onSelect: () => _goBranch(s.key), + withoutTitle: !_drawerExpanded, + ), + ), + ], + ), + ), + Expanded( + child: PageTransitionSwitcher( + duration: const Duration(milliseconds: 200), + transitionBuilder: ( + (child, primaryAnimation, secondaryAnimation) => FadeThroughTransition( + animation: primaryAnimation, + secondaryAnimation: secondaryAnimation, + child: child, + ) + ), + child: appConfigProvider.selectedScreen < screens.length + ? screens[appConfigProvider.selectedScreen].child + : screens[0].child, + ), + ), + ], + ), + ), + ); + } + else { + final screens = serversProvider.selectedServer != null && serversProvider.apiClient2 != null + ? screensServerConnected + : screensSelectServer; + + return OverlayStyle( + child: Scaffold( + body: PageTransitionSwitcher( + duration: const Duration(milliseconds: 200), + transitionBuilder: ( + (child, primaryAnimation, secondaryAnimation) => FadeThroughTransition( + animation: primaryAnimation, + secondaryAnimation: secondaryAnimation, + child: child, + ) + ), + child: appConfigProvider.selectedScreen < screens.length + ? screens[appConfigProvider.selectedScreen].child + : screens[0].child, + ), + bottomNavigationBar: NavigationBar( + selectedIndex: (serversProvider.selectedServer == null || serversProvider.apiClient2 == null) && appConfigProvider.selectedScreen > 1 + ? 0 + : appConfigProvider.selectedScreen, + onDestinationSelected: (s) => _goBranch(s), + destinations: screens.asMap().entries.map((screen) => NavigationDestination( + icon: Stack( + children: [ + Icon( + screen.value.icon, + color: appConfigProvider.selectedScreen == screen.key + ? Theme.of(context).colorScheme.onSecondaryContainer + : Theme.of(context).colorScheme.onSurfaceVariant, + ), + if ( + screen.value.name == 'settings' && + serversProvider.updateAvailable.data != null && + serversProvider.updateAvailable.data!.canAutoupdate == true + ) Positioned( + bottom: 0, + right: -12, + child: Container( + width: 10, + height: 10, + margin: const EdgeInsets.only(right: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Colors.red + ), + ), + ) + ], + ), + label: translatedName(screen.value.name) + )).toList(), + ) + ), + ); + } + } +} + +class DrawerTile extends StatelessWidget { + final IconData icon; + final String title; + final bool isSelected; + final void Function() onSelect; + final bool? withoutTitle; + + const DrawerTile({ + super.key, + required this.icon, + required this.title, + required this.isSelected, + required this.onSelect, + this.withoutTitle, + }); + + @override + Widget build(BuildContext context) { + Widget iconWidget = withoutTitle == true + ? Tooltip( + message: title, + child: Icon( + icon, + color: isSelected + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + : Icon( + icon, + color: isSelected + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onSurfaceVariant, + ); + + return Padding( + padding: const EdgeInsets.only(right: 16), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: const BorderRadius.only( + topRight: Radius.circular(30), + bottomRight: Radius.circular(30), + ), + onTap: onSelect, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: isSelected + ? Theme.of(context).colorScheme.secondaryContainer + : null, + borderRadius: const BorderRadius.only( + topRight: Radius.circular(30), + bottomRight: Radius.circular(30), + ), + ), + child: Row(children: [ + iconWidget, + const SizedBox(width: 16), + Flexible( + child: Text( + title, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14, + color: isSelected + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ) + ]), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/line_chart.dart b/lib/widgets/line_chart.dart index 78338a5..f6899d1 100644 --- a/lib/widgets/line_chart.dart +++ b/lib/widgets/line_chart.dart @@ -1,27 +1,65 @@ import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; - class CustomLineChart extends StatelessWidget { final List data; final Color color; + final List dates; + final bool daysInterval; + final BuildContext context; const CustomLineChart({ - Key? key, + super.key, required this.data, - required this.color - }) : super(key: key); + required this.color, + required this.dates, + required this.daysInterval, + required this.context + }); + + String chartDate(DateTime date) { + String twoDigits(int number) => number.toString().padLeft(2, '0'); + + String shortMonth(String month) => month.length > 3 ? month.substring(0, 3) : month; + + String getMonth(int month) { + final List months = [ + AppLocalizations.of(context)!.january, + AppLocalizations.of(context)!.february, + AppLocalizations.of(context)!.march, + AppLocalizations.of(context)!.april, + AppLocalizations.of(context)!.may, + AppLocalizations.of(context)!.june, + AppLocalizations.of(context)!.july, + AppLocalizations.of(context)!.august, + AppLocalizations.of(context)!.september, + AppLocalizations.of(context)!.october, + AppLocalizations.of(context)!.november, + AppLocalizations.of(context)!.december, + ]; + + return months[month-1]; + } + + if (daysInterval == true) { + return "${date.day} ${shortMonth(getMonth(date.month))}"; + } + else { + return "${date.day} ${shortMonth(getMonth(date.month))} ${twoDigits(date.hour)}:00"; + } + } LineChartData mainData(Map data, ThemeMode selectedTheme) { return LineChartData( - gridData: FlGridData( + gridData: const FlGridData( show: false, drawVerticalLine: false, ), - titlesData: FlTitlesData( + titlesData: const FlTitlesData( show: false, ), borderData: FlBorderData( @@ -35,7 +73,7 @@ class CustomLineChart extends StatelessWidget { barWidth: 2, isStrokeCapRound: true, preventCurveOverShooting: true, - dotData: FlDotData( + dotData: const FlDotData( show: false, ), belowBarData: BarAreaData( @@ -43,14 +81,28 @@ class CustomLineChart extends StatelessWidget { color: color.withOpacity(0.2) ), ), + LineChartBarData( + spots: data['data'], + color: Colors.transparent, + barWidth: 0, + ), ], lineTouchData: LineTouchData( enabled: true, touchTooltipData: LineTouchTooltipData( - tooltipBgColor: selectedTheme == ThemeMode.light + fitInsideHorizontally: true, + getTooltipColor: (touchedSpot) => selectedTheme == ThemeMode.light ? const Color.fromRGBO(220, 220, 220, 0.9) : const Color.fromRGBO(35, 35, 35, 0.9), getTooltipItems: (items) => [ + LineTooltipItem( + chartDate(dates[items[0].x.toInt()]), + TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: Theme.of(context).colorScheme.onSurface + ) + ), LineTooltipItem( items[0].y.toInt().toString(), TextStyle( diff --git a/lib/widgets/list_bottom_sheet.dart b/lib/widgets/list_bottom_sheet.dart new file mode 100644 index 0000000..83f9c78 --- /dev/null +++ b/lib/widgets/list_bottom_sheet.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; + +class ListBottomSheet extends StatelessWidget { + final IconData icon; + final String title; + final List children; + final double? initialChildSize; + final double? minChildSize; + final double? maxChildSize; + + const ListBottomSheet({ + super.key, + required this.icon, + required this.title, + required this.children, + this.initialChildSize, + this.maxChildSize, + this.minChildSize, + }); + + @override + Widget build(BuildContext context) { + return DraggableScrollableSheet( + initialChildSize: initialChildSize ?? 0.6, + minChildSize: minChildSize ?? 0.3, + maxChildSize: 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 + ], + ), + ), + ); + }, + ); + } +} \ No newline at end of file diff --git a/lib/widgets/load_status_widgets.dart b/lib/widgets/load_status_widgets.dart new file mode 100644 index 0000000..2b1e699 --- /dev/null +++ b/lib/widgets/load_status_widgets.dart @@ -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 + ), + ), + ) + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/master_switch.dart b/lib/widgets/master_switch.dart new file mode 100644 index 0000000..323de68 --- /dev/null +++ b/lib/widgets/master_switch.dart @@ -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, + ) + ], + ), + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/menu_bar.dart b/lib/widgets/menu_bar.dart new file mode 100644 index 0000000..fe80446 --- /dev/null +++ b/lib/widgets/menu_bar.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/config/app_screens.dart'; +import 'package:adguard_home_manager/models/app_screen.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; + +class CustomMenuBar extends StatelessWidget { + final Widget child; + + const CustomMenuBar({ + super.key, + required this.child + }); + + @override + Widget build(BuildContext context) { + final serversProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + + List screens = serversProvider.selectedServer != null + ? screensServerConnected + : screensSelectServer; + + String translatedName(String key) { + switch (key) { + case 'connect': + return AppLocalizations.of(context)!.connect; + + case 'home': + return AppLocalizations.of(context)!.home; + + case 'settings': + return AppLocalizations.of(context)!.settings; + + case 'clients': + return AppLocalizations.of(context)!.clients; + + case 'logs': + return AppLocalizations.of(context)!.logs; + + case 'filters': + return AppLocalizations.of(context)!.filters; + + default: + return ''; + } + } + + return PlatformMenuBar( + menus: [ + PlatformMenu( + label: 'AdGuard Home Manager', + menus: [ + if ( + PlatformProvidedMenuItem.hasMenu(PlatformProvidedMenuItemType.about) + ) const PlatformMenuItemGroup( + members: [ + PlatformProvidedMenuItem( + type: PlatformProvidedMenuItemType.about, + ), + ] + ), + if ( + PlatformProvidedMenuItem.hasMenu(PlatformProvidedMenuItemType.quit) + ) const PlatformMenuItemGroup( + members: [ + PlatformProvidedMenuItem( + type: PlatformProvidedMenuItemType.quit, + ), + ] + ) + ], + ), + PlatformMenu( + label: AppLocalizations.of(context)!.screens, + menus: [ + PlatformMenuItemGroup( + members: screens.asMap().entries.map((e) => PlatformMenuItem( + label: "${appConfigProvider.selectedScreen == e.key ? '✔' : ''} ${translatedName(e.value.name)}", + onSelected: () => appConfigProvider.setSelectedScreen(e.key), + )).toList() + ), + ], + ), + ], + child: child, + ); + } +} \ No newline at end of file diff --git a/lib/widgets/navigation_rail.dart b/lib/widgets/navigation_rail.dart new file mode 100644 index 0000000..2d2e1ef --- /dev/null +++ b/lib/widgets/navigation_rail.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/providers/logs_provider.dart'; +import 'package:adguard_home_manager/config/app_screens.dart'; +import 'package:adguard_home_manager/models/app_screen.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; + +class SideNavigationRail extends StatelessWidget { + const SideNavigationRail({super.key}); + + @override + Widget build(BuildContext context) { + final serversProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + final logsProvider = Provider.of(context); + + List screens = serversProvider.selectedServer != null + ? screensServerConnected + : screensSelectServer; + + String translatedName(String key) { + switch (key) { + case 'home': + return AppLocalizations.of(context)!.home; + + case 'settings': + return AppLocalizations.of(context)!.settings; + + case 'connect': + return AppLocalizations.of(context)!.connect; + + case 'clients': + return AppLocalizations.of(context)!.clients; + + case 'logs': + return AppLocalizations.of(context)!.logs; + + case 'filters': + return AppLocalizations.of(context)!.filters; + + default: + return ''; + } + } + + if ((serversProvider.selectedServer == null || serversProvider.apiClient2 == null) && appConfigProvider.selectedScreen > 1) { + appConfigProvider.setSelectedScreen(0); + } + + return NavigationRail( + selectedIndex: (serversProvider.selectedServer == null || serversProvider.apiClient2 == null) && appConfigProvider.selectedScreen > 1 + ? 0 + : appConfigProvider.selectedScreen, + destinations: screens.map((screen) => NavigationRailDestination( + icon: Icon( + screen.icon, + color: screens[appConfigProvider.selectedScreen] == screen + ? Theme.of(context).colorScheme.onSecondaryContainer + : Theme.of(context).colorScheme.onSurfaceVariant, + ), + label: Text(translatedName(screen.name)) + )).toList(), + onDestinationSelected: (value) { + // Reset clients tab to 0 when changing screen + if (value != 1) { + appConfigProvider.setSelectedClientsTab(0); + } + // Reset logs filters when changing screen + if (value != 2) { + logsProvider.resetFilters(); + } + // Reset settings selected screen + if (value != screens.length-1) { + appConfigProvider.setSelectedSettingsScreen(screen: null); + } + appConfigProvider.setSelectedScreen(value); + }, + labelType: NavigationRailLabelType.all, + useIndicator: true, + groupAlignment: 0, + backgroundColor: Theme.of(context).colorScheme.primary.withOpacity(0.05), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/option_box.dart b/lib/widgets/option_box.dart index 7c05a67..e8514ef 100644 --- a/lib/widgets/option_box.dart +++ b/lib/widgets/option_box.dart @@ -1,18 +1,18 @@ import 'package:flutter/material.dart'; class OptionBox extends StatelessWidget { - final Widget child; final dynamic optionsValue; final dynamic itemValue; final void Function(dynamic) onTap; + final String label; const OptionBox({ - Key? key, - required this.child, + super.key, required this.optionsValue, required this.itemValue, required this.onTap, - }) : super(key: key); + required this.label, + }); @override Widget build(BuildContext context) { @@ -25,19 +25,27 @@ class OptionBox extends StatelessWidget { child: AnimatedContainer( duration: const Duration(milliseconds: 250), curve: Curves.easeInOut, - padding: const EdgeInsets.all(15), + padding: const EdgeInsets.symmetric(vertical: 16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(50), - border: Border.all( - color: optionsValue == itemValue - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurfaceVariant - ), color: optionsValue == itemValue ? Theme.of(context).colorScheme.primary - : Colors.transparent, + : Theme.of(context).colorScheme.primaryContainer, + ), + child: AnimatedDefaultTextStyle( + duration: const Duration(milliseconds: 250), + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14, + color: optionsValue == itemValue + ? Theme.of(context).colorScheme.onInverseSurface + : Theme.of(context).colorScheme.onSurface + ), + child: Text( + label, + textAlign: TextAlign.center, + ), ), - child: child, ), ), ); diff --git a/lib/widgets/options_menu.dart b/lib/widgets/options_menu.dart new file mode 100644 index 0000000..8daeb00 --- /dev/null +++ b/lib/widgets/options_menu.dart @@ -0,0 +1,124 @@ +import 'dart:io'; + +import 'package:contextmenu/contextmenu.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/widgets/custom_list_tile_dialog.dart'; +import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; + +import 'package:adguard_home_manager/models/menu_option.dart'; + +class OptionsMenu extends StatelessWidget { + final Widget child; + final List Function(dynamic) options; + final dynamic value; + final BorderRadius? borderRadius; + final void Function(dynamic)? onTap; + + const OptionsMenu({ + super.key, + required this.child, + required this.options, + this.value, + this.borderRadius, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + void openOptionsModal() { + showDialog( + context: context, + builder: (context) => _OptionsModal( + options: options, + value: value + ) + ); + } + + return Material( + color: Colors.transparent, + child: ContextMenuArea( + builder: (context) => options(value).map((opt) => CustomListTile( + title: opt.title, + icon: opt.icon, + onTap: () { + opt.action(); + Navigator.pop(context); + }, + )).toList(), + child: InkWell( + onTap: onTap != null + ? () => onTap!(value) + : null, + onLongPress: (Platform.isAndroid || Platform.isIOS) + ? () => openOptionsModal() + : null, + borderRadius: borderRadius, + child: child, + ), + ), + ); + } +} + +class _OptionsModal extends StatelessWidget { + final List Function(dynamic) options; + final dynamic value; + + const _OptionsModal({ + required this.options, + this.value, + }); + + @override + Widget build(BuildContext context) { + return AlertDialog( + contentPadding: const EdgeInsets.symmetric(vertical: 16), + scrollable: true, + title: Column( + children: [ + Icon( + Icons.more_horiz, + size: 24, + color: Theme.of(context).listTileTheme.iconColor + ), + const SizedBox(height: 16), + Text( + AppLocalizations.of(context)!.options, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface + ), + ) + ], + ), + content: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500 + ), + child: Column( + children: options(value).map((opt) => CustomListTileDialog( + title: opt.title, + icon: opt.icon, + onTap: () { + Navigator.pop(context); + opt.action(); + }, + )).toList() + ), + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.cancel) + ) + ], + ) + ], + ); + } +} \ No newline at end of file diff --git a/lib/widgets/process_dialog.dart b/lib/widgets/process_dialog.dart index 84c1185..f7f85a9 100644 --- a/lib/widgets/process_dialog.dart +++ b/lib/widgets/process_dialog.dart @@ -4,9 +4,9 @@ class ProcessDialog extends StatelessWidget { final String message; const ProcessDialog({ - Key? key, + super.key, required this.message, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -20,13 +20,16 @@ class ProcessDialog extends StatelessWidget { horizontal: 30 ), child: Row( + mainAxisSize: MainAxisSize.min, children: [ const CircularProgressIndicator(), const SizedBox(width: 40), - Text( - message, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface + Flexible( + child: Text( + message, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface + ), ), ) ], diff --git a/lib/widgets/section_label.dart b/lib/widgets/section_label.dart index c1f767a..1e3980f 100644 --- a/lib/widgets/section_label.dart +++ b/lib/widgets/section_label.dart @@ -5,10 +5,10 @@ class SectionLabel extends StatelessWidget { final EdgeInsets? padding; const SectionLabel({ - Key? key, + super.key, required this.label, this.padding - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/servers_list/delete_modal.dart b/lib/widgets/servers_list/delete_modal.dart index 2cc9809..4067d18 100644 --- a/lib/widgets/servers_list/delete_modal.dart +++ b/lib/widgets/servers_list/delete_modal.dart @@ -13,9 +13,9 @@ class DeleteModal extends StatelessWidget { final Server serverToDelete; const DeleteModal({ - Key? key, + super.key, required this.serverToDelete, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -23,19 +23,25 @@ class DeleteModal extends StatelessWidget { final appConfigProvider = Provider.of(context); void removeServer() async { + final previouslySelectedServer = serversProvider.selectedServer; + final deleted = await serversProvider.removeServer(serverToDelete); + Navigator.pop(context); + if (deleted == true) { - showSnacbkar( - context: context, + if (previouslySelectedServer != null && previouslySelectedServer.id == serverToDelete.id) { + appConfigProvider.setSelectedScreen(0); + } + + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.connectionRemoved, color: Colors.green ); } else { - showSnacbkar( - context: context, + showSnackbar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.connectionCannotBeRemoved, color: Colors.red diff --git a/lib/widgets/servers_list/server_tile_functions.dart b/lib/widgets/servers_list/server_tile_functions.dart new file mode 100644 index 0000000..06dbaee --- /dev/null +++ b/lib/widgets/servers_list/server_tile_functions.dart @@ -0,0 +1,167 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/widgets/add_server/unsupported_version_modal.dart'; +import 'package:adguard_home_manager/widgets/servers_list/delete_modal.dart'; +import 'package:adguard_home_manager/widgets/add_server/add_server_functions.dart'; + +import 'package:adguard_home_manager/config/globals.dart'; +import 'package:adguard_home_manager/config/minimum_server_version.dart'; +import 'package:adguard_home_manager/functions/compare_versions.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/functions/snackbar.dart'; +import 'package:adguard_home_manager/models/server_status.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/services/api_client.dart'; +import 'package:adguard_home_manager/services/auth.dart'; +import 'package:adguard_home_manager/classes/process_modal.dart'; +import 'package:adguard_home_manager/models/server.dart'; + +EdgeInsets generateMargins({ + required int index, + required int serversListLength +}) { + if (index == 0) { + return const EdgeInsets.only(top: 16, left: 16, right: 8, bottom: 8); + } + if (index == 1) { + return const EdgeInsets.only(top: 16, left: 8, right: 16, bottom: 8); + } + else if (index == serversListLength-1 && (index+1)%2 == 0) { + return const EdgeInsets.only(top: 8, left: 8, right: 16, bottom: 16); + } + else if (index == serversListLength-1 && (index+1)%2 == 1) { + return const EdgeInsets.only(top: 8, left: 16, right: 8, bottom: 16); + } + else { + if ((index+1)%2 == 0) { + return const EdgeInsets.only(top: 8, left: 8, right: 16, bottom: 8); + } + else { + return const EdgeInsets.only(top: 8, left: 16, right: 8, bottom: 8); + } + } +} + +void showDeleteModal({ + required BuildContext context, + required Server server +}) async { + await Future.delayed(const Duration(seconds: 0), () { + if (!context.mounted) return; + showDialog( + context: context, + builder: (context) => DeleteModal( + serverToDelete: server, + ), + barrierDismissible: false + ); + }); +} + +void openServerModal({ + required BuildContext context, + required double width, + Server? server, +}) async { + await Future.delayed(const Duration(seconds: 0), (() => { + openServerFormModal(context: context, width: width, server: server) + })); +} + +void connectToServer({ + required BuildContext context, + required Server server +}) async { + final ProcessModal process = ProcessModal(); + process.open(AppLocalizations.of(context)!.connecting); + + final result = server.runningOnHa == true + ? await ServerAuth.loginHA(server) + : await ServerAuth.login(server); + + if (result == AuthStatus.success && context.mounted) { + final serversProvider = Provider.of(context, listen: false); + final statusProvider = Provider.of(context, listen: false); + + final ApiClientV2 apiClient2 = ApiClientV2(server: server); + serversProvider.setApiClient2(apiClient2); + + statusProvider.setServerStatusLoad(LoadStatus.loading); + final serverStatus = await apiClient2.getServerStatus(); + if (serverStatus.successful == true) { + final status = serverStatus.content as ServerStatus; + + // Check if server version is supported + final validVersion = serverVersionIsAhead( + currentVersion: status.serverVersion, + referenceVersion: MinimumServerVersion.stable, + referenceVersionBeta: MinimumServerVersion.beta + ); + if (validVersion == false) { + process.close(); + showDialog( + context: globalNavigatorKey.currentContext!, + builder: (ctx) => UnsupportedVersionModal( + serverVersion: status.serverVersion, + onClose: () { + serversProvider.setSelectedServer(null); + } + ) + ); + return; + } + + serversProvider.setSelectedServer(server); + statusProvider.setServerStatusData( + data: status + ); + serversProvider.checkServerUpdatesAvailable( + server: server, + ); + statusProvider.setServerStatusLoad(LoadStatus.loaded); + } + else { + statusProvider.setServerStatusLoad(LoadStatus.error); + } + + process.close(); + } + else { + process.close(); + if (!context.mounted) return; + final appConfigProvider = Provider.of(context, listen: false); + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.cannotConnect, + color: Colors.red + ); + } +} + +void setDefaultServer({ + required BuildContext context, + required Server server +}) async { + final serversProvider = Provider.of(context, listen: false); + final result = await serversProvider.setDefaultServer(server); + if (!context.mounted) return; + final appConfigProvider = Provider.of(context, listen: false); + if (result == null) { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.connectionDefaultSuccessfully, + color: Colors.green + ); + } + else { + showSnackbar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.connectionDefaultFailed, + color: Colors.red + ); + } +} \ No newline at end of file diff --git a/lib/widgets/servers_list/servers_list.dart b/lib/widgets/servers_list/servers_list.dart index 1d6123c..d5a8d1b 100644 --- a/lib/widgets/servers_list/servers_list.dart +++ b/lib/widgets/servers_list/servers_list.dart @@ -1,430 +1,64 @@ // ignore_for_file: use_build_context_synchronously -import 'package:adguard_home_manager/functions/snackbar.dart'; import 'package:flutter/material.dart'; import 'package:expandable/expandable.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:adguard_home_manager/widgets/servers_list/delete_modal.dart'; -import 'package:adguard_home_manager/widgets/add_server_modal.dart'; +import 'package:adguard_home_manager/widgets/servers_list/servers_tile_item.dart'; -import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/models/app_log.dart'; -import 'package:adguard_home_manager/classes/process_modal.dart'; -import 'package:adguard_home_manager/models/server.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; -class ServersList extends StatefulWidget { +class ServersList extends StatelessWidget { final BuildContext context; final List controllers; final Function(int) onChange; final ScrollController scrollController; + final double breakingWidth; const ServersList({ - Key? key, + super.key, required this.context, required this.controllers, required this.onChange, required this.scrollController, - }) : super(key: key); + required this.breakingWidth + }); @override - State createState() => _ServersListState(); -} - -class _ServersListState extends State with SingleTickerProviderStateMixin { - late AnimationController animationController; - late Animation animation; - - @override - void initState() { - for (ExpandableController controller in widget.controllers) { - controller.addListener(() async { - await Future.delayed(const Duration(milliseconds: 200)); - if (controller.value == false) { - animationController.animateTo(0); - } - else { - animationController.animateBack(1); - } - }); - } - - animationController = AnimationController( - duration: const Duration(milliseconds: 200), - vsync: this, - ) - ..addListener(() => setState(() => {})); - animation = Tween( - begin: 0.0, - end: 0.5, - ).animate(CurvedAnimation( - parent: animationController, - curve: Curves.easeInOut - )); - - super.initState(); - } - - @override - void dispose() { - animationController.dispose(); - super.dispose(); - } - - @override - // ignore: avoid_renaming_method_parameters Widget build(BuildContext context) { final serversProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - - List servers = serversProvider.serversList; - - void showDeleteModal(Server server) async { - await Future.delayed(const Duration(seconds: 0), () => { - showDialog( - context: context, - builder: (context) => DeleteModal( - serverToDelete: server, - ), - barrierDismissible: false - ) - }); - } - - void openAddServerBottomSheet({Server? server}) async { - await Future.delayed(const Duration(seconds: 0), (() => { - Navigator.push(context, MaterialPageRoute( - fullscreenDialog: true, - builder: (BuildContext context) => AddServerModal(server: server) - )) - })); - } - - void connectToServer(Server server) async { - final ProcessModal process = ProcessModal(context: context); - process.open(AppLocalizations.of(context)!.connecting); - - final result = server.runningOnHa == true - ? await loginHA(server) - : await login(server); - - if (result['result'] == 'success') { - serversProvider.setSelectedServer(server); - - serversProvider.setServerStatusLoad(0); - final serverStatus = await getServerStatus(server); - if (serverStatus['result'] == 'success') { - serversProvider.setServerStatusData(serverStatus['data']); - serversProvider.setServerStatusLoad(1); - } - else { - appConfigProvider.addLog(serverStatus['log']); - serversProvider.setServerStatusLoad(2); - } - - process.close(); - } - else { - process.close(); - appConfigProvider.addLog(result['log']); - showSnacbkar( - context: context, - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.cannotConnect, - color: Colors.red - ); - } - } - - void setDefaultServer(Server server) async { - final result = await serversProvider.setDefaultServer(server); - if (result == null) { - showSnacbkar( - context: context, - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.connectionDefaultSuccessfully, - color: Colors.green - ); - } - else { - appConfigProvider.addLog( - AppLog( - type: 'set_default_server', - dateTime: DateTime.now(), - message: result.toString() - ) - ); - showSnacbkar( - context: context, - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.connectionDefaultFailed, - color: Colors.red - ); - } - } - - Widget leadingIcon(Server server) { - if (server.defaultServer == true) { - return Stack( - alignment: Alignment.center, - children: [ - Icon( - Icons.storage_rounded, - color: serversProvider.selectedServer != null && serversProvider.selectedServer?.id == server.id - ? serversProvider.serverStatus.data != null - ? Colors.green - : Colors.orange - : null, - ), - SizedBox( - width: 25, - height: 25, - child: Stack( - alignment: Alignment.bottomRight, - children: [ - Container( - padding: const EdgeInsets.all(1), - decoration: BoxDecoration( - color: Theme.of(context).floatingActionButtonTheme.backgroundColor, - borderRadius: BorderRadius.circular(20) - ), - child: Icon( - Icons.star, - color: Theme.of(context).floatingActionButtonTheme.foregroundColor, - size: 10, - ), - ), - ], - ), - ) - ], - ); - } - else { - return Icon( - Icons.storage_rounded, - color: serversProvider.selectedServer != null && serversProvider.selectedServer?.id == server.id - ? serversProvider.serverStatus.data != null - ? Colors.green - : Colors.orange - : null, - ); - } - } - - Widget topRow(Server server, int index) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + + if (serversProvider.serversList.isNotEmpty) { + return ListView( children: [ - Expanded( - child: Row( - children: [ - Container( - margin: const EdgeInsets.only(right: 16), - child: leadingIcon(servers[index]), - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "${server.connectionMethod}://${server.domain}${server.path ?? ""}${server.port != null ? ':${server.port}' : ""}", - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurface - ), - ), - Column( - children: [ - const SizedBox(height: 3), - Text( - servers[index].name, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurfaceVariant - ), - ) - ], - ) - ], - ), - ), - ], - ), - ), - RotationTransition( - turns: animation, - child: const Icon(Icons.keyboard_arrow_down_rounded), + Wrap( + children: serversProvider.serversList.asMap().entries.map( + (s) => ServersTileItem( + breakingWidth: breakingWidth, + server: serversProvider.serversList[s.key], + index: s.key, + onChange: onChange + ) + ).toList(), ), + const SizedBox(height: 8) ], ); } - - Widget bottomRow(Server server, int index) { - return Column( - children: [ - const SizedBox(height: 20), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - PopupMenuButton( - // color: Theme.of(context).dialogBackgroundColor, - itemBuilder: (context) => [ - PopupMenuItem( - enabled: server.defaultServer == false - ? true - : false, - onTap: server.defaultServer == false - ? (() => setDefaultServer(server)) - : null, - child: SizedBox( - child: Row( - children: [ - const Icon(Icons.star), - const SizedBox(width: 15), - Text( - server.defaultServer == true - ? AppLocalizations.of(context)!.defaultConnection - : AppLocalizations.of(context)!.setDefault, - ) - ], - ), - ) - ), - PopupMenuItem( - onTap: (() => openAddServerBottomSheet(server: server)), - child: Row( - children: [ - const Icon(Icons.edit), - const SizedBox(width: 15), - Text(AppLocalizations.of(context)!.edit) - ], - ) - ), - PopupMenuItem( - onTap: (() => showDeleteModal(server)), - child: Row( - children: [ - const Icon(Icons.delete), - const SizedBox(width: 15), - Text(AppLocalizations.of(context)!.delete) - ], - ) - ), - ] - ), - SizedBox( - child: serversProvider.selectedServer != null && serversProvider.selectedServer?.id == servers[index].id - ? Container( - margin: const EdgeInsets.only(right: 12), - padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10), - decoration: BoxDecoration( - color: serversProvider.serverStatus.data != null - ? Colors.green - : Colors.orange, - borderRadius: BorderRadius.circular(30) - ), - child: Row( - children: [ - Icon( - serversProvider.serverStatus.data != null - ? Icons.check - : Icons.warning, - color: Colors.white, - ), - const SizedBox(width: 10), - Text( - serversProvider.serverStatus.data != null - ? AppLocalizations.of(context)!.connected - : AppLocalizations.of(context)!.selectedDisconnected, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.w500 - ), - ) - ], - ), - ) - : Container( - margin: const EdgeInsets.only(right: 10), - child: TextButton( - onPressed: () => connectToServer(servers[index]), - child: Text(AppLocalizations.of(context)!.connect), - ), - ), - ) - ], - ) - ], + else { + return SizedBox( + height: double.maxFinite, + child: Center( + child: Text( + AppLocalizations.of(context)!.noSavedConnections, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), ); } - - return servers.isNotEmpty ? - ListView.builder( - controller: widget.scrollController, - itemCount: servers.length, - itemBuilder: (context, index) => Container( - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: Theme.of(context).colorScheme.surfaceVariant, - width: 1 - ) - ) - ), - child: ExpandableNotifier( - controller: widget.controllers[index], - child: Column( - children: [ - Expandable( - collapsed: Material( - color: Colors.transparent, - child: InkWell( - onTap: () => widget.onChange(index), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: topRow(servers[index], index), - ), - ), - ), - expanded: Material( - color: Colors.transparent, - child: InkWell( - onTap: () => widget.onChange(index), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Column( - children: [ - topRow(servers[index], index), - bottomRow(servers[index], index) - ], - ), - ), - ), - ) - ) - ], - ), - ), - ) - ) : SizedBox( - height: double.maxFinite, - child: Center( - child: Text( - AppLocalizations.of(context)!.noSavedConnections, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ), - ); } } \ No newline at end of file diff --git a/lib/widgets/servers_list/servers_list_item.dart b/lib/widgets/servers_list/servers_list_item.dart new file mode 100644 index 0000000..29b11a5 --- /dev/null +++ b/lib/widgets/servers_list/servers_list_item.dart @@ -0,0 +1,361 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/material.dart'; +import 'package:expandable/expandable.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/widgets/servers_list/server_tile_functions.dart'; + +import 'package:adguard_home_manager/providers/status_provider.dart'; +import 'package:adguard_home_manager/models/server.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; + +class ServersListItem extends StatefulWidget { + final ExpandableController expandableController; + final Server server; + final int index; + final void Function(int) onChange; + + const ServersListItem({ + super.key, + required this.expandableController, + required this.server, + required this.index, + required this.onChange + }); + + @override + State createState() => _ServersListItemState(); +} + +class _ServersListItemState extends State with SingleTickerProviderStateMixin { + late AnimationController animationController; + late Animation animation; + + @override + void initState() { + widget.expandableController.addListener(() async { + await Future.delayed(const Duration(milliseconds: 200)); + if (widget.expandableController.value == false) { + animationController.animateTo(0); + } + else { + animationController.animateBack(1); + } + }); + + animationController = AnimationController( + duration: const Duration(milliseconds: 200), + vsync: this, + ); + animation = Tween( + begin: 0.0, + end: 0.5, + ).animate(CurvedAnimation( + parent: animationController, + curve: Curves.easeInOut + )); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width; + + return Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + width: 1 + ) + ) + ), + child: ExpandableNotifier( + controller: widget.expandableController, + child: Column( + children: [ + Expandable( + collapsed: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => widget.onChange(widget.index), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: _TopRow(server: widget.server, animation: animation) + ), + ), + ), + expanded: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => widget.onChange(widget.index), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Column( + children: [ + _TopRow(server: widget.server, animation: animation), + _BottomRow( + server: widget.server, + connectToServer: (s) => connectToServer(context: context, server: s), + openServerModal: (s) => openServerModal(context: context, server: s, width: width), + setDefaultServer: (s) => setDefaultServer(context: context, server: s), + showDeleteModal: (s) => showDeleteModal(context: context, server: s), + ) + ], + ), + ), + ), + ) + ) + ], + ), + ), + ); + } +} + +class _TopRow extends StatelessWidget { + final Server server; + final Animation animation; + + const _TopRow({ + required this.server, + required this.animation, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + children: [ + Container( + margin: const EdgeInsets.only(right: 16), + child: _LeadingIcon(server: server), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${server.connectionMethod}://${server.domain}${server.path ?? ""}${server.port != null ? ':${server.port}' : ""}", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface + ), + ), + Column( + children: [ + const SizedBox(height: 3), + Text( + server.name, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ) + ], + ) + ], + ), + ), + ], + ), + ), + RotationTransition( + turns: animation, + child: const Icon(Icons.keyboard_arrow_down_rounded), + ), + ], + ); + } +} + +class _LeadingIcon extends StatelessWidget { + final Server server; + + const _LeadingIcon({ + required this.server, + }); + + @override + Widget build(BuildContext context) { + final serversProvider = Provider.of(context); + final statusProvider = Provider.of(context); + + if (server.defaultServer == true) { + return Stack( + alignment: Alignment.center, + children: [ + Icon( + Icons.storage_rounded, + color: serversProvider.selectedServer != null && serversProvider.selectedServer?.id == server.id + ? statusProvider.serverStatus != null + ? Colors.green + : Colors.orange + : null, + ), + SizedBox( + width: 25, + height: 25, + child: Stack( + alignment: Alignment.bottomRight, + children: [ + Container( + padding: const EdgeInsets.all(1), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(20) + ), + child: Icon( + Icons.star, + color: Theme.of(context).colorScheme.onPrimaryContainer, + size: 10, + ), + ), + ], + ), + ) + ], + ); + } + else { + return Icon( + Icons.storage_rounded, + color: serversProvider.selectedServer != null && serversProvider.selectedServer?.id == server.id + ? statusProvider.serverStatus != null + ? Colors.green + : Colors.orange + : null, + ); + } + } +} + +class _BottomRow extends StatelessWidget { + final Server server; + final void Function(Server) setDefaultServer; + final void Function(Server) openServerModal; + final void Function(Server) showDeleteModal; + final void Function(Server) connectToServer; + + const _BottomRow({ + required this.server, + required this.setDefaultServer, + required this.openServerModal, + required this.showDeleteModal, + required this.connectToServer, + }); + + @override + Widget build(BuildContext context) { + final serversProvider = Provider.of(context); + final statusProvider = Provider.of(context); + + return Column( + children: [ + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + PopupMenuButton( + // color: Theme.of(context).dialogBackgroundColor, + itemBuilder: (context) => [ + PopupMenuItem( + enabled: server.defaultServer == false + ? true + : false, + onTap: server.defaultServer == false + ? (() => setDefaultServer(server)) + : null, + child: SizedBox( + child: Row( + children: [ + const Icon(Icons.star), + const SizedBox(width: 15), + Text( + server.defaultServer == true + ? AppLocalizations.of(context)!.defaultConnection + : AppLocalizations.of(context)!.setDefault, + ) + ], + ), + ) + ), + PopupMenuItem( + onTap: (() => openServerModal(server)), + child: Row( + children: [ + const Icon(Icons.edit), + const SizedBox(width: 15), + Text(AppLocalizations.of(context)!.edit) + ], + ) + ), + PopupMenuItem( + onTap: (() => showDeleteModal(server)), + child: Row( + children: [ + const Icon(Icons.delete), + const SizedBox(width: 15), + Text(AppLocalizations.of(context)!.delete) + ], + ) + ), + ] + ), + SizedBox( + child: serversProvider.selectedServer != null && serversProvider.selectedServer?.id == server.id + ? Container( + margin: const EdgeInsets.only(right: 12), + padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10), + decoration: BoxDecoration( + color: statusProvider.serverStatus != null + ? Colors.green + : Colors.orange, + borderRadius: BorderRadius.circular(30) + ), + child: Row( + children: [ + Icon( + statusProvider.serverStatus != null + ? Icons.check + : Icons.warning, + color: Colors.white, + ), + const SizedBox(width: 10), + Text( + statusProvider.serverStatus != null + ? AppLocalizations.of(context)!.connected + : AppLocalizations.of(context)!.selectedDisconnected, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500 + ), + ) + ], + ), + ) + : Container( + margin: const EdgeInsets.only(right: 10), + child: TextButton( + onPressed: () => connectToServer(server), + child: Text(AppLocalizations.of(context)!.connect), + ), + ), + ) + ], + ) + ], + ); + } +} \ No newline at end of file diff --git a/lib/widgets/servers_list/servers_tile_item.dart b/lib/widgets/servers_list/servers_tile_item.dart new file mode 100644 index 0000000..bf28ade --- /dev/null +++ b/lib/widgets/servers_list/servers_tile_item.dart @@ -0,0 +1,306 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/widgets/servers_list/server_tile_functions.dart'; + +import 'package:adguard_home_manager/providers/status_provider.dart'; +import 'package:adguard_home_manager/models/server.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; + +class ServersTileItem extends StatefulWidget { + final Server server; + final int index; + final void Function(int) onChange; + final double breakingWidth; + + const ServersTileItem({ + super.key, + required this.server, + required this.index, + required this.onChange, + required this.breakingWidth + }); + + @override + State createState() => _ServersTileItemState(); +} + +class _ServersTileItemState extends State with SingleTickerProviderStateMixin { + @override + Widget build(BuildContext context) { + final serversProvider = Provider.of(context); + + final width = MediaQuery.of(context).size.width; + + return FractionallySizedBox( + widthFactor: width > widget.breakingWidth ? 0.5 : 1, + child: Card( + margin: width > widget.breakingWidth + ? generateMargins(index: widget.index, serversListLength: serversProvider.serversList.length) + : const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: _TopRow(server: widget.server, index: widget.index) + ), + Padding( + padding: const EdgeInsets.only( + left: 8, right: 8, bottom: 16 + ), + child: _BottomRow( + server: widget.server, + connectToServer: (s) => connectToServer(context: context, server: s), + openServerModal: (s) => openServerModal(context: context, server: s, width: width), + setDefaultServer: (s) => setDefaultServer(context: context, server: s), + showDeleteModal: (s) => showDeleteModal(context: context, server: s), + ) + ) + ], + ), + ), + ); + } +} + +class _LeadingIcon extends StatelessWidget { + final Server server; + + const _LeadingIcon({ + required this.server, + }); + + @override + Widget build(BuildContext context) { + final serversProvider = Provider.of(context); + final statusProvider = Provider.of(context); + + if (server.defaultServer == true) { + return Stack( + alignment: Alignment.center, + children: [ + Icon( + Icons.storage_rounded, + color: serversProvider.selectedServer != null && serversProvider.selectedServer?.id == server.id + ? statusProvider.serverStatus != null + ? Colors.green + : Colors.orange + : null, + ), + SizedBox( + width: 25, + height: 25, + child: Stack( + alignment: Alignment.bottomRight, + children: [ + Container( + padding: const EdgeInsets.all(1), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(20) + ), + child: Icon( + Icons.star, + color: Theme.of(context).colorScheme.onPrimaryContainer, + size: 10, + ), + ), + ], + ), + ) + ], + ); + } + else { + return Icon( + Icons.storage_rounded, + color: serversProvider.selectedServer != null && serversProvider.selectedServer?.id == server.id + ? statusProvider.serverStatus != null + ? Colors.green + : Colors.orange + : null, + ); + } + } +} + +class _TopRow extends StatelessWidget { + final Server server; + final int index; + + const _TopRow({ + required this.server, + required this.index, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + children: [ + Container( + margin: const EdgeInsets.only(right: 16), + child: _LeadingIcon(server: server), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${server.connectionMethod}://${server.domain}${server.path ?? ""}${server.port != null ? ':${server.port}' : ""}", + textAlign: TextAlign.left, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface + ), + ), + Column( + children: [ + const SizedBox(height: 3), + Text( + server.name, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ) + ], + ) + ], + ), + ), + ], + ), + ), + ], + ); + } +} + +class _BottomRow extends StatelessWidget { + final Server server; + final void Function(Server) setDefaultServer; + final void Function(Server) openServerModal; + final void Function(Server) showDeleteModal; + final void Function(Server) connectToServer; + + const _BottomRow({ + required this.server, + required this.setDefaultServer, + required this.openServerModal, + required this.showDeleteModal, + required this.connectToServer, + }); + + @override + Widget build(BuildContext context) { + final serversProvider = Provider.of(context); + final statusProvider = Provider.of(context); + + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + enabled: server.defaultServer == false + ? true + : false, + onTap: server.defaultServer == false + ? (() => setDefaultServer(server)) + : null, + child: SizedBox( + child: Row( + children: [ + const Icon(Icons.star), + const SizedBox(width: 15), + Text( + server.defaultServer == true + ? AppLocalizations.of(context)!.defaultConnection + : AppLocalizations.of(context)!.setDefault, + ) + ], + ), + ) + ), + PopupMenuItem( + onTap: (() => openServerModal(server)), + child: Row( + children: [ + const Icon(Icons.edit), + const SizedBox(width: 15), + Text(AppLocalizations.of(context)!.edit) + ], + ) + ), + PopupMenuItem( + onTap: (() => showDeleteModal(server)), + child: Row( + children: [ + const Icon(Icons.delete), + const SizedBox(width: 15), + Text(AppLocalizations.of(context)!.delete) + ], + ) + ), + ] + ), + SizedBox( + child: serversProvider.selectedServer != null && + serversProvider.selectedServer != null && serversProvider.selectedServer?.id == server.id && statusProvider.serverStatus != null && + serversProvider.selectedServer?.id == server.id + ? Padding( + padding: const EdgeInsets.only(right: 16), + child: Row( + children: [ + Icon( + serversProvider.selectedServer != null && serversProvider.selectedServer?.id == server.id && statusProvider.serverStatus != null + ? Icons.check + : Icons.warning, + color: serversProvider.selectedServer != null && serversProvider.selectedServer?.id == server.id && statusProvider.serverStatus != null + ? Colors.green + : Colors.orange, + ), + const SizedBox(width: 10), + Text( + serversProvider.selectedServer != null && serversProvider.selectedServer?.id == server.id && statusProvider.serverStatus != null + ? AppLocalizations.of(context)!.connected + : AppLocalizations.of(context)!.selectedDisconnected, + style: TextStyle( + color: serversProvider.selectedServer != null && serversProvider.selectedServer?.id == server.id && statusProvider.serverStatus != null + ? Colors.green + : Colors.orange, + fontWeight: FontWeight.w500 + ), + ) + ], + ), + ) + : Container( + margin: const EdgeInsets.only(right: 10), + child: FilledButton.icon( + icon: const Icon(Icons.login), + onPressed: () => connectToServer(server), + label: Text(AppLocalizations.of(context)!.connect), + ), + ), + ) + ], + ) + ], + ); + } +} \ No newline at end of file diff --git a/lib/widgets/system_ui_overlay_style.dart b/lib/widgets/system_ui_overlay_style.dart new file mode 100644 index 0000000..ab5b8d0 --- /dev/null +++ b/lib/widgets/system_ui_overlay_style.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class OverlayStyle extends StatelessWidget { + final Widget child; + + const OverlayStyle({ + super.key, + required this.child + }); + + @override + Widget build(BuildContext context) { + final systemGestureInsets = MediaQuery.of(context).systemGestureInsets; + + return AnnotatedRegion( + value: SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarBrightness: Theme.of(context).brightness == Brightness.light + ? Brightness.light + : Brightness.dark, + statusBarIconBrightness: Theme.of(context).brightness == Brightness.light + ? Brightness.dark + : Brightness.light, + systemNavigationBarColor: systemGestureInsets.left > 0 // If true gestures navigation + ? Colors.transparent + : ElevationOverlay.applySurfaceTint( + Theme.of(context).colorScheme.surface, + Theme.of(context).colorScheme.surfaceTint, + 3 + ), + systemNavigationBarIconBrightness: Theme.of(context).brightness == Brightness.light + ? Brightness.dark + : Brightness.light, + ), + child: child, + ); + } +} \ No newline at end of file diff --git a/lib/widgets/tab_content_list.dart b/lib/widgets/tab_content_list.dart new file mode 100644 index 0000000..0e09a4f --- /dev/null +++ b/lib/widgets/tab_content_list.dart @@ -0,0 +1,203 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'package:adguard_home_manager/providers/app_config_provider.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; + +class CustomTabContentList extends StatelessWidget { + final Widget Function() loadingGenerator; + final int itemsCount; + final Widget Function(int index) contentWidget; + final Widget noData; + final Widget Function() errorGenerator; + final LoadStatus loadStatus; + final Future Function() onRefresh; + final double? refreshIndicatorOffset; + final Widget? fab; + final bool? fabVisible; + final bool? noSliver; + final EdgeInsets? listPadding; + final double? heightFabHidden; + + const CustomTabContentList({ + super.key, + required this.loadingGenerator, + required this.itemsCount, + required this.contentWidget, + required this.noData, + required this.errorGenerator, + required this.loadStatus, + required this.onRefresh, + this.refreshIndicatorOffset, + this.fab, + this.fabVisible, + this.noSliver, + this.listPadding, + this.heightFabHidden, + }); + + @override + Widget build(BuildContext context) { + final appConfigProvider = Provider.of(context); + + switch (loadStatus) { + case LoadStatus.loading: + if (noSliver == true) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: loadingGenerator() + ); + } + else { + return SafeArea( + top: false, + child: Builder( + builder: (BuildContext context) => CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + ), + SliverFillRemaining( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: loadingGenerator() + ), + ) + ], + ), + ) + ); + } + + + case LoadStatus.loaded: + if (noSliver == true) { + if (itemsCount > 0) { + return SafeArea( + child: Stack( + children: [ + ListView.builder( + padding: listPadding, + itemCount: itemsCount, + itemBuilder: (context, index) => contentWidget(index), + ), + if (fab != null) AnimatedPositioned( + duration: const Duration(milliseconds: 100), + curve: Curves.easeInOut, + bottom: fabVisible != null && fabVisible == true ? + appConfigProvider.showingSnackbar + ? 70 : 20 + : -70, + right: 20, + child: fab! + ), + ], + ), + ); + } + else { + return SafeArea( + child: Stack( + children: [ + noData, + if (fab != null) AnimatedPositioned( + duration: const Duration(milliseconds: 100), + curve: Curves.easeInOut, + bottom: fabVisible != null && fabVisible == true ? + appConfigProvider.showingSnackbar + ? 70 : 20 + : -70, + right: 20, + child: fab! + ), + ], + ), + ); + } + } + else { + return Stack( + children: [ + SafeArea( + top: false, + bottom: false, + child: Builder( + builder: (BuildContext context) { + return RefreshIndicator( + onRefresh: onRefresh, + edgeOffset: refreshIndicatorOffset ?? 70, + child: CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + ), + if (itemsCount > 0) SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) => contentWidget(index), + childCount: itemsCount + ), + ), + if (itemsCount == 0) SliverFillRemaining( + child: noData, + ) + ], + ), + ); + }, + ), + ), + if (fab != null) AnimatedPositioned( + duration: const Duration(milliseconds: 100), + curve: Curves.easeInOut, + bottom: fabVisible != null && fabVisible == true ? + appConfigProvider.showingSnackbar + ? 90 : 20 + : (heightFabHidden ?? -90), + right: 20, + child: SafeArea(child: fab!) + ), + ], + ); + } + + case LoadStatus.error: + if (noSliver == true) { + return Padding( + padding: const EdgeInsets.only( + top: 95, + left: 16, + right: 16 + ), + child: errorGenerator() + ); + } + else { + return SafeArea( + top: false, + child: Builder( + builder: (BuildContext context) => CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + ), + SliverFillRemaining( + child: Padding( + padding: const EdgeInsets.only( + top: 95, + left: 16, + right: 16 + ), + child: errorGenerator() + ), + ) + ], + ), + ) + ); + } + + default: + return const SizedBox(); + } + } +} \ No newline at end of file diff --git a/lib/widgets/update_modal.dart b/lib/widgets/update_modal.dart index b3acc8d..8cedd2e 100644 --- a/lib/widgets/update_modal.dart +++ b/lib/widgets/update_modal.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:adguard_home_manager/functions/app_update_download_link.dart'; import 'package:adguard_home_manager/models/github_release.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; @@ -10,10 +11,10 @@ class UpdateModal extends StatefulWidget { final void Function(String, String) onDownload; const UpdateModal({ - Key? key, + super.key, required this.gitHubRelease, required this.onDownload, - }) : super(key: key); + }); @override State createState() => _UpdateModalState(); @@ -22,14 +23,12 @@ class UpdateModal extends StatefulWidget { class _UpdateModalState extends State { bool doNotRemember = false; - String getDownloadLink() { - return widget.gitHubRelease.assets.firstWhere((item) => item.browserDownloadUrl.contains('apk')).browserDownloadUrl; - } - @override Widget build(BuildContext context) { final appConfigProvider = Provider.of(context); + final downloadLink = getAppUpdateDownloadLink(widget.gitHubRelease); + return AlertDialog( scrollable: true, title: Column( @@ -61,7 +60,7 @@ class _UpdateModalState extends State { ), const SizedBox(height: 10), Text( - "${AppLocalizations.of(context)!.newVersion}: ${widget.gitHubRelease.tagName}", + "${AppLocalizations.of(context)!.newVersion}: ${widget.gitHubRelease.name.replaceAll('v', '')}", style: TextStyle( color: Theme.of(context).colorScheme.onSurfaceVariant ), @@ -102,12 +101,14 @@ class _UpdateModalState extends State { ), actions: [ Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: downloadLink != null + ? MainAxisAlignment.spaceBetween + : MainAxisAlignment.end, children: [ - TextButton( + if (downloadLink != null) TextButton( onPressed: () { Navigator.pop(context); - widget.onDownload(getDownloadLink(), widget.gitHubRelease.tagName); + widget.onDownload(downloadLink, widget.gitHubRelease.tagName); }, child: Text(AppLocalizations.of(context)!.download) ), diff --git a/lib/widgets/version_warning_modal.dart b/lib/widgets/version_warning_modal.dart new file mode 100644 index 0000000..72dbe4d --- /dev/null +++ b/lib/widgets/version_warning_modal.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class VersionWarningModal extends StatelessWidget { + final String version; + + const VersionWarningModal({ + super.key, + required this.version, + }); + + @override + Widget build(BuildContext context) { + return AlertDialog( + scrollable: true, + title: Column( + children: [ + Icon( + Icons.warning_rounded, + size: 24, + color: Theme.of(context).listTileTheme.iconColor + ), + const SizedBox(height: 16), + Text( + AppLocalizations.of(context)!.unsupportedVersion, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface + ), + ) + ], + ), + content: Text(AppLocalizations.of(context)!.unsupprtedVersionMessage(version)), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.iUnderstand) + ) + ], + ) + ], + ); + } +} \ No newline at end of file diff --git a/linux/.gitignore b/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt new file mode 100644 index 0000000..273981d --- /dev/null +++ b/linux/CMakeLists.txt @@ -0,0 +1,138 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "AdGuardHomeManager") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.jgeek00.adguard_home_manager") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/linux/build-app.sh b/linux/build-app.sh new file mode 100755 index 0000000..513fd3b --- /dev/null +++ b/linux/build-app.sh @@ -0,0 +1,30 @@ +#!/bin/bash + + +# Build the Flutter app and package into an archive. + + +# Exit if any command fails +set -e + +# Echo all commands for debug purposes +set -x + + +projectName=AdGuard-Home-Manager + +archiveName=$projectName-Linux.tar.gz +baseDir=$(pwd) + + +# ----------------------------- Build Flutter app ---------------------------- # + +flutter clean +flutter pub get +flutter build linux --release + +flutter_to_debian + +cd build/linux/x64/release/bundle || exit +mv "AdGuardHomeManager" "AdGuard Home Manager" +tar -czaf $archiveName ./* \ No newline at end of file diff --git a/linux/flutter/CMakeLists.txt b/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..4fbee04 --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,35 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include +#include +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) dynamic_color_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); + dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); + g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin"); + screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar); + g_autoptr(FlPluginRegistrar) sentry_flutter_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "SentryFlutterPlugin"); + sentry_flutter_plugin_register_with_registrar(sentry_flutter_registrar); + g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); + sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); + g_autoptr(FlPluginRegistrar) window_manager_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); + window_manager_plugin_register_with_registrar(window_manager_registrar); +} diff --git a/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..787cc64 --- /dev/null +++ b/linux/flutter/generated_plugins.cmake @@ -0,0 +1,29 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + dynamic_color + screen_retriever_linux + sentry_flutter + sqlite3_flutter_libs + url_launcher_linux + window_manager +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/linux/main.cc b/linux/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/linux/my_application.cc b/linux/my_application.cc new file mode 100644 index 0000000..49e0a92 --- /dev/null +++ b/linux/my_application.cc @@ -0,0 +1,111 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "AdGuard Home Manager"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "AdGuard Home Manager"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + + g_autoptr(GError) error = nullptr; + gtk_window_set_icon_from_file(window, g_strconcat(fl_dart_project_get_assets_path(project), "/assets/icon/icon-circle-256.png", NULL), &error); + if (error != nullptr) { + g_warning("Failed to set icon: %s", error->message); + } + + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/linux/my_application.h b/linux/my_application.h new file mode 100644 index 0000000..72271d5 --- /dev/null +++ b/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/macos/.gitignore b/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..4b81f9b --- /dev/null +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..5caa9d1 --- /dev/null +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..69e9dbc --- /dev/null +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,30 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import device_info_plus +import dynamic_color +import package_info_plus +import screen_retriever_macos +import sentry_flutter +import shared_preferences_foundation +import sqflite_darwin +import sqlite3_flutter_libs +import url_launcher_macos +import window_manager + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) + DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) + SentryFlutterPlugin.register(with: registry.registrar(forPlugin: "SentryFlutterPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) +} diff --git a/macos/Podfile b/macos/Podfile new file mode 100644 index 0000000..049abe2 --- /dev/null +++ b/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.14' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/macos/Podfile.lock b/macos/Podfile.lock new file mode 100644 index 0000000..ae5c12f --- /dev/null +++ b/macos/Podfile.lock @@ -0,0 +1,105 @@ +PODS: + - device_info_plus (0.0.1): + - FlutterMacOS + - dynamic_color (0.0.2): + - FlutterMacOS + - FlutterMacOS (1.0.0) + - package_info_plus (0.0.1): + - FlutterMacOS + - screen_retriever_macos (0.0.1): + - FlutterMacOS + - Sentry/HybridSDK (8.44.0) + - sentry_flutter (8.13.2): + - Flutter + - FlutterMacOS + - Sentry/HybridSDK (= 8.44.0) + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - sqflite_darwin (0.0.4): + - Flutter + - FlutterMacOS + - sqlite3 (3.49.1): + - sqlite3/common (= 3.49.1) + - sqlite3/common (3.49.1) + - sqlite3/dbstatvtab (3.49.1): + - sqlite3/common + - sqlite3/fts5 (3.49.1): + - sqlite3/common + - sqlite3/perf-threadsafe (3.49.1): + - sqlite3/common + - sqlite3/rtree (3.49.1): + - sqlite3/common + - sqlite3_flutter_libs (0.0.1): + - Flutter + - FlutterMacOS + - sqlite3 (~> 3.49.1) + - sqlite3/dbstatvtab + - sqlite3/fts5 + - sqlite3/perf-threadsafe + - sqlite3/rtree + - url_launcher_macos (0.0.1): + - FlutterMacOS + - window_manager (0.2.0): + - FlutterMacOS + +DEPENDENCIES: + - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) + - dynamic_color (from `Flutter/ephemeral/.symlinks/plugins/dynamic_color/macos`) + - FlutterMacOS (from `Flutter/ephemeral`) + - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) + - screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`) + - sentry_flutter (from `Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos`) + - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) + - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) + - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) + +SPEC REPOS: + trunk: + - Sentry + - sqlite3 + +EXTERNAL SOURCES: + device_info_plus: + :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos + dynamic_color: + :path: Flutter/ephemeral/.symlinks/plugins/dynamic_color/macos + FlutterMacOS: + :path: Flutter/ephemeral + package_info_plus: + :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos + screen_retriever_macos: + :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos + sentry_flutter: + :path: Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos + shared_preferences_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin + sqflite_darwin: + :path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin + sqlite3_flutter_libs: + :path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + window_manager: + :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos + +SPEC CHECKSUMS: + device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215 + dynamic_color: 2eaa27267de1ca20d879fbd6e01259773fb1670f + FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b + screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161 + Sentry: 0f9bc9adfc0b960e7f3bb5ec67e9a3d8193f3bdb + sentry_flutter: 64a43fb39ab4c7f67d8a4cad52b49e22439e58b7 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d + sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983 + sqlite3_flutter_libs: cc304edcb8e1d8c595d1b08c7aeb46a47691d9db + url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 + window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 + +PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7 + +COCOAPODS: 1.15.2 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..f7a463a --- /dev/null +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,653 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + E73513DE6359E03D97EE7354 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7D05500669291AB89B123249 /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0C04B50F31022114B20541C8 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 2B20822A5E5ADA2133261EFB /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* AdGuard Home Manager.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "AdGuard Home Manager.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 7D05500669291AB89B123249 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + 985E47E8D393FC1DF77CB4F3 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E73513DE6359E03D97EE7354 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 46371A716FC111389EADDE5D /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* AdGuard Home Manager.app */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + 46371A716FC111389EADDE5D /* Pods */ = { + isa = PBXGroup; + children = ( + 2B20822A5E5ADA2133261EFB /* Pods-Runner.debug.xcconfig */, + 985E47E8D393FC1DF77CB4F3 /* Pods-Runner.release.xcconfig */, + 0C04B50F31022114B20541C8 /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 7D05500669291AB89B123249 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 7C0020C10EF81232492FC538 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + CC375DDE73754317056A739B /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* AdGuard Home Manager.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 7C0020C10EF81232492FC538 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + CC375DDE73754317056A739B /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 63; + DEVELOPMENT_TEAM = 38Z3B9TJTR; + INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "AdGuard Home Manager"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MARKETING_VERSION = 2.4.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.jgeek00.adguard-home-manager"; + PRODUCT_COPYRIGHT = "Copyright © 2023 JGeek00. All rights reserved."; + PRODUCT_NAME = "AdGuard Home Manager"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 63; + DEVELOPMENT_TEAM = 38Z3B9TJTR; + INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "AdGuard Home Manager"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MARKETING_VERSION = 2.4.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.jgeek00.adguard-home-manager"; + PRODUCT_COPYRIGHT = "Copyright © 2023 JGeek00. All rights reserved."; + PRODUCT_NAME = "AdGuard Home Manager"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 63; + DEVELOPMENT_TEAM = 38Z3B9TJTR; + INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "AdGuard Home Manager"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MARKETING_VERSION = 2.4.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.jgeek00.adguard-home-manager"; + PRODUCT_COPYRIGHT = "Copyright © 2023 JGeek00. All rights reserved."; + PRODUCT_NAME = "AdGuard Home Manager"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..dcd4e29 --- /dev/null +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..b3c1761 --- /dev/null +++ b/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..96d3fee --- /dev/null +++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "info": { + "version": 1, + "author": "xcode" + }, + "images": [ + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_16.png", + "scale": "1x" + }, + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "2x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "1x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_64.png", + "scale": "2x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_128.png", + "scale": "1x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "2x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "1x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "2x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "1x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_1024.png", + "scale": "2x" + } + ] +} \ No newline at end of file diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..8b1c839 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..2f23ec2 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..066823a Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..5f8cce7 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..7856b4a Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..e851645 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..1a2df1d Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/macos/Runner/Base.lproj/MainMenu.xib b/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..ea900ed --- /dev/null +++ b/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,344 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..01712f7 --- /dev/null +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = adguard_home_manager + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.jgeek00.adguardHomeManager + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2023 com.jgeek00. All rights reserved. diff --git a/macos/Runner/Configs/Debug.xcconfig b/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Release.xcconfig b/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Warnings.xcconfig b/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..08c3ab1 --- /dev/null +++ b/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.network.client + + + diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist new file mode 100644 index 0000000..a7cd9c5 --- /dev/null +++ b/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + AdGuard Home Manager + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..2722837 --- /dev/null +++ b/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements new file mode 100644 index 0000000..ee95ab7 --- /dev/null +++ b/macos/Runner/Release.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/pubspec.lock b/pubspec.lock index 4225038..207783c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,198 +1,166 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + after_layout: + dependency: transitive + description: + name: after_layout + sha256: "95a1cb2ca1464f44f14769329fbf15987d20ab6c88f8fc5d359bd362be625f29" + url: "https://pub.dev" + source: hosted + version: "1.2.0" animations: dependency: "direct main" description: name: animations - sha256: fe8a6bdca435f718bb1dc8a11661b2c22504c6da40ef934cee8327ed77934164 + sha256: d3d6dcfb218225bbe68e87ccf6378bbb2e32a94900722c5f81611dad089911cb url: "https://pub.dev" source: hosted - version: "2.0.7" + version: "2.0.11" + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" + url: "https://pub.dev" + source: hosted + version: "2.0.3" archive: dependency: transitive description: name: archive - sha256: ed7cc591a948744994714375caf9a2ce89e1d82e8243997c8a2994d57181c212 + sha256: "7dcbd0f87fe5f61cb28da39a1a8b70dbc106e2fe0516f7836eb7bb2948481a12" url: "https://pub.dev" source: hosted - version: "3.3.5" + version: "4.0.5" args: dependency: transitive description: name: args - sha256: "139d809800a412ebb26a3892da228b2d0ba36f0ef5d9a82166e5e52ec8d61611" + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.7.0" async: - dependency: transitive + dependency: "direct main" description: name: async - sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.12.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" - bottom_inset_observer: - dependency: transitive - description: - name: bottom_inset_observer - sha256: cbfb01e0e07cc4922052701786d5e607765a6f54e1844f41061abf8744519a7d - url: "https://pub.dev" - source: hosted - version: "3.1.0" - bottom_sheet: - dependency: "direct main" - description: - name: bottom_sheet - sha256: "7a3d4a1515eba91a7d9e1359e49416147de339889170fc879a8b905d27958c94" - url: "https://pub.dev" - source: hosted - version: "3.1.2" + version: "2.1.2" characters: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.4.0" checked_yaml: dependency: transitive description: name: checked_yaml - sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.0.3" cli_util: dependency: transitive description: name: cli_util - sha256: "66f86e916d285c1a93d3b79587d94bd71984a66aac4ff74e524cfa7877f1395c" + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c url: "https://pub.dev" source: hosted - version: "0.3.5" + version: "0.4.2" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.17.0" - convert: - dependency: transitive + version: "1.19.1" + contextmenu: + dependency: "direct main" description: - name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + name: contextmenu + sha256: e0c7d60e2fc9f316f5b03f5fe2c0f977d65125345d1a1f77eea02be612e32d0c url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.0.0" crypto: dependency: transitive description: name: crypto - sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.6" csslib: dependency: transitive description: name: csslib - sha256: b36c7f7e24c0bdf1bf9a3da461c837d1de64b9f8beb190c9011d8c72a3dfd745 + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" url: "https://pub.dev" source: hosted - version: "0.17.2" + version: "1.0.2" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.8" device_info_plus: dependency: "direct main" description: name: device_info_plus - sha256: b809c4ed5f7fcdb325ccc70b80ad934677dc4e2aa414bf46859a42bfdfafcbb6 + sha256: "306b78788d1bb569edb7c55d622953c2414ca12445b41c9117963e03afc5c513" url: "https://pub.dev" source: hosted - version: "4.1.3" - device_info_plus_linux: - dependency: transitive - description: - name: device_info_plus_linux - sha256: "77a8b3c4af06bc46507f89304d9f49dfc64b4ae004b994532ed23b34adeae4b3" - url: "https://pub.dev" - source: hosted - version: "3.0.0" - device_info_plus_macos: - dependency: transitive - description: - name: device_info_plus_macos - sha256: "37961762fbd46d3620c7b69ca606671014db55fc1b7a11e696fd90ed2e8fe03d" - url: "https://pub.dev" - source: hosted - version: "3.0.0" + version: "11.3.3" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - sha256: "83fdba24fcf6846d3b10f10dfdc8b6c6d7ada5f8ed21d62ea2909c2dfa043773" + sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2" url: "https://pub.dev" source: hosted - version: "3.0.0" - device_info_plus_web: - dependency: transitive - description: - name: device_info_plus_web - sha256: "5890f6094df108181c7a29720bc23d0fd6159f17d82787fac093d1fefcaf6325" - url: "https://pub.dev" - source: hosted - version: "3.0.0" - device_info_plus_windows: - dependency: transitive - description: - name: device_info_plus_windows - sha256: "23a2874af0e23ee6e3a2a0ebcecec3a9da13241f2cb93a93a44c8764df123dd7" - url: "https://pub.dev" - source: hosted - version: "4.1.0" + version: "7.0.2" dynamic_color: dependency: "direct main" description: name: dynamic_color - sha256: "37a15576f5a0bfd5555b613cf20ea3bd379607cf88d457374a16032f4e942174" + sha256: eae98052fa6e2826bdac3dd2e921c6ce2903be15c6b7f8b6d8a5d49b5086298d url: "https://pub.dev" source: hosted - version: "1.5.4" + version: "1.7.0" equatable: dependency: transitive description: name: equatable - sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.0.7" expandable: dependency: "direct main" description: @@ -205,190 +173,286 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" ffi: dependency: transitive description: name: ffi - sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.4" file: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" fl_chart: dependency: "direct main" description: name: fl_chart - sha256: "749b3342ea3e95cbf61a0fec31a62606e837377b8b6d0caa7367a7ef80f38b7d" + sha256: "5276944c6ffc975ae796569a826c38a62d2abcf264e26b88fa6f482e107f4237" url: "https://pub.dev" source: hosted - version: "0.55.2" + version: "0.70.2" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" - flutter_displaymode: + flutter_custom_tabs: dependency: "direct main" description: - name: flutter_displaymode - sha256: fdeaceb35d84ed000625ae0666bbec5b015407d1f90135cdd1bbfdb4ca73de1a + name: flutter_custom_tabs + sha256: "34167bd15fa3479855c011f868e0789c9569c12b64358ca7250accc5a24c3312" url: "https://pub.dev" source: hosted - version: "0.4.1" + version: "2.1.0" + flutter_custom_tabs_android: + dependency: transitive + description: + name: flutter_custom_tabs_android + sha256: cf06fde8c002f326dc6cbf69ee3f97c3feead4436229da02d2e2aa39d5a5dbf4 + url: "https://pub.dev" + source: hosted + version: "2.1.0" + flutter_custom_tabs_ios: + dependency: transitive + description: + name: flutter_custom_tabs_ios + sha256: ef2de533bc45fb84fefc3854bc8b1e43001671c6bc6bc55faa57942eecd3f70a + url: "https://pub.dev" + source: hosted + version: "2.1.0" + flutter_custom_tabs_platform_interface: + dependency: transitive + description: + name: flutter_custom_tabs_platform_interface + sha256: e18e9b08f92582123bdb84fb6e4c91804b0579700fed6f887d32fd9a1e96a5d5 + url: "https://pub.dev" + source: hosted + version: "2.1.0" + flutter_custom_tabs_web: + dependency: transitive + description: + name: flutter_custom_tabs_web + sha256: "08ae322b11e1972a233d057542279873d0f9d1d5f8159c2c741457239d9d562c" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + flutter_dotenv: + dependency: "direct main" + description: + name: flutter_dotenv + sha256: b7c7be5cd9f6ef7a78429cabd2774d3c4af50e79cb2b7593e3d5d763ef95c61b + url: "https://pub.dev" + source: hosted + version: "5.2.1" + flutter_html: + dependency: "direct main" + description: + name: flutter_html + sha256: "38a2fd702ffdf3243fb7441ab58aa1bc7e6922d95a50db76534de8260638558d" + url: "https://pub.dev" + source: hosted + version: "3.0.0" flutter_launcher_icons: dependency: "direct dev" description: name: flutter_launcher_icons - sha256: a9de6706cd844668beac27c0aed5910fa0534832b3c2cad61a5fd977fce82a5d + sha256: bfa04787c85d80ecb3f8777bde5fc10c3de809240c48fa061a2c2bf15ea5211c url: "https://pub.dev" source: hosted - version: "0.10.0" + version: "0.14.3" flutter_lints: dependency: "direct dev" description: name: flutter_lints - sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "5.0.0" flutter_localizations: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_markdown: + dependency: "direct main" + description: + name: flutter_markdown + sha256: e7bbc718adc9476aa14cfddc1ef048d2e21e4e8f18311aaac723266db9f9e7b5 + url: "https://pub.dev" + source: hosted + version: "0.7.6+2" flutter_native_splash: dependency: "direct dev" description: name: flutter_native_splash - sha256: "6777a3abb974021a39b5fdd2d46a03ca390e03903b6351f21d10e7ecc969f12d" + sha256: edb09c35ee9230c4b03f13dd45bb3a276d0801865f0a4650b7e2a3bba61a803a url: "https://pub.dev" source: hosted - version: "2.2.16" + version: "2.4.5" + flutter_reorderable_list: + dependency: "direct main" + description: + name: flutter_reorderable_list + sha256: "0400ef34fa00b7cac69f71efc92d7e49727f425bc1080180ebe70bf47618afe0" + url: "https://pub.dev" + source: hosted + version: "1.3.1" flutter_svg: dependency: "direct main" description: name: flutter_svg - sha256: "6ff9fa12892ae074092de2fa6a9938fb21dbabfdaa2ff57dc697ff912fc8d4b2" + sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b url: "https://pub.dev" source: hosted - version: "1.1.6" + version: "2.0.17" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" - flutter_web_browser: - dependency: "direct main" - description: - name: flutter_web_browser - sha256: a5564b736253f745e147b8c4eff86de436324d081974cc1f16bff881134a400f - url: "https://pub.dev" - source: hosted - version: "0.17.1" flutter_web_plugins: dependency: transitive description: flutter source: sdk version: "0.0.0" html: - dependency: transitive + dependency: "direct main" description: name: html - sha256: d9793e10dbe0e6c364f4c59bf3e01fb33a9b2a674bc7a1081693dba0614b6269 + sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" url: "https://pub.dev" source: hosted - version: "0.15.1" + version: "0.15.5" http: - dependency: transitive + dependency: "direct main" description: name: http - sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f url: "https://pub.dev" source: hosted - version: "0.13.5" + version: "1.3.0" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" image: dependency: transitive description: name: image - sha256: "8e9d133755c3e84c73288363e6343157c383a0c6c56fc51afcc5d4d7180306d6" + sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "4.5.4" intl: dependency: "direct main" description: name: intl - sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.17.0" - js: - dependency: transitive - description: - name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" - url: "https://pub.dev" - source: hosted - version: "0.6.5" + version: "0.19.0" json_annotation: dependency: transitive description: name: json_annotation - sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.8.0" + version: "4.9.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + url: "https://pub.dev" + source: hosted + version: "10.0.8" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + url: "https://pub.dev" + source: hosted + version: "3.0.9" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" lints: dependency: transitive description: name: lints - sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "5.1.1" + list_counter: + dependency: transitive + description: + name: list_counter + sha256: c447ae3dfcd1c55f0152867090e67e219d42fe6d4f2807db4bbe8b8d69912237 + url: "https://pub.dev" + source: hosted + version: "1.0.2" + markdown: + dependency: "direct main" + description: + name: markdown + sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1" + url: "https://pub.dev" + source: hosted + version: "7.3.0" matcher: dependency: transitive description: name: matcher - sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.13" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.16.0" nested: dependency: transitive description: @@ -401,223 +465,487 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: f62d7253edc197fe3c88d7c2ddab82d68f555e778d55390ccc3537eca8e8d637 + sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191" url: "https://pub.dev" source: hosted - version: "1.4.3+1" - package_info_plus_linux: - dependency: transitive - description: - name: package_info_plus_linux - sha256: "04b575f44233d30edbb80a94e57cad9107aada334fc02aabb42b6becd13c43fc" - url: "https://pub.dev" - source: hosted - version: "1.0.5" - package_info_plus_macos: - dependency: transitive - description: - name: package_info_plus_macos - sha256: a2ad8b4acf4cd479d4a0afa5a74ea3f5b1c7563b77e52cc32b3ee6956d5482a6 - url: "https://pub.dev" - source: hosted - version: "1.3.0" + version: "8.3.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: f7a0c8f1e7e981bc65f8b64137a53fd3c195b18d429fba960babc59a5a1c7ae8 + sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c" url: "https://pub.dev" source: hosted - version: "1.0.2" - package_info_plus_web: - dependency: transitive - description: - name: package_info_plus_web - sha256: f0829327eb534789e0a16ccac8936a80beed4e2401c4d3a74f3f39094a822d3b - url: "https://pub.dev" - source: hosted - version: "1.0.6" - package_info_plus_windows: - dependency: transitive - description: - name: package_info_plus_windows - sha256: "79524f11c42dd9078b96d797b3cf79c0a2883a50c4920dc43da8562c115089bc" - url: "https://pub.dev" - source: hosted - version: "2.1.0" + version: "3.2.0" path: dependency: transitive description: name: path - sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.8.2" - path_drawing: - dependency: transitive - description: - name: path_drawing - sha256: bbb1934c0cbb03091af082a6389ca2080345291ef07a5fa6d6e078ba8682f977 - url: "https://pub.dev" - source: hosted - version: "1.0.1" + version: "1.9.1" path_parsing: dependency: transitive description: name: path_parsing - sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" percent_indicator: dependency: "direct main" description: name: percent_indicator - sha256: cec41f67181fbd5322aa68b355621d1a4eea827426b8eeb613f6cbe195ff7b4a + sha256: "0d77d5c6fa9b7f60202cedf748b568ba9ba38d3f30405d6ceae4da76f5185462" url: "https://pub.dev" source: hosted - version: "4.2.2" + version: "4.2.4" petitparser: dependency: transitive description: name: petitparser - sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4" + sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "6.1.0" + pie_chart: + dependency: "direct main" + description: + name: pie_chart + sha256: "58e6a46999ac938bfa1c3e5be414d6e149f037647197dca03ba3614324c12c82" + url: "https://pub.dev" + source: hosted + version: "5.4.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "2.1.3" - pointycastle: + version: "2.1.8" + posix: dependency: transitive description: - name: pointycastle - sha256: db7306cf0249f838d1a24af52b5a5887c5bf7f31d8bb4e827d071dc0939ad346 + name: posix + sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a url: "https://pub.dev" source: hosted - version: "3.6.2" + version: "6.0.1" provider: dependency: "direct main" description: name: provider - sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c url: "https://pub.dev" source: hosted - version: "6.0.5" + version: "6.1.2" + screen_retriever: + dependency: transitive + description: + name: screen_retriever + sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_linux: + dependency: transitive + description: + name: screen_retriever_linux + sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_macos: + dependency: transitive + description: + name: screen_retriever_macos + sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_platform_interface: + dependency: transitive + description: + name: screen_retriever_platform_interface + sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_windows: + dependency: transitive + description: + name: screen_retriever_windows + sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + segmented_button_slide: + dependency: "direct main" + description: + name: segmented_button_slide + sha256: d0563010c79a101340cfc127ee5a27c8c7d8640d5ef857a36e9d0b46bc01fed4 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + sentry: + dependency: transitive + description: + name: sentry + sha256: "3a64dd001bff768ce5ab6fc3608deef4dde22acd4b5d947763557b20db9e2a32" + url: "https://pub.dev" + source: hosted + version: "8.14.0" + sentry_flutter: + dependency: "direct main" + description: + name: sentry_flutter + sha256: "3d361f2d5f805783e2e4ed1bd475ef126b36cf525b359dc3627a765a3fb7424d" + url: "https://pub.dev" + source: hosted + version: "8.14.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a" + url: "https://pub.dev" + source: hosted + version: "2.5.2" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "3ec7210872c4ba945e3244982918e502fa2bfb5230dff6832459ca0e1879b7ad" + url: "https://pub.dev" + source: hosted + version: "2.4.8" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" sqflite: dependency: "direct main" description: name: sqflite - sha256: "067ab48dbc66bae05e18073a604443baa35957101bd3905b94f65e764c6d0688" + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.4.2" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b" + url: "https://pub.dev" + source: hosted + version: "2.4.1" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: b2ed22d1d62c944ec0dac5cc687ae99cb3331c3ebe146d726ed24704634b5ccd + sha256: "84731e8bfd8303a3389903e01fb2141b6e59b5973cacbb0929021df08dddbe8b" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.5.5" + sqflite_common_ffi: + dependency: "direct main" + description: + name: sqflite_common_ffi + sha256: "1f3ef3888d3bfbb47785cc1dda0dc7dd7ebd8c1955d32a9e8e9dae1e38d1c4c1" + url: "https://pub.dev" + source: hosted + version: "2.3.5" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e" + url: "https://pub.dev" + source: hosted + version: "2.7.5" + sqlite3_flutter_libs: + dependency: "direct main" + description: + name: sqlite3_flutter_libs + sha256: "1a96b59227828d9eb1463191d684b37a27d66ee5ed7597fcf42eee6452c88a14" + url: "https://pub.dev" + source: hosted + version: "0.5.32" stack_trace: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.0" - store_checker: - dependency: "direct main" - description: - name: store_checker - sha256: "0bb9b67fe3c9b0545918ca8621983ab830a1b242eef12ac19b17abac56d452b8" - url: "https://pub.dev" - source: hosted - version: "1.1.0" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" synchronized: dependency: transitive description: name: synchronized - sha256: "33b31b6beb98100bf9add464a36a8dd03eb10c7a8cf15aeec535e9b054aaf04b" + sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.3.1" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.4.16" + version: "0.7.4" + timezone: + dependency: "direct main" + description: + name: timezone + sha256: ffc9d5f4d1193534ef051f9254063fa53d588609418c84299956c3db9383587d + url: "https://pub.dev" + source: hosted + version: "0.10.0" typed_data: dependency: transitive description: name: typed_data - sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.4.0" universal_io: dependency: transitive description: name: universal_io - sha256: "79f78ddad839ee3aae3ec7c01eb4575faf0d5c860f8e5223bc9f9c17f7f03cef" + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.2.2" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" + url: "https://pub.dev" + source: hosted + version: "6.3.1" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "1d0eae19bd7606ef60fe69ef3b312a437a16549476c42321d5dc1506c9ca3bf4" + url: "https://pub.dev" + source: hosted + version: "6.3.15" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" uuid: dependency: "direct main" description: name: uuid - sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "4.5.1" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de" + url: "https://pub.dev" + source: hosted + version: "1.1.18" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" + url: "https://pub.dev" + source: hosted + version: "1.1.16" vector_math: dependency: transitive description: @@ -626,30 +954,70 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + url: "https://pub.dev" + source: hosted + version: "14.3.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" win32: dependency: transitive description: name: win32 - sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 + sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "5.12.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + window_manager: + dependency: "direct main" + description: + name: window_manager + sha256: "732896e1416297c63c9e3fb95aea72d0355f61390263982a47fd519169dc5059" + url: "https://pub.dev" + source: hosted + version: "0.4.3" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" xml: dependency: transitive description: name: xml - sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5" + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.5.0" yaml: dependency: transitive description: name: yaml - sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.3" sdks: - dart: ">=2.18.1 <4.0.0" - flutter: ">=3.3.0" + dart: ">=3.7.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml index 5e2acf3..2fffd97 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,11 +17,15 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.11.0+45 +version: 2.20.4+151 environment: sdk: '>=2.18.1 <3.0.0' +scripts: + build: + linux: "./linux/build-app.sh" + # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions # consider running `flutter pub upgrade --major-versions`. Alternatively, @@ -34,22 +38,37 @@ dependencies: cupertino_icons: ^1.0.2 flutter_localizations: sdk: flutter - intl: ^0.17.0 - provider: ^6.0.3 - sqflite: ^2.1.0 - package_info_plus: ^1.4.3+1 - flutter_displaymode: ^0.4.0 - dynamic_color: ^1.5.4 - animations: ^2.0.5 - device_info_plus: ^4.1.2 - uuid: ^3.0.6 + intl: any + provider: ^6.1.1 + sqflite: ^2.3.0 + package_info_plus: ^8.0.0 + dynamic_color: ^1.7.0 + animations: ^2.0.10 + device_info_plus: ^11.2.1 + uuid: ^4.2.1 expandable: ^5.0.1 - fl_chart: ^0.55.2 - flutter_web_browser: ^0.17.1 - flutter_svg: ^1.1.5 - bottom_sheet: ^3.1.2 - percent_indicator: ^4.2.2 - store_checker: ^1.1.0 + fl_chart: ^0.70.2 + flutter_svg: ^2.0.9 + percent_indicator: ^4.2.3 + flutter_markdown: ^0.7.1 + markdown: ^7.1.1 + html: ^0.15.4 + flutter_html: ^3.0.0-beta.2 + sqlite3_flutter_libs: ^0.5.18 + sqflite_common_ffi: ^2.3.1 + contextmenu: ^3.0.0 + async: ^2.11.0 + sentry_flutter: ^8.0.0 + flutter_dotenv: ^5.1.0 + flutter_reorderable_list: ^1.3.1 + pie_chart: ^5.4.0 + segmented_button_slide: ^2.0.0 + http: ^1.1.2 + timezone: ^0.10.0 + flutter_custom_tabs: ^2.0.0+1 + url_launcher: ^6.2.4 + shared_preferences: ^2.2.2 + window_manager: ^0.4.2 dev_dependencies: flutter_test: @@ -60,9 +79,9 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^2.0.0 - flutter_launcher_icons: ^0.10.0 - flutter_native_splash: ^2.2.10+1 + flutter_lints: ^5.0.0 + flutter_launcher_icons: ^0.14.1 + flutter_native_splash: ^2.3.8 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec @@ -75,6 +94,12 @@ flutter_icons: image_path_android: "assets/icon/icon-android.png" adaptive_icon_foreground: "assets/icon/icon-android-adap.png" adaptive_icon_background: "#ffffff" + macos: + generate: true + image_path: "assets/icon/icon-macos.png" + windows: + generate: true + image_path: "assets/icon/icon-circle.png" flutter_native_splash: android12: true @@ -98,10 +123,14 @@ flutter: - assets/icon/icon-android-adap.png - assets/icon/icon-android.png - assets/icon/icon-splash.png + - assets/icon/icon-macos.png + - assets/icon/icon-circle.png + - assets/icon/icon-circle-256.png - assets/icon/icon1024-white-center.png - assets/other/get_google_play.png - assets/resources/github.svg - assets/resources/google-play.svg + - .env # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware diff --git a/windows/.gitignore b/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt new file mode 100644 index 0000000..f992491 --- /dev/null +++ b/windows/CMakeLists.txt @@ -0,0 +1,101 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(adguard_home_manager LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "adguard_home_manager") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..b0ea5ac --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,29 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + DynamicColorPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); + ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); + SentryFlutterPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SentryFlutterPlugin")); + Sqlite3FlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); + WindowManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowManagerPlugin")); +} diff --git a/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..5ab1216 --- /dev/null +++ b/windows/flutter/generated_plugins.cmake @@ -0,0 +1,29 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + dynamic_color + screen_retriever_windows + sentry_flutter + sqlite3_flutter_libs + url_launcher_windows + window_manager +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/windows/innosetup_installer_builder.iss b/windows/innosetup_installer_builder.iss new file mode 100644 index 0000000..ed5df88 --- /dev/null +++ b/windows/innosetup_installer_builder.iss @@ -0,0 +1,57 @@ +; Script generated by the Inno Setup Script Wizard. +; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! + +#define MyAppName "AdGuard Home Manager" +#define MyAppVersion "" +#define MyAppPublisher "JGeek00" +#define MyAppURL "https://github.com/JGeek00/adguard-home-manager" +#define MyAppExeName "adguard_home_manager.exe" + +[Setup] +; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. +; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) +AppId={{41521D70-0D21-4A9B-8CD4-2760753054C9} +AppName={#MyAppName} +AppVersion={#MyAppVersion} +;AppVerName={#MyAppName} {#MyAppVersion} +AppPublisher={#MyAppPublisher} +AppPublisherURL={#MyAppURL} +AppSupportURL={#MyAppURL} +AppUpdatesURL={#MyAppURL} +DefaultDirName={autopf}\{#MyAppName} +DisableProgramGroupPage=yes +; Remove the following line to run in administrative install mode (install for all users.) +PrivilegesRequired=lowest +OutputDir=..\build\windows +OutputBaseFilename=aghm_installer +SetupIconFile=..\assets\icon\icon-circle.ico +Compression=lzma +SolidCompression=yes +WizardStyle=modern + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" +Name: "spanish"; MessagesFile: "compiler:Languages\Spanish.isl" + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked + +[Files] +Source: "..\build\windows\x64\runner\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion +Source: "..\build\windows\x64\runner\Release\dynamic_color_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "..\build\windows\x64\runner\Release\flutter_windows.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "..\build\windows\x64\runner\Release\screen_retriever_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "..\build\windows\x64\runner\Release\sqlite3.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "..\build\windows\x64\runner\Release\sqlite3_flutter_libs_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "..\build\windows\x64\runner\Release\url_launcher_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "..\build\windows\x64\runner\Release\window_manager_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "..\build\windows\x64\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs +; NOTE: Don't use "Flags: ignoreversion" on any shared system files + +[Icons] +Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" +Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon + +[Run] +Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent + diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc new file mode 100644 index 0000000..a56b12b --- /dev/null +++ b/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "JGeek00" "\0" + VALUE "FileDescription", "AdGuard Home Manager" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "AdGuard Home Manager" "\0" + VALUE "LegalCopyright", "Copyright (C) 2023 JGeek00. All rights reserved." "\0" + VALUE "OriginalFilename", "adguard_home_manager.exe" "\0" + VALUE "ProductName", "AdGuard Home Manager" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/windows/runner/flutter_window.h b/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp new file mode 100644 index 0000000..618eef7 --- /dev/null +++ b/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(500,700); + if (!window.Create(L"AdGuard Home Manager", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/windows/runner/resource.h b/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..6d25264 Binary files /dev/null and b/windows/runner/resources/app_icon.ico differ diff --git a/windows/runner/runner.exe.manifest b/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..a42ea76 --- /dev/null +++ b/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp new file mode 100644 index 0000000..f5bf9fa --- /dev/null +++ b/windows/runner/utils.cpp @@ -0,0 +1,64 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/windows/runner/utils.h b/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp new file mode 100644 index 0000000..041a385 --- /dev/null +++ b/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h new file mode 100644 index 0000000..c86632d --- /dev/null +++ b/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_