diff --git a/.github/workflows/release-beta.yaml b/.github/workflows/release-beta.yaml index 472e119..8f72ac4 100644 --- a/.github/workflows/release-beta.yaml +++ b/.github/workflows/release-beta.yaml @@ -2,19 +2,9 @@ name: Compile and release beta build on: workflow_dispatch: - inputs: - version: - description: "Version" - required: true - default: "1.0.0" - beta-number: - description: "Beta number" - required: true - default: "1" - number: - description: "Build number" - required: true - default: "1" + branches: + - beta + jobs: build-android: name: Build Android .apk and .aab @@ -22,7 +12,9 @@ jobs: env: ANDROID_AAB_RELEASE_PATH: build/app/outputs/bundle/release ANDROID_APK_RELEASE_PATH: build/app/outputs/apk/release - VERSION_NAME: ${{ github.event.inputs.version }}-beta.${{ github.event.inputs.beta-number }} + outputs: + VERSION_NAME: ${{ steps.save_version.outputs.version_name }} + VERSION_NUMBER: ${{ steps.save_version.outputs.version_number }} steps: - uses: actions/checkout@v3 with: @@ -33,8 +25,21 @@ jobs: run: echo "${{ secrets.KEY_PROPERTIES }}" | base64 --decode > android/key.properties - name: Decode .env run: echo "${{ secrets.ENV }}" | base64 --decode > .env - - name: Update version in YAML - run: sed -i 's/99.99.99+99/${{ env.VERSION_NAME }}+${{ github.event.inputs.number }}/g' pubspec.yaml + - name: Read pubspec.yaml + uses: adore-me/read-yaml@v1.0.0 + id: read_pubspec + with: + file: './pubspec.yaml' + key-path: '["version"]' + - name: Save version on env variable + id: save_version + run: | + version=${{ steps.read_pubspec.outputs.data }} + IFS='+' + read -r -a split <<< "$version" + echo "VERSION_NAME=$(echo ${split[0]})" >> $GITHUB_ENV + echo "version_name=${split[0]}" >> $GITHUB_OUTPUT + echo "version_number=${split[1]}" >> $GITHUB_OUTPUT - name: Update KeyStore password in gradle properties run: sed -i 's/#{KEYSTORE_PASS}#/${{ secrets.KEYSTORE_PASS }}/g' android/key.properties - name: Update KeyStore key password in gradle properties @@ -69,8 +74,9 @@ jobs: name: Release beta build to GitHub runs-on: ubuntu-latest needs: [build-android] - env: - VERSION_NAME: ${{ github.event.inputs.version }}-beta.${{ github.event.inputs.beta-number }} + env: + VERSION_NAME: ${{ needs.build-android.outputs.VERSION_NAME }} + VERSION_NUMBER: ${{ needs.build-android.outputs.VERSION_NUMBER }} steps: - uses: actions/checkout@v3 with: @@ -87,7 +93,7 @@ jobs: with: artifacts: "releases/*" token: ${{ secrets.GH_TOKEN }} - tag: '${{ env.VERSION_NAME }}_(${{ github.event.inputs.number }})' + tag: '${{ env.VERSION_NAME }}_(${{ env.VERSION_NUMBER }})' name: v${{ env.VERSION_NAME }} draft: true prerelease: true @@ -97,7 +103,7 @@ jobs: runs-on: ubuntu-latest needs: [build-android] env: - VERSION_NAME: ${{ github.event.inputs.version }}-beta.${{ github.event.inputs.beta-number }} + VERSION_NAME: ${{ needs.build-android.outputs.VERSION_NAME }} steps: - uses: actions/checkout@v3 with: diff --git a/.github/workflows/release-stable.yaml b/.github/workflows/release-stable.yaml index cfebecd..220c56d 100644 --- a/.github/workflows/release-stable.yaml +++ b/.github/workflows/release-stable.yaml @@ -2,15 +2,9 @@ name: Compile and release production build on: workflow_dispatch: - inputs: - version: - description: "Version" - required: true - default: "1.0.0" - number: - description: "Build number" - required: true - default: "1" + branches: + - master + jobs: build-android: name: Build Android .apk and .aab @@ -18,6 +12,9 @@ jobs: env: ANDROID_AAB_RELEASE_PATH: build/app/outputs/bundle/release ANDROID_APK_RELEASE_PATH: build/app/outputs/apk/release + outputs: + VERSION_NAME: ${{ steps.save_version.outputs.version_name }} + VERSION_NUMBER: ${{ steps.save_version.outputs.version_number }} steps: - uses: actions/checkout@v3 - name: Decode android/app/keystore.jks @@ -26,8 +23,21 @@ jobs: run: echo "${{ secrets.KEY_PROPERTIES }}" | base64 --decode > android/key.properties - name: Decode .env run: echo "${{ secrets.ENV }}" | base64 --decode > .env - - name: Update version in YAML - run: sed -i 's/99.99.99+99/${{ github.event.inputs.version }}+${{ github.event.inputs.number }}/g' pubspec.yaml + - name: Read pubspec.yaml + uses: adore-me/read-yaml@v1.0.0 + id: read_pubspec + with: + file: './pubspec.yaml' + key-path: '["version"]' + - name: Save version on env variable + id: save_version + run: | + version=${{ steps.read_pubspec.outputs.data }} + IFS='+' + read -r -a split <<< "$version" + echo "VERSION_NAME=$(echo ${split[0]})" >> $GITHUB_ENV + echo "version_name=${split[0]}" >> $GITHUB_OUTPUT + echo "version_number=${split[1]}" >> $GITHUB_OUTPUT - name: Update KeyStore password in gradle properties run: sed -i 's/#{KEYSTORE_PASS}#/${{ secrets.KEYSTORE_PASS }}/g' android/key.properties - name: Update KeyStore key password in gradle properties @@ -44,37 +54,67 @@ jobs: - run: flutter build apk --release - run: flutter build appbundle --release - name: Rename apk - run: mv $ANDROID_APK_RELEASE_PATH/app-release.apk $ANDROID_APK_RELEASE_PATH/AdGuardHomeManager_${{ github.event.inputs.version }}_Android.apk + run: mv $ANDROID_APK_RELEASE_PATH/app-release.apk $ANDROID_APK_RELEASE_PATH/AdGuardHomeManager_${{ env.VERSION_NAME }}_Android.apk - name: Rename aab - run: mv $ANDROID_AAB_RELEASE_PATH/app-release.aab $ANDROID_AAB_RELEASE_PATH/AdGuardHomeManager_${{ github.event.inputs.version }}_Android.aab + run: mv $ANDROID_AAB_RELEASE_PATH/app-release.aab $ANDROID_AAB_RELEASE_PATH/AdGuardHomeManager_${{ env.VERSION_NAME }}_Android.aab - name: Copy apk to project root - run: cp $ANDROID_APK_RELEASE_PATH/AdGuardHomeManager_${{ github.event.inputs.version }}_Android.apk AdGuardHomeManager_${{ github.event.inputs.version }}_Android.apk + run: cp $ANDROID_APK_RELEASE_PATH/AdGuardHomeManager_${{ env.VERSION_NAME }}_Android.apk AdGuardHomeManager_${{ env.VERSION_NAME }}_Android.apk - name: Copy aab to project root - run: cp $ANDROID_AAB_RELEASE_PATH/AdGuardHomeManager_${{ github.event.inputs.version }}_Android.aab AdGuardHomeManager_${{ github.event.inputs.version }}_Android.aab + run: cp $ANDROID_AAB_RELEASE_PATH/AdGuardHomeManager_${{ env.VERSION_NAME }}_Android.aab AdGuardHomeManager_${{ env.VERSION_NAME }}_Android.aab - name: Upload artifact uses: actions/upload-artifact@v3 with: name: android path: | - AdGuardHomeManager_${{ github.event.inputs.version }}_Android.aab - AdGuardHomeManager_${{ github.event.inputs.version }}_Android.apk + AdGuardHomeManager_${{ env.VERSION_NAME }}_Android.aab + AdGuardHomeManager_${{ env.VERSION_NAME }}_Android.apk build-macos: name: Build macOS .dmg runs-on: macos-latest env: MACOS_APP_RELEASE_PATH: build/macos/Build/Products/Release + outputs: + VERSION_NAME: ${{ steps.save_version.outputs.version_name }} + VERSION_NUMBER: ${{ steps.save_version.outputs.version_number }} steps: - uses: actions/checkout@v3 - name: Decode .env run: echo "${{ secrets.ENV }}" | base64 --decode > .env - - name: Update version in YAML - run: sed -i '' 's/99.99.99+99/${{ github.event.inputs.version }}+${{ github.event.inputs.number }}/g' pubspec.yaml + - name: Read pubspec.yaml + uses: adore-me/read-yaml@v1.0.0 + id: read_pubspec + with: + file: './pubspec.yaml' + key-path: '["version"]' + - name: Save version on env variable + id: save_version + run: | + version=${{ steps.read_pubspec.outputs.data }} + IFS='+' + read -r -a split <<< "$version" + echo "VERSION_NAME=$(echo ${split[0]})" >> $GITHUB_ENV + echo "version_name=${split[0]}" >> $GITHUB_OUTPUT + echo "version_number=${split[1]}" >> $GITHUB_OUTPUT - uses: subosito/flutter-action@v2 with: channel: "stable" - run: flutter clean - run: flutter pub get - run: flutter build macos --release + - name: Install the Apple certificate and sign the application + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PWD: ${{ secrets.APPLE_CERTIFICATE_PWD }} + APPLE_KEYCHAIN_PWD: ${{ secrets.APPLE_KEYCHAIN_PWD }} + APPLE_IDENTITY_ID: ${{ secrets.APPLE_IDENTITY_ID }} + run: | + echo "$APPLE_CERTIFICATE" | base64 --decode > certificate.p12 + security create-keychain -p $APPLE_KEYCHAIN_PWD build.keychain + security default-keychain -s build.keychain + security unlock-keychain -p $APPLE_KEYCHAIN_PWD build.keychain + security import certificate.p12 -k build.keychain -P $APPLE_CERTIFICATE_PWD -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $APPLE_KEYCHAIN_PWD build.keychain + /usr/bin/codesign --force -s "$APPLE_IDENTITY_ID" $MACOS_APP_RELEASE_PATH/AdGuard\ Home\ Manager.app -v - name: Create folder to build dmg run: mkdir $MACOS_APP_RELEASE_PATH/AdGuard\ Home\ Manager - name: Copy app into folder @@ -82,25 +122,41 @@ jobs: - name: Generate symbolic link to Applications dir run: ln -s /Applications $MACOS_APP_RELEASE_PATH/AdGuard\ Home\ Manager - name: Generate dmg - run: hdiutil create -srcfolder $MACOS_APP_RELEASE_PATH/AdGuard\ Home\ Manager $MACOS_APP_RELEASE_PATH/AdGuardHomeManager_${{ github.event.inputs.version }}_macOS_Universal.dmg + run: hdiutil create -srcfolder $MACOS_APP_RELEASE_PATH/AdGuard\ Home\ Manager $MACOS_APP_RELEASE_PATH/AdGuardHomeManager_${{ env.VERSION_NAME }}_macOS_Universal.dmg - name: Copy dmg to project root - run: cp $MACOS_APP_RELEASE_PATH/AdGuardHomeManager_${{ github.event.inputs.version }}_macOS_Universal.dmg AdGuardHomeManager_${{ github.event.inputs.version }}_macOS_Universal.dmg + run: cp $MACOS_APP_RELEASE_PATH/AdGuardHomeManager_${{ env.VERSION_NAME }}_macOS_Universal.dmg AdGuardHomeManager_${{ env.VERSION_NAME }}_macOS_Universal.dmg - name: Upload artifact uses: actions/upload-artifact@v3 with: name: macos - path: AdGuardHomeManager_${{ github.event.inputs.version }}_macOS_Universal.dmg + path: AdGuardHomeManager_${{ env.VERSION_NAME }}_macOS_Universal.dmg build-linux: name: Build Linux .tar.gz and .deb runs-on: ubuntu-latest + outputs: + VERSION_NAME: ${{ steps.save_version.outputs.version_name }} + VERSION_NUMBER: ${{ steps.save_version.outputs.version_number }} steps: - uses: actions/checkout@v3 - name: Decode .env run: echo "${{ secrets.ENV }}" | base64 --decode > .env - - name: Update version in pubspec.yaml - run: sed -i 's/99.99.99+99/${{ github.event.inputs.version }}+${{ github.event.inputs.number }}/g' pubspec.yaml + - name: Read pubspec.yaml + uses: adore-me/read-yaml@v1.0.0 + id: read_pubspec + with: + file: './pubspec.yaml' + key-path: '["version"]' + - name: Save version on env variable + id: save_version + run: | + version=${{ steps.read_pubspec.outputs.data }} + IFS='+' + read -r -a split <<< "$version" + echo "VERSION_NAME=$(echo ${split[0]})" >> $GITHUB_ENV + echo "version_name=${split[0]}" >> $GITHUB_OUTPUT + echo "version_number=${split[1]}" >> $GITHUB_OUTPUT - name: Update version in debian.yaml - run: sed -i 's//${{ github.event.inputs.version }}/g' debian/debian.yaml + run: sed -i 's//${{ env.VERSION_NAME }}/g' debian/debian.yaml - name: Update dependencies list run: sudo apt-get update - name: Install dependencies @@ -116,7 +172,7 @@ jobs: - name: Generate .deb package run: flutter_to_debian - name: Move .deb package to project root - run: mv debian/packages/AdGuardHomeManager_${{ github.event.inputs.version }}_amd64.deb AdGuardHomeManager_${{ github.event.inputs.version }}_Linux_amd64.deb + run: mv build/linux/x64/release/debian/AdGuardHomeManager_${{ env.VERSION_NAME }}_amd64.deb AdGuardHomeManager_${{ env.VERSION_NAME }}_Linux_amd64.deb - name: Generate .tar.gz package uses: a7ul/tar-action@v1.1.3 id: compress @@ -127,30 +183,46 @@ jobs: ./data ./lib ./AdGuardHomeManager - outPath: AdGuardHomeManager_${{ github.event.inputs.version }}_Linux.tar.gz + outPath: AdGuardHomeManager_${{ env.VERSION_NAME }}_Linux.tar.gz - name: Upload artifact uses: actions/upload-artifact@v3 with: name: linux path: | - AdGuardHomeManager_${{ github.event.inputs.version }}_Linux_amd64.deb - AdGuardHomeManager_${{ github.event.inputs.version }}_Linux.tar.gz + AdGuardHomeManager_${{ env.VERSION_NAME }}_Linux_amd64.deb + AdGuardHomeManager_${{ env.VERSION_NAME }}_Linux.tar.gz build-windows: name: Build Windows installer runs-on: windows-latest + outputs: + VERSION_NAME: ${{ steps.save_version.outputs.version_name }} + VERSION_NUMBER: ${{ steps.save_version.outputs.version_number }} steps: - uses: actions/checkout@v3 - name: Decode .env shell: pwsh run: | [IO.File]::WriteAllBytes('.env', [Convert]::FromBase64String('${{ secrets.ENV }}')) - - name: Update version in pubspec.yaml + - name: Read pubspec.yaml + uses: adore-me/read-yaml@v1.0.0 + id: read_pubspec + with: + file: './pubspec.yaml' + key-path: '["version"]' + - name: Save version on env variable shell: bash - run: sed -i 's/99.99.99+99/${{ github.event.inputs.version }}+${{ github.event.inputs.number }}/g' pubspec.yaml + id: save_version + run: | + version=${{ steps.read_pubspec.outputs.data }} + IFS='+' + read -r -a split <<< "$version" + echo "VERSION_NAME=$(echo ${split[0]})" >> $GITHUB_ENV + echo "version_name=${split[0]}" >> $GITHUB_OUTPUT + echo "version_number=${split[1]}" >> $GITHUB_OUTPUT - name: Update version in innosetup config file shell: pwsh run: | - (Get-Content windows/innosetup_installer_builder.iss) -replace '', '${{ github.event.inputs.version }}' | Out-File -encoding ASCII windows/innosetup_installer_builder.iss + (Get-Content windows/innosetup_installer_builder.iss) -replace '', '${{ env.VERSION_NAME }}' | Out-File -encoding ASCII windows/innosetup_installer_builder.iss - uses: subosito/flutter-action@v2 with: channel: "stable" @@ -160,16 +232,19 @@ jobs: - name: Build installer witn innosetup run: iscc /Q windows/innosetup_installer_builder.iss - name: Move installer file to root directory - run: move build/windows/aghm_installer.exe AdGuardHomeManager_${{ github.event.inputs.version }}_Windows_x64.exe + run: move build/windows/aghm_installer.exe AdGuardHomeManager_${{ env.VERSION_NAME }}_Windows_x64.exe - name: Upload artifact uses: actions/upload-artifact@v3 with: name: windows - path: AdGuardHomeManager_${{ github.event.inputs.version }}_Windows_x64.exe + path: AdGuardHomeManager_${{ env.VERSION_NAME }}_Windows_x64.exe release-builds-github: name: Release builds to GitHub runs-on: ubuntu-latest needs: [build-android, build-macos, build-linux, build-windows] + env: + VERSION_NAME: ${{ needs.build-android.outputs.VERSION_NAME }} + VERSION_NUMBER: ${{ needs.build-android.outputs.VERSION_NUMBER }} steps: - uses: actions/checkout@v3 - name: Create builds directory @@ -199,8 +274,8 @@ jobs: with: artifacts: "releases/*" token: ${{ secrets.GH_TOKEN }} - tag: '${{ github.event.inputs.version }}_(${{ github.event.inputs.number }})' - name: v${{ github.event.inputs.version }} + tag: '${{ env.VERSION_NAME }}_(${{ env.VERSION_NUMBER }})' + name: v${{ env.VERSION_NAME }} draft: true prerelease: false commit: ${{ github.sha }} @@ -208,6 +283,8 @@ jobs: name: Release Android build to the Google Play Store runs-on: ubuntu-latest needs: [build-android, build-macos, build-linux, build-windows] + env: + VERSION_NAME: ${{ needs.build-android.outputs.VERSION_NAME }} steps: - uses: actions/checkout@v3 - name: Download Android artifacts @@ -219,7 +296,7 @@ jobs: with: serviceAccountJsonPlainText: ${{ secrets.PLAYSTORE_ACCOUNT_KEY }} packageName: com.jgeek00.adguard_home_manager - releaseFiles: AdGuardHomeManager_${{ github.event.inputs.version }}_Android.aab + releaseFiles: AdGuardHomeManager_${{ env.VERSION_NAME }}_Android.aab track: production status: draft - releaseName: ${{ github.event.inputs.version }} \ No newline at end of file + releaseName: ${{ env.VERSION_NAME }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 429a1bd..6f4a17f 100644 --- a/.gitignore +++ b/.gitignore @@ -46,4 +46,6 @@ app.*.map.json /android/app/profile /android/app/release -/debian/packages \ No newline at end of file +/debian/packages + +untranslated.json \ No newline at end of file diff --git a/README.md b/README.md index d8e05f3..930d090 100644 --- a/README.md +++ b/README.md @@ -44,18 +44,28 @@ On [this repository](https://github.com/JuanRodenas/Pihole_list) you can find a ## Generate production build
    +
  • + Prerequisites +
      +
    1. Open pubspec.yaml and change the version name and the version number.
    2. +
    3. Run flutter clean.
    4. +
    5. Run flutter pub get.
    6. +
    +
  • +
  • + Android +
      +
    1. Make sure you have your key.properties file at android/, with all the required values of your signing key correctly set up.
    2. +
    3. Make sure you have your keystore file at android/app.
    4. +
    5. Run flutter build apk --release to compile the APK.
    6. +
    7. The .apk package is located at build/app/outputs/flutter-apk/app-release.apk.
    8. +
    +
  • macOS
      -
    1. flutter clean
    2. -
    3. flutter pub get
    4. -
    5. flutter build macos --release
    6. -
    7. Open macos/Runner.xcworkspace on Xcode
    8. -
    9. Make sure all the pods have the minimum deployment version at 10.14
    10. -
    11. Select Runner > Targets Runner
    12. -
    13. Make sure the Version and Build numbers are correct
    14. -
    15. Click on Product menu and on Archive
    16. -
    17. Select the first on the list and click on Distribute app, select Copy App and click on Next
    18. +
    19. Run flutter build macos --release to compile the production build.
    20. +
    21. The .app package is located at build/macos/Build/Products/Release/AdGuard Home Manager.app.
  • @@ -67,24 +77,22 @@ On [this repository](https://github.com/JuanRodenas/Pihole_list) you can find a Build
      -
    1. Open debian.yaml file inside debian/ and update the version number
    2. +
    3. Open debian.yaml file inside debian/ and update the version number
    4. run rps build linux
    5. -
    6. The .tar.gz is at build/linux/x64/release/bundle
    7. -
    8. The .deb package is at debian/packages
    9. +
    10. The .tar.gz is at build/linux/x64/release/bundle
    11. +
    12. The .deb package is at build/linux/x64/release/debian/
  • Windows
      -
    1. flutter clean
    2. -
    3. flutter pub get
    4. -
    5. flutter build windows
    6. +
    7. Run flutter build windows --release.
    8. Open Inno Setup Compiler application and load the script
    9. -
    10. The script is located at windows/innosetup_installer_builder.iss
    11. +
    12. The script is located at windows/innosetup_installer_builder.iss
    13. Update the version number and save the changes
    14. Click on the Compile button
    15. -
    16. The installer will be generated at build/windows/aghm_installer.exe
    17. +
    18. The installer will be generated at build/windows/aghm_installer.exe.
  • @@ -119,6 +127,9 @@ On [this repository](https://github.com/JuanRodenas/Pihole_list) you can find a - [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)
    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"> = 2.7.5) + - sqlite3 (3.44.0): + - sqlite3/common (= 3.44.0) + - sqlite3/common (3.44.0) + - sqlite3/fts5 (3.44.0): + - sqlite3/common + - sqlite3/perf-threadsafe (3.44.0): + - sqlite3/common + - sqlite3/rtree (3.44.0): + - sqlite3/common + - sqlite3_flutter_libs (0.0.1): + - Flutter + - sqlite3 (~> 3.44.0) + - sqlite3/fts5 + - sqlite3/perf-threadsafe + - sqlite3/rtree - store_checker (0.0.1): - Flutter + - url_launcher_ios (0.0.1): + - Flutter DEPENDENCIES: - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) @@ -23,12 +47,18 @@ DEPENDENCIES: - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_web_browser (from `.symlinks/plugins/flutter_web_browser/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) + - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`) + - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`) - store_checker (from `.symlinks/plugins/store_checker/ios`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) SPEC REPOS: trunk: - FMDB + - Sentry + - SentryPrivate + - sqlite3 EXTERNAL SOURCES: device_info_plus: @@ -41,21 +71,33 @@ EXTERNAL SOURCES: :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" + sqlite3_flutter_libs: + :path: ".symlinks/plugins/sqlite3_flutter_libs/ios" store_checker: :path: ".symlinks/plugins/store_checker/ios" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: - device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed + device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef flutter_web_browser: 7bccaafbb0c5b8862afe7bcd158f15557109f61f FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a - package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e - sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 + package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 + Sentry: 6f5742b4c47c17c9adcf265f6f328cf4a0ed1923 + sentry_flutter: 2c309a1d4b45e59d02cfa15795705687f1e2081b + SentryPrivate: b2f7996f37781080f04a946eb4e377ff63c64195 + sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a + sqlite3: 6e2d4a4879854d0ec86b476bf3c3e30870bac273 + sqlite3_flutter_libs: eb769059df0356dc52ddda040f09cacc9391a7cf store_checker: 359c5051d9ec30ff0a8fa39eb5ec9df021bb745d + url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 -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 7d212ec..00edeae --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -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 = { @@ -222,6 +222,7 @@ files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -324,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; @@ -334,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; @@ -342,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; @@ -360,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.jgeek00.adguard_home_manager; + 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; @@ -396,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; @@ -406,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; @@ -420,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; @@ -451,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; @@ -461,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; @@ -469,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; @@ -489,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.jgeek00.adguard_home_manager; + 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; @@ -512,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.jgeek00.adguard_home_manager; + 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 b2b593b..0000000 --- a/lib/base.dart +++ /dev/null @@ -1,116 +0,0 @@ -// ignore_for_file: use_build_context_synchronously, depend_on_referenced_packages - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:animations/animations.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:provider/provider.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:flutter/services.dart'; - -import 'package:adguard_home_manager/widgets/bottom_nav_bar.dart'; -import 'package:adguard_home_manager/widgets/menu_bar.dart'; -import 'package:adguard_home_manager/widgets/update_modal.dart'; -import 'package:adguard_home_manager/widgets/navigation_rail.dart'; - -import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/functions/check_app_updates.dart'; -import 'package:adguard_home_manager/functions/open_url.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 { - const Base({Key? key}) : super(key: key); - - @override - State createState() => _BaseState(); -} - -class _BaseState extends State with WidgetsBindingObserver { - int selectedScreen = 0; - - @override - void initState() { - WidgetsBinding.instance.addObserver(this); - - super.initState(); - - WidgetsBinding.instance.addPostFrameCallback((_) async { - 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) { - await showDialog( - context: context, - builder: (context) => UpdateModal( - gitHubRelease: result, - onDownload: (link, version) => openUrl(link), - ), - ); - } - }); - } - - @override - Widget build(BuildContext context) { - final serversProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - - final width = MediaQuery.of(context).size.width; - - List screens = serversProvider.selectedServer != null - ? screensServerConnected - : screensSelectServer; - - if (kDebugMode && dotenv.env['ENABLE_SENTRY'] == "true") { - Sentry.captureMessage("Debug mode"); - } - - return CustomMenuBar( - child: 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: Row( - children: [ - if (width > 900) const SideNavigationRail(), - Expanded( - child: PageTransitionSwitcher( - duration: const Duration(milliseconds: 200), - transitionBuilder: ( - (child, primaryAnimation, secondaryAnimation) => FadeThroughTransition( - animation: primaryAnimation, - secondaryAnimation: secondaryAnimation, - child: child, - ) - ), - child: screens[appConfigProvider.selectedScreen].body, - ), - ), - ], - ), - bottomNavigationBar: width <= 900 - ? const BottomNavBar() - : null, - ) - ), - ); - } -} \ 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 index d21860b..a955a77 100644 --- a/lib/config/globals.dart +++ b/lib/config/globals.dart @@ -1,3 +1,4 @@ import 'package:flutter/material.dart'; -final GlobalKey scaffoldMessengerKey = GlobalKey(); \ No newline at end of file +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 index 5220987..a5a23f0 100644 --- a/lib/config/home_top_items_default_order.dart +++ b/lib/config/home_top_items_default_order.dart @@ -5,7 +5,9 @@ import 'package:adguard_home_manager/constants/enums.dart'; final List homeTopItemsDefaultOrder = [ HomeTopItems.queriedDomains, HomeTopItems.blockedDomains, - HomeTopItems.recurrentClients + HomeTopItems.recurrentClients, + HomeTopItems.topUpstreams, + HomeTopItems.avgUpstreamResponseTime ]; final String homeTopItemsDefaultOrderString = jsonEncode( 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..8a8aadf 100644 --- a/lib/config/theme.dart +++ b/lib/config/theme.dart @@ -15,7 +15,6 @@ 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, ); ThemeData darkTheme(ColorScheme? dynamicColorScheme) => ThemeData( @@ -34,7 +33,6 @@ 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, ); ThemeData lightThemeOldVersions(MaterialColor primaryColor) => ThemeData( @@ -53,7 +51,6 @@ ThemeData lightThemeOldVersions(MaterialColor primaryColor) => ThemeData( iconColor: Color.fromRGBO(117, 117, 117, 1), ), brightness: Brightness.light, - androidOverscrollIndicator: AndroidOverscrollIndicator.stretch ); ThemeData darkThemeOldVersions(MaterialColor primaryColor) => ThemeData( @@ -75,5 +72,4 @@ ThemeData darkThemeOldVersions(MaterialColor primaryColor) => ThemeData( iconColor: Color.fromRGBO(187, 187, 187, 1), ), brightness: Brightness.dark, - androidOverscrollIndicator: AndroidOverscrollIndicator.stretch ); \ No newline at end of file diff --git a/lib/constants/enums.dart b/lib/constants/enums.dart index fed648e..f54e665 100644 --- a/lib/constants/enums.dart +++ b/lib/constants/enums.dart @@ -1,2 +1,2 @@ enum LoadStatus { loading, loaded, error } -enum HomeTopItems { queriedDomains, blockedDomains, recurrentClients } \ No newline at end of file +enum HomeTopItems { queriedDomains, blockedDomains, recurrentClients, topUpstreams, avgUpstreamResponseTime } \ 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 440c8d7..b7eb069 100644 --- a/lib/constants/urls.dart +++ b/lib/constants/urls.dart @@ -3,6 +3,7 @@ class Urls { 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 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"; diff --git a/lib/functions/check_app_updates.dart b/lib/functions/check_app_updates.dart index eb44177..e002556 100644 --- a/lib/functions/check_app_updates.dart +++ b/lib/functions/check_app_updates.dart @@ -3,8 +3,8 @@ import 'dart:io'; import 'package:store_checker/store_checker.dart'; import 'package:adguard_home_manager/functions/compare_versions.dart'; +import 'package:adguard_home_manager/services/external_requests.dart'; import 'package:adguard_home_manager/models/github_release.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; Future checkAppUpdates({ required String currentBuildNumber, @@ -12,21 +12,27 @@ Future checkAppUpdates({ required Source installationSource, required bool isBeta }) async { - final result = await checkAppUpdatesGitHub(); + var result = isBeta + ? await ExternalRequests.getReleasesGitHub() + : await ExternalRequests.getReleaseData(); + + if (result.successful == true) { + late GitHubRelease gitHubRelease; + if (isBeta) { + gitHubRelease = (result.content as List).firstWhere((r) => r.prerelease == true); + } + else { + gitHubRelease = result.content as GitHubRelease; + } - if (result['result'] == 'success') { final update = gitHubUpdateExists( currentBuildNumber: currentBuildNumber, - gitHubReleases: result['body'], + gitHubRelease: gitHubRelease, isBeta: isBeta ); - +print(update); if (update == true) { - final release = isBeta == true - ? result['body'].firstWhere((release) => release.prerelease == true) - : result['body'].firstWhere((release) => release.prerelease == false); - - setUpdateAvailable(release); + setUpdateAvailable(gitHubRelease); if (Platform.isAndroid) { if ( @@ -34,7 +40,7 @@ Future checkAppUpdates({ installationSource == Source.IS_INSTALLED_FROM_PLAY_PACKAGE_INSTALLER || installationSource == Source.UNKNOWN ) { - return release; + return gitHubRelease; } else { return null; @@ -44,7 +50,7 @@ Future checkAppUpdates({ return null; } else { - return release; + return gitHubRelease; } } else { diff --git a/lib/functions/clear_dns_cache.dart b/lib/functions/clear_dns_cache.dart index 0ff1606..4a6fe53 100644 --- a/lib/functions/clear_dns_cache.dart +++ b/lib/functions/clear_dns_cache.dart @@ -4,27 +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'; -Future clearDnsCache(BuildContext context, Server server) async { +Future clearDnsCache(BuildContext context, Server server) async { final serversProvider = Provider.of(context, listen: false); - final ProcessModal processModal = ProcessModal(context: context); + final ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.clearingDnsCache); - final result = await serversProvider.apiClient!.resetDnsCache(); + 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 index 33ae8fe..b2f37e1 100644 --- a/lib/functions/compare_versions.dart +++ b/lib/functions/compare_versions.dart @@ -151,15 +151,11 @@ bool serverVersionIsAhead({ bool gitHubUpdateExists({ required String currentBuildNumber, - required List gitHubReleases, + required GitHubRelease gitHubRelease, required bool isBeta }) { - final release = isBeta == true - ? gitHubReleases.firstWhere((release) => release.prerelease == true) - : gitHubReleases.firstWhere((release) => release.prerelease == false); - final versionNumberRegex = RegExp(r'\(\d+\)'); - final releaseNumberExtractedMatches = versionNumberRegex.allMatches(release.tagName); + final releaseNumberExtractedMatches = versionNumberRegex.allMatches(gitHubRelease.tagName); if (releaseNumberExtractedMatches.isNotEmpty) { final releaseNumberExtracted = releaseNumberExtractedMatches.first.group(0); @@ -181,12 +177,12 @@ bool gitHubUpdateExists({ } } else { - Sentry.captureMessage("Invalid release number. Tagname: ${release.tagName}"); + Sentry.captureMessage("Invalid release number. Tagname: ${gitHubRelease.tagName}"); return false; } } else { - Sentry.captureMessage("No matches. ${release.tagName}"); + Sentry.captureMessage("No matches. ${gitHubRelease.tagName}"); return false; } } \ 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/l10n/app_en.arb b/lib/l10n/app_en.arb index 37e0acb..3778a60 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", @@ -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", @@ -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...", @@ -649,7 +651,7 @@ "october": "October", "november": "November", "december": "December", - "malwarePhising": "Malware/phising", + "malwarePhishing": "Malware/phishing", "queries": "Queries", "adultSites": "Adult sites", "quickFilters": "Quick filters", @@ -660,5 +662,69 @@ "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?" + "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": "Configuración de la caché DNS upstream", + "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." } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 4b2af05..03e3440 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", @@ -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...", @@ -649,7 +651,7 @@ "october": "Octubre", "november": "Noviembre", "december": "Diciembre", - "malwarePhising": "Malware/phising", + "malwarePhishing": "Malware/phising", "queries": "Peticiones", "adultSites": "Sitios de adultos", "quickFilters": "Filtros rápidos", @@ -660,5 +662,69 @@ "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?" + "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." } \ No newline at end of file diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index a3dabaa..077fcbb 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -170,7 +170,7 @@ "dnsQueries": "Zapytania DNS", "average": "Średnia", "blockedFilters": "Zablokowane przez filtry", - "malwarePhisingBlocked": "Zablokowane złośliwe oprogramowanie/phishing", + "malwarePhishingBlocked": "Zablokowane złośliwe oprogramowanie/phishing", "blockedAdultWebsites": "Zablokowane witryny dla dorosłych", "generalSettings": "Ustawienia główne", "generalSettingsDescription": "Różne ustawienia", @@ -649,7 +649,7 @@ "october": "Październik", "november": "Listopad", "december": "Grudzień", - "malwarePhising": "Złośliwe oprogramowanie / wyłudzanie informacji", + "malwarePhishing": "Złośliwe oprogramowanie / wyłudzanie informacji", "queries": "Zapytania", "adultSites": "Strony dla dorosłych", "quickFilters": "Szybkie filtry", diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb new file mode 100644 index 0000000..06c3b08 --- /dev/null +++ b/lib/l10n/app_tr.arb @@ -0,0 +1,692 @@ +{ + "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", + "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 ayarla", + "edit": "Düzenle", + "delete": "Sil", + "save": "Kaydet", + "serverStatus": "Sunucu durumu", + "connectionNotUpdated": "Bağlantı Güncellenmedi", + "ruleFilteringWidget": "Kural filtreleme", + "safeBrowsingWidget": "Güvenli gezinti", + "parentalFilteringWidget": "Ebeveyn filtreleme", + "safeSearchWidget": "Güvenli arama", + "ruleFiltering": "Kural filtreleme", + "safeBrowsing": "Güvenli gezinti", + "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": "Aydınlık", + "dark": "Karanlık", + "systemDefined": "Sistemle uyumlu hale getir", + "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": "Ters proxy kullanıyorsanız", + "aboutApp": "Uygulama hakkında", + "appVersion": "Uygulama sürümü", + "createdBy": "Geliştirici", + "clients": "İstemciler", + "allowed": "İzin verildi", + "blocked": "Engellendi", + "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ı asla 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 gezinti", + "blockedSafeBrowsingRow": "Engellendi (Güvenli gezinti)", + "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": "Zaman", + "blocklist": "Engelleme Listesi", + "request": "İstek", + "domain": "Alan adı", + "type": "Tip", + "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çin", + "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 gezintiyi etkinleştir", + "enableParentalControl": "Ebeveyn kontrolünü etkinleştir", + "enableSafeSearch": "Güvenli aramayı aktif et", + "blockedServices": "Engellenen hizmetler", + "selectBlockedServices": "Engellenen hizmetleri seç", + "noBlockedServicesSelected": "Engellenen hizmetler seçilmedi", + "services": "Hizmetler", + "servicesBlocked": "Hizmetler engellendi", + "tagsSelected": "Seçilen etiketler", + "upstreamServers": "Üst kaynak sunucuları", + "serverAddress": "Sunucu adresi", + "noUpstreamServers": "Üst kaynak sunucusu yok.", + "willBeUsedGeneralServers": "Genel üst kaynak sunucuları kullanılacak.", + "added": "Eklenenler", + "clientUpdatedSuccessfully": "İstemci 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 kurallar", + "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 kesin 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 etkin mi?", + "dhcpAvailable": "DHCP mevcut mu?", + "serverRunning": "Sunucu çalışıyor mu?", + "serverVersion": "Sunucu sürümü", + "serverLanguage": "Sunucu dili", + "yes": "Evet", + "no": "Hayır", + "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": "İstemciye izin verme", + "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": "Ekle ($important)", + "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çin", + "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 kiralamaları", + "noDhcpStaticLeases": "DHCP statik kiralamaları bulunamadı", + "deleting": "Siliniyor...", + "staticLeaseDeleted": "DHCP statik kiralama başarıyla silindi", + "staticLeaseNotDeleted": "DHCP statik kiralaması silinemedi", + "deleteStaticLease": "Statik kiralamayı sil", + "deleteStaticLeaseDescription": "DHCP statik kirası silinecek. Bu işlem geri alınamaz.", + "addStaticLease": "Statik kiralama 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 kiralaması başarıyla oluşturuldu", + "staticLeaseNotCreated": "DHCP statik kiralaması oluşturulamadı", + "staticLeaseExists": "DHCP statik kiralaması zaten mevcut", + "serverNotConfigured": "Sunucu yapılandırılmamış", + "restoreLeases": "Kiralamaları sıfırla", + "restoreLeasesMessage": "Devam etmek istediğinizden emin misiniz? Bu, mevcut tüm kiralamaları sıfırlayacaktır. Bu işlem geri alınamaz.", + "restoringLeases": "Kiralamalar sıfırlanıyor...", + "leasesRestored": "Kiralamalar başarıyla sıfırlandı", + "leasesNotRestored": "Kiralar sıfırlanamadı", + "dhcpLeases": "DHCP kiralamaları", + "noLeases": "Kullanılabilir DHCP kiralaması 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ı 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 anonimleştir", + "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ı başarıyla güncellendi", + "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 kaynak DNS sunucuları", + "bootstrapDns": "Önyükleme DNS sunucuları", + "noUpstreamDns": "Üst kaynak 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 kaynak sunucusuna sorgu yap. 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 kaynak sunucularını aynı anda sorgulayarak çözümlemeyi hızlandırmak için paralel sorgular kullanın.", + "fastestIpAddressDescription": "Tüm DNS sunucularına sorgu yapın ve tüm yanıtlar arasında en hızlı IP adresini döndürün. 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 kaynak sunucularını 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": "İstemcilerin IP adreslerini karşılık gelen çözücülere PTR sorguları göndererek IP adreslerini tersine çözümleyerek (yerel istemciler için özel DNS sunucuları, genel IP adresine sahip istemciler için üst kaynak sunucuları) istemcilerin ana bilgisayar adlarını tersine çöz.", + "dnsServerSettings": "AdGuard Home DNS sunucusu ayarları", + "limitRequestsSecond": "Saniye başına sınırlama isteği", + "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) ekleyin ve istemciler tarafından gönderilen değerleri sorgu günlüğüne kaydedin.", + "enableDnssec": "DNSSEC'i etkinleştir", + "enableDnssecDescription": "Giden DNS sorguları için DNSSEC özelliğini etkinleştir ve sonucu kontrol edin.(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ın (AAAA yazın) ve HTTPS yanıtlarından IPv6 ipuçlarını kaldırın.", + "blockingMode": "Engelleme modu", + "defaultMode": "Varsayılan", + "defaultDescription": "Reklam engelleme tarzı bir kural tarafından engellendiğinde sıfır IP adresi ile yanıt verin. (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 verin.", + "refusedDescription": "REFUSED kodu ile yanıt verin.", + "nxdomainDescription": "NXDOMAIN kodu ile yanıt verin.", + "nullIp": "Boş IP", + "nullIpDescription": "Sıfır IP adresi ile yanıt verin. (A için 0.0.0.0; :: AAAA için)", + "customIp": "Özel IP", + "customIpDescription": "Manuel olarak ayarlanmış bir IP adresi ile yanıt verin.", + "dnsCacheConfig": "DNS önbellek yapılandırması", + "cacheSize": "Önbellek boyutu", + "inBytes": "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 ayarlayın (saniye olarak)", + "overrideMaximumTtl": "Maksimum kullanım süresini geçersiz kıl", + "overrideMaximumTtlDescription": "DNS önbelleğindeki girişler için maksimum kullanım süresi değerini ayarlayın (saniye olarak)", + "optimisticCaching": "İyimser önbelleğe alma", + "optimisticCachingDescription": "Girişlerin süresi dolmuş olsa bile Adguard Home'un önbellekten yanıt vermesini sağlayın ve aynı zamanda bunları yenilemeye çalışın.", + "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 kaynak sunucularını 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. Onu eklemenize gerek yok, otomatik olarak eklenir.", + "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": "Sertifika 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ş anahtarı kullan", + "privateKeyPath": "Özel anahtar 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üklerdeki işlem süresi", + "timeLogsDescription": "Günlükler listesinde zaman yerine işlem süresini göster", + "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": "Mevcut 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": "Liste bağlantısını kopyala", + "listUrlCopied": "Panoya kopyalanan bağlantı adresini listeleyin", + "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üklerdeki IP", + "ipLogsDescription": "Günlükler listesinde istemci adı yerine IP adresini göster", + "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": "İnternette alan adı ara", + "hideServerAddress": "Sunucu adresini gizle", + "hideServerAddressDescription": "Ana ekranda sunucu adresini gizler.", + "topItemsOrder": "Öne çıkan öğeler sıralaması", + "topItemsOrderDescription": "Ana ekrandaki öne çıkan öğe listelerini sırala", + "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 üst kaynaklar", + "averageUpstreamResponseTime": "Üst kaynak ortalama yanıt süresi" +} diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index f6a5eaf..8d246eb 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -170,7 +170,7 @@ "dnsQueries": "DNS 查询", "average": "平均值", "blockedFilters": "被过滤器拦截", - "malwarePhisingBlocked": "被拦截的恶意/钓鱼网站", + "malwarePhishingBlocked": "被拦截的恶意/钓鱼网站", "blockedAdultWebsites": "被拦截的成人网站", "generalSettings": "常规设置", "generalSettingsDescription": "各种不同的设置", @@ -649,9 +649,16 @@ "october": "10月", "november": "11月", "december": "12月", - "malwarePhising": "恶意/钓鱼网站", + "malwarePhishing": "恶意/钓鱼网站", "queries": "查询", "adultSites": "成人网站", "quickFilters": "状态过滤器", - "searchDomainInternet": "在互联网上搜索该域名" + "searchDomainInternet": "在互联网上搜索该域名", + "hideServerAddress": "隐藏服务器地址", + "hideServerAddressDescription": "在主页上隐藏服务器地址", + "topItemsOrder": "顶部项目顺序", + "topItemsOrderDescription": "排列主页顶部项目列表", + "topItemsReorderInfo": "按住并滑动一个项目以重新排序。", + "discardChanges": "放弃更改", + "discardChangesDescription": "您确定要放弃更改吗?" } diff --git a/lib/l10n/app_zh_CN.arb b/lib/l10n/app_zh_CN.arb index f6a5eaf..8d246eb 100644 --- a/lib/l10n/app_zh_CN.arb +++ b/lib/l10n/app_zh_CN.arb @@ -170,7 +170,7 @@ "dnsQueries": "DNS 查询", "average": "平均值", "blockedFilters": "被过滤器拦截", - "malwarePhisingBlocked": "被拦截的恶意/钓鱼网站", + "malwarePhishingBlocked": "被拦截的恶意/钓鱼网站", "blockedAdultWebsites": "被拦截的成人网站", "generalSettings": "常规设置", "generalSettingsDescription": "各种不同的设置", @@ -649,9 +649,16 @@ "october": "10月", "november": "11月", "december": "12月", - "malwarePhising": "恶意/钓鱼网站", + "malwarePhishing": "恶意/钓鱼网站", "queries": "查询", "adultSites": "成人网站", "quickFilters": "状态过滤器", - "searchDomainInternet": "在互联网上搜索该域名" + "searchDomainInternet": "在互联网上搜索该域名", + "hideServerAddress": "隐藏服务器地址", + "hideServerAddressDescription": "在主页上隐藏服务器地址", + "topItemsOrder": "顶部项目顺序", + "topItemsOrderDescription": "排列主页顶部项目列表", + "topItemsReorderInfo": "按住并滑动一个项目以重新排序。", + "discardChanges": "放弃更改", + "discardChangesDescription": "您确定要放弃更改吗?" } diff --git a/lib/main.dart b/lib/main.dart index a21a99e..e6fb4ec 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,9 +2,10 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter_displaymode/flutter_displaymode.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:provider/provider.dart'; +import 'package:flutter_displaymode/flutter_displaymode.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'; @@ -15,7 +16,8 @@ import 'package:window_size/window_size.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:adguard_home_manager/base.dart'; +import 'package:adguard_home_manager/widgets/layout.dart'; +import 'package:adguard_home_manager/widgets/menu_bar.dart'; import 'package:adguard_home_manager/providers/logs_provider.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; @@ -34,6 +36,7 @@ import 'package:adguard_home_manager/services/db/database.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { setWindowMinSize(const Size(500, 500)); @@ -196,6 +199,7 @@ class _MainState extends State
    { @override void initState() { displayMode(); + super.initState(); } @@ -230,22 +234,16 @@ class _MainState extends State
    { Locale('zh', ''), Locale('zh', 'CN'), Locale('pl', ''), + Locale('tr', ''), Locale('ru', '') ], scaffoldMessengerKey: scaffoldMessengerKey, - builder: (context, child) { - return MediaQuery( - data: MediaQuery.of(context).copyWith( - textScaleFactor: !(Platform.isAndroid || Platform.isIOS) - ? 0.9 - : 1.0 - ), - child: child!, - ); - }, - home: const Base(), + navigatorKey: globalNavigatorKey, + builder: (context, child) => CustomMenuBar( + child: child!, + ), + 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/clients.dart b/lib/models/clients.dart index 4d8e7db..67740bd 100644 --- a/lib/models/clients.dart +++ b/lib/models/clients.dart @@ -84,10 +84,13 @@ 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; Client({ required this.name, @@ -98,10 +101,13 @@ 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, }); factory Client.fromJson(Map json) => Client( @@ -113,12 +119,15 @@ 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 + : null, + ignoreQuerylog: json["ignore_querylog"], + ignoreStatistics: json["ignore_statistics"], + upstreamsCacheEnabled: json["upstreams_cache_enabled"], + upstreamsCacheSize: json["upstreams_cache_size"] ); Map toJson() => { @@ -130,9 +139,12 @@ 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 }; } \ No newline at end of file diff --git a/lib/models/dhcp.dart b/lib/models/dhcp.dart index aaf4a42..9e7f7ca 100644 --- a/lib/models/dhcp.dart +++ b/lib/models/dhcp.dart @@ -1,9 +1,11 @@ import 'dart:convert'; class DhcpModel { + bool dhcpAvailable; List networkInterfaces; - DhcpStatus dhcpStatus; + DhcpStatus? dhcpStatus; DhcpModel({ + required this.dhcpAvailable, required this.networkInterfaces, required this.dhcpStatus, }); @@ -72,11 +74,11 @@ 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() => { diff --git a/lib/models/dns_info.dart b/lib/models/dns_info.dart index 138efa3..c0cabd6 100644 --- a/lib/models/dns_info.dart +++ b/lib/models/dns_info.dart @@ -2,32 +2,39 @@ class DnsInfo { List upstreamDns; 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; + bool? cacheOptimistic; + bool? resolveClients; + bool? usePrivatePtrResolvers; List localPtrUpstreams; String blockingIpv4; String blockingIpv6; List defaultLocalPtrUpstreams; + int? blockedResponseTtl; 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, @@ -41,16 +48,20 @@ class DnsInfo { required this.blockingIpv4, required this.blockingIpv6, required this.defaultLocalPtrUpstreams, + required this.blockedResponseTtl, }); 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: 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"], @@ -64,16 +75,20 @@ class DnsInfo { 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"] ); 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, @@ -87,5 +102,6 @@ class DnsInfo { "blocking_ipv4": blockingIpv4, "blocking_ipv6": blockingIpv6, "default_local_ptr_upstreams": List.from(defaultLocalPtrUpstreams.map((x) => x)), + "blocked_response_ttl": blockedResponseTtl }; } diff --git a/lib/models/dns_statistics.dart b/lib/models/dns_statistics.dart index 45993bd..f279886 100644 --- a/lib/models/dns_statistics.dart +++ b/lib/models/dns_statistics.dart @@ -9,6 +9,8 @@ class DnsStatistics { 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, @@ -42,6 +46,8 @@ class DnsStatistics { 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)))), + 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 8f8a3a0..1315868 100644 --- a/lib/models/encryption.dart +++ b/lib/models/encryption.dart @@ -122,3 +122,128 @@ class EncryptionData { "private_key_saved": privateKeySaved, }; } + + +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 9b1d6ce..7b521d2 100644 --- a/lib/models/filtering.dart +++ b/lib/models/filtering.dart @@ -1,6 +1,6 @@ class Filtering { - final List filters; - final List whitelistFilters; + List filters; + List whitelistFilters; List userRules; List blockedServices; int interval; @@ -40,7 +40,7 @@ class Filter { final DateTime? lastUpdated; final int id; final int rulesCount; - final bool enabled; + bool enabled; Filter({ required this.url, @@ -69,3 +69,13 @@ class Filter { "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/providers/app_config_provider.dart b/lib/providers/app_config_provider.dart index 3522edc..d44b5af 100644 --- a/lib/providers/app_config_provider.dart +++ b/lib/providers/app_config_provider.dart @@ -52,6 +52,8 @@ class AppConfigProvider with ChangeNotifier { int _combinedChartHome = 0; + int _showTopItemsChart = 0; + String? _doNotRememberVersion; GitHubRelease? _appUpdatesAvailable; @@ -168,6 +170,10 @@ class AppConfigProvider with ChangeNotifier { return _hideServerAddress == 1 ? true : false; } + bool get showTopItemsChart { + return _showTopItemsChart == 1 ? true : false; + } + void setDbInstance(Database db) { _dbInstance = db; } @@ -402,6 +408,22 @@ class AppConfigProvider with ChangeNotifier { } } + Future setShowTopItemsChart(bool value) async { + final updated = await updateConfigQuery( + db: _dbInstance!, + column: 'showTopItemsChart', + value: value == true ? 1 : 0 + ); + if (updated == true) { + _showTopItemsChart = value == true ? 1 : 0; + notifyListeners(); + return true; + } + else { + return false; + } + } + Future setDoNotRememberVersion(String value) async { final updated = await updateConfigQuery( @@ -424,9 +446,10 @@ class AppConfigProvider with ChangeNotifier { _showIpLogs = dbData['showIpLogs'] ?? 0; _combinedChartHome = dbData['combinedChart'] ?? 0; _hideServerAddress = dbData['hideServerAddress']; + _showTopItemsChart = dbData['showTopItemsChart']; if (dbData['homeTopItemsOrder'] != null) { try { - _homeTopItemsOrder = List.from( + final itemsOrder = List.from( List.from(jsonDecode(dbData['homeTopItemsOrder'])).map((e) { switch (e) { case 'queriedDomains': @@ -438,11 +461,22 @@ class AppConfigProvider with ChangeNotifier { case 'recurrentClients': return HomeTopItems.recurrentClients; + case 'topUpstreams': + return HomeTopItems.topUpstreams; + + case 'avgUpstreamResponseTime': + return HomeTopItems.avgUpstreamResponseTime; + default: return null; } }).where((e) => e != null).toList() ); + final missingItems = homeTopItemsDefaultOrder.where((e) => !itemsOrder.contains(e)); + _homeTopItemsOrder = [ + ...itemsOrder, + ...missingItems + ]; } catch (e) { Sentry.captureException(e); _homeTopItemsOrder = homeTopItemsDefaultOrder; diff --git a/lib/providers/clients_provider.dart b/lib/providers/clients_provider.dart index 8cf79a5..36185cc 100644 --- a/lib/providers/clients_provider.dart +++ b/lib/providers/clients_provider.dart @@ -1,20 +1,20 @@ 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/compare_versions.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; - StatusProvider? _statusProvider; update(ServersProvider? servers, StatusProvider? status) { _serversProvider = servers; - _statusProvider = status; } LoadStatus _loadStatus = LoadStatus.loading; @@ -103,9 +103,9 @@ class ClientsProvider with ChangeNotifier { if (updateLoading == true) { _loadStatus = LoadStatus.loading; } - final result = await _serversProvider!.apiClient!.getClients(); - if (result['result'] == 'success') { - setClientsData(result['data'], false); + final result = await _serversProvider!.apiClient2!.getClients(); + if (result.successful == true) { + setClientsData(result.content as Clients, false); _loadStatus = LoadStatus.loaded; notifyListeners(); return true; @@ -120,9 +120,9 @@ class ClientsProvider with ChangeNotifier { } Future deleteClient(Client client) async { - final result = await _serversProvider!.apiClient!.postDeleteClient(name: client.name); + final result = await _serversProvider!.apiClient2!.postDeleteClient(name: client.name); - if (result['result'] == 'success') { + if (result.successful == true) { Clients clientsData = clients!; clientsData.clients = clientsData.clients.where((c) => c.name != client.name).toList(); setClientsData(clientsData, false); @@ -136,20 +136,14 @@ class ClientsProvider with ChangeNotifier { } Future editClient(Client client) async { - final result = await _serversProvider!.apiClient!.postUpdateClient( + final result = await _serversProvider!.apiClient2!.postUpdateClient( data: { 'name': client.name, - 'data': serverVersionIsAhead( - currentVersion: _statusProvider!.serverStatus!.serverVersion, - referenceVersion: 'v0.107.28', - referenceVersionBeta: 'v0.108.0-b.33' - ) == false - ? removePropFromMap(client.toJson(), 'safesearch_enabled') - : removePropFromMap(client.toJson(), 'safe_search') + 'data': removePropFromMap(client.toJson(), 'safe_search') } ); - if (result['result'] == 'success') { + if (result.successful == true) { Clients clientsData = clients!; clientsData.clients = clientsData.clients.map((e) { if (e.name == client.name) { @@ -171,17 +165,11 @@ class ClientsProvider with ChangeNotifier { } Future addClient(Client client) async { - final result = await _serversProvider!.apiClient!.postAddClient( - data: serverVersionIsAhead( - currentVersion: _statusProvider!.serverStatus!.serverVersion, - referenceVersion: 'v0.107.28', - referenceVersionBeta: 'v0.108.0-b.33' - ) == false - ? removePropFromMap(client.toJson(), 'safesearch_enabled') - : removePropFromMap(client.toJson(), 'safe_search') + final result = await _serversProvider!.apiClient2!.postAddClient( + data: removePropFromMap(client.toJson(), 'safe_search') ); - if (result['result'] == 'success') { + if (result.successful == true) { Clients clientsData = clients!; clientsData.clients.add(client); setClientsData(clientsData, false); @@ -195,91 +183,105 @@ class ClientsProvider with ChangeNotifier { } } - Future> addClientList(String item, String type) async { + 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 (type == 'allowed') { + 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 == 'disallowed') { + else if (type == AccessSettingsList.disallowed) { body['disallowed_clients']!.add(item); } - else if (type == 'domains') { + else if (type == AccessSettingsList.domains) { body['blocked_hosts']!.add(item); } - final result = await _serversProvider!.apiClient!.requestAllowedBlockedClientsHosts(body); + final result = await _serversProvider!.apiClient2!.requestAllowedBlockedClientsHosts( + body: body + ); - if (result['result'] == 'success') { + if (result.successful == true) { _clients?.clientsAllowedBlocked = ClientsAllowedBlocked( allowedClients: body['allowed_clients'] ?? [], disallowedClients: body['disallowed_clients'] ?? [], blockedHosts: body['blocked_hosts'] ?? [], ); notifyListeners(); - return { 'success': true }; + return result; } - else if (result['result'] == 'error' && result['message'] == 'client_another_list') { + else if (result.successful == false && result.content == 'client_another_list') { notifyListeners(); - return { - 'success': false, - 'error': 'client_another_list' - }; + return result; } else { notifyListeners(); - return { - 'success': false, - 'error': null - }; + return result; } } - Future> removeClientList(String client, String type) async { + 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 == 'allowed') { + if (type == AccessSettingsList.allowed) { body['allowed_clients'] = body['allowed_clients']!.where((c) => c != client).toList(); } - else if (type == 'disallowed') { + else if (type == AccessSettingsList.disallowed) { body['disallowed_clients'] = body['disallowed_clients']!.where((c) => c != client).toList(); } - else if (type == 'domains') { + else if (type == AccessSettingsList.domains) { body['blocked_hosts'] = body['blocked_hosts']!.where((c) => c != client).toList(); } - final result = await _serversProvider!.apiClient!.requestAllowedBlockedClientsHosts(body); + final result = await _serversProvider!.apiClient2!.requestAllowedBlockedClientsHosts( + body: body + ); - if (result['result'] == 'success') { + if (result.successful == true) { _clients?.clientsAllowedBlocked = ClientsAllowedBlocked( allowedClients: body['allowed_clients'] ?? [], disallowedClients: body['disallowed_clients'] ?? [], blockedHosts: body['blocked_hosts'] ?? [], ); notifyListeners(); - return { 'success': true }; + return result; } - else if (result['result'] == 'error' && result['message'] == 'client_another_list') { + else if (result.successful == false && result.content == 'client_another_list') { notifyListeners(); - return { - 'success': false, - 'error': 'client_another_list' - }; + return result; } else { notifyListeners(); - return { - 'success': false, - 'error': null - }; + return result; } } } \ No newline at end of file diff --git a/lib/providers/dhcp_provider.dart b/lib/providers/dhcp_provider.dart index 2c2e7d9..4fbd66b 100644 --- a/lib/providers/dhcp_provider.dart +++ b/lib/providers/dhcp_provider.dart @@ -1,5 +1,6 @@ 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'; @@ -41,9 +42,9 @@ class DhcpProvider with ChangeNotifier { _loadStatus = LoadStatus.loading; notifyListeners(); } - final result = await _serversProvider!.apiClient!.getDhcpData(); - if (result['result'] == 'success') { - _dhcp = result['data']; + final result = await _serversProvider!.apiClient2!.getDhcpData(); + if (result.successful == true) { + _dhcp = result.content as DhcpModel; _loadStatus = LoadStatus.loaded; notifyListeners(); return true; @@ -58,7 +59,7 @@ class DhcpProvider with ChangeNotifier { } Future deleteLease(Lease lease) async { - final result = await _serversProvider!.apiClient!.deleteStaticLease( + final result = await _serversProvider!.apiClient2!.deleteStaticLease( data: { "mac": lease.mac, "ip": lease.ip, @@ -66,9 +67,9 @@ class DhcpProvider with ChangeNotifier { } ); - if (result['result'] == 'success') { + if (result.successful == true) { DhcpModel data = dhcp!; - data.dhcpStatus.staticLeases = data.dhcpStatus.staticLeases.where((l) => l.mac != lease.mac).toList(); + data.dhcpStatus!.staticLeases = data.dhcpStatus!.staticLeases.where((l) => l.mac != lease.mac).toList(); setDhcpData(data); return true; } @@ -78,8 +79,8 @@ class DhcpProvider with ChangeNotifier { } } - Future> createLease(Lease lease) async { - final result = await _serversProvider!.apiClient!.createStaticLease( + Future createLease(Lease lease) async { + final result = await _serversProvider!.apiClient2!.createStaticLease( data: { "mac": lease.mac, "ip": lease.ip, @@ -87,29 +88,14 @@ class DhcpProvider with ChangeNotifier { } ); - if (result['result'] == 'success') { + if (result.successful == true) { DhcpModel data = dhcp!; - data.dhcpStatus.staticLeases.add(lease); + data.dhcpStatus!.staticLeases.add(lease); setDhcpData(data); - return { 'success': true }; - } - else if (result['result'] == 'error' && result['message'] == 'already_exists' ) { - return { - 'success': false, - 'error': 'already_exists' - }; - } - else if (result['result'] == 'error' && result['message'] == 'server_not_configured' ) { - return { - 'success': false, - 'error': 'server_not_configured' - }; + return result; } else { - return { - 'success': false, - 'error': null - }; + return result; } } } \ No newline at end of file diff --git a/lib/providers/dns_provider.dart b/lib/providers/dns_provider.dart index 0965725..84b86cd 100644 --- a/lib/providers/dns_provider.dart +++ b/lib/providers/dns_provider.dart @@ -1,5 +1,6 @@ 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'; @@ -41,10 +42,10 @@ class DnsProvider with ChangeNotifier { _loadStatus = LoadStatus.loading; } - final result = await _serversProvider!.apiClient!.getDnsInfo(); + final result = await _serversProvider!.apiClient2!.getDnsInfo(); - if (result['result'] == 'success') { - _dnsInfo = result['data']; + if (result.successful == true) { + _dnsInfo = result.content as DnsInfo; _loadStatus = LoadStatus.loaded; notifyListeners(); return true; @@ -58,12 +59,12 @@ class DnsProvider with ChangeNotifier { } } - Future> savePrivateReverseServersConfig(Map value) async { - final result = await _serversProvider!.apiClient!.setDnsConfig( + Future savePrivateReverseServersConfig(Map value) async { + final result = await _serversProvider!.apiClient2!.setDnsConfig( data: value ); - if (result['result'] == 'success') { + if (result.successful == true) { DnsInfo data = dnsInfo!; if (value['local_ptr_upstreams'] != null) { data.localPtrUpstreams = value['local_ptr_upstreams']; @@ -71,107 +72,87 @@ class DnsProvider with ChangeNotifier { data.usePrivatePtrResolvers = value['use_private_ptr_resolvers']; data.resolveClients = value['resolve_clients']; setDnsInfoData(data); - return { 'success': true }; - } - else if (result['log'] != null && result['log'].statusCode == '400') { - return { - 'success': false, - 'error': 400 - }; + return result; } else { - return { - 'success': false, - 'error': null - }; - } + return result; + } } - Future> saveUpstreamDnsConfig(Map value) async { - final result = await _serversProvider!.apiClient!.setDnsConfig( + Future saveUpstreamDnsConfig(Map value) async { + final result = await _serversProvider!.apiClient2!.setDnsConfig( data: value ); - if (result['result'] == 'success') { + if (result.successful == true) { DnsInfo data = dnsInfo!; data.upstreamDns = List.from(value['upstream_dns']); data.upstreamMode = value['upstream_mode']; setDnsInfoData(data); - return { 'success': true }; - } - else if (result['log'] != null && result['log'].statusCode == '400') { - return { - 'success': false, - 'error': 400 - }; + return result; } else { - return { - 'success': false, - 'error': null - }; - } + return result; + } } - Future> saveBootstrapDnsConfig(Map value) async { - final result = await _serversProvider!.apiClient!.setDnsConfig( + Future saveBootstrapDnsConfig(Map value) async { + final result = await _serversProvider!.apiClient2!.setDnsConfig( data: value ); - if (result['result'] == 'success') { + if (result.successful == true) { DnsInfo data = dnsInfo!; data.bootstrapDns = List.from(value['bootstrap_dns']); setDnsInfoData(data); - return { 'success': true }; - } - else if (result['log'] != null && result['log'].statusCode == '400') { - return { - 'success': false, - 'error': 400 - }; + return result; } else { - return { - 'success': false, - 'error': null - }; - } + return result; + } } - Future> saveCacheCacheConfig(Map value) async { - final result = await _serversProvider!.apiClient!.setDnsConfig( + Future saveFallbackDnsConfig(Map value) async { + final result = await _serversProvider!.apiClient2!.setDnsConfig( data: value ); - if (result['result'] == 'success') { + 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 { 'success': true }; - } - else if (result['log'] != null && result['log'].statusCode == '400') { - return { - 'success': false, - 'error': 400 - }; + return result; } else { - return { - 'success': false, - 'error': null - }; - } + return result; + } } - Future> saveDnsServerConfig(Map value) async { - final result = await _serversProvider!.apiClient!.setDnsConfig( + Future saveDnsServerConfig(Map value) async { + final result = await _serversProvider!.apiClient2!.setDnsConfig( data: value ); - if (result['result'] == 'success') { + if (result.successful == true) { DnsInfo data = dnsInfo!; data.ratelimit = value['ratelimit']; data.ednsCsEnabled = value['edns_cs_enabled']; @@ -180,20 +161,12 @@ class DnsProvider with ChangeNotifier { data.blockingMode = value['blocking_mode']; data.blockingIpv4 = value['blocking_ipv4']; data.blockingIpv6 = value['blocking_ipv6']; + data.blockedResponseTtl = value['blocked_response_ttl']; setDnsInfoData(data); - return { 'success': true }; - } - else if (result['log'] != null && result['log'].statusCode == '400') { - return { - 'success': false, - 'error': 400 - }; + return result; } else { - return { - 'success': false, - 'error': null - }; - } + return result; + } } } \ No newline at end of file diff --git a/lib/providers/filtering_provider.dart b/lib/providers/filtering_provider.dart index 8b2d459..a264429 100644 --- a/lib/providers/filtering_provider.dart +++ b/lib/providers/filtering_provider.dart @@ -88,10 +88,10 @@ class FilteringProvider with ChangeNotifier { _blockedServicesLoadStatus = LoadStatus.loading; if (showLoader == true) notifyListeners(); - final result = await _serversProvider!.apiClient!.getBlockedServices(); - if (result['result'] == 'success') { + final result = await _serversProvider!.apiClient2!.getBlockedServices(); + if (result.successful == true) { _blockedServicesLoadStatus = LoadStatus.loaded; - _blockedServicesList = BlockedServices(services: result['data']); + _blockedServicesList = BlockedServices(services: result.content as List); notifyListeners(); return true; @@ -112,9 +112,9 @@ class FilteringProvider with ChangeNotifier { _loadStatus = LoadStatus.loading; } - final result = await _serversProvider!.apiClient!.getFiltering(); - if (result['result'] == 'success') { - _filtering = result['data']; + final result = await _serversProvider!.apiClient2!.getFiltering(); + if (result.successful == true) { + _filtering = result.content as Filtering; _loadStatus = LoadStatus.loaded; notifyListeners(); return true; @@ -127,15 +127,16 @@ class FilteringProvider with ChangeNotifier { } Future> updateLists() async { - final result = await _serversProvider!.apiClient!.updateLists(); - if (result['result'] == 'success') { - final result2 = await _serversProvider!.apiClient!.getFiltering(); - if (result2['result'] == 'success') { - _filtering = result2['data']; + 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(); + print(result.content); return { "success": true, - "data": result['data'] + "data": result.content }; } else { @@ -151,10 +152,10 @@ class FilteringProvider with ChangeNotifier { Future enableDisableFiltering() async { final newValue = !_statusProvider!.serverStatus!.filteringEnabled; - final result = await _serversProvider!.apiClient!.updateFiltering( + final result = await _serversProvider!.apiClient2!.updateFiltering( enable: newValue ); - if (result['result'] == 'success') { + if (result.successful == true) { setFilteringProtectionStatus(newValue, false); notifyListeners(); return true; @@ -166,13 +167,13 @@ class FilteringProvider with ChangeNotifier { } Future changeUpdateFrequency(int value) async { - final result = await _serversProvider!.apiClient!.requestChangeUpdateFrequency( + final result = await _serversProvider!.apiClient2!.requestChangeUpdateFrequency( data: { "enabled": filtering!.enabled, "interval": value } ); - if (result['result'] == 'success') { + if (result.successful == true) { setFiltersUpdateFrequency(value); return true; } @@ -185,9 +186,9 @@ class FilteringProvider with ChangeNotifier { Future removeCustomRule(String rule) async { final List newRules = filtering!.userRules.where((r) => r != rule).toList(); - final result = await _serversProvider!.apiClient!.setCustomRules(rules: newRules); + final result = await _serversProvider!.apiClient2!.setCustomRules(rules: newRules); - if (result['result'] == 'success') { + if (result.successful == true) { Filtering filteringData = filtering!; filteringData.userRules = newRules; _filtering = filteringData; @@ -205,18 +206,18 @@ class FilteringProvider with ChangeNotifier { required String listUrl, required String type }) async { - final result1 = await _serversProvider!.apiClient!.deleteFilterList( + final result1 = await _serversProvider!.apiClient2!.deleteFilterList( data: { "url": listUrl, "whitelist": type == 'whitelist' ? true : false } ); - if (result1['result'] == 'success') { - final result2 = await _serversProvider!.apiClient!.getFiltering(); + if (result1.successful == true) { + final result2 = await _serversProvider!.apiClient2!.getFiltering(); - if (result2['result'] == 'success') { - _filtering = result2['data']; + if (result2.successful == true) { + _filtering = result2.content as Filtering; notifyListeners(); return true; } @@ -236,7 +237,7 @@ class FilteringProvider with ChangeNotifier { required String type, required FilteringListActions action }) async { - final result1 = await _serversProvider!.apiClient!.updateFilterList( + final result1 = await _serversProvider!.apiClient2!.updateFilterList( data: { "data": { "enabled": action == FilteringListActions.disable || action == FilteringListActions.enable @@ -250,11 +251,11 @@ class FilteringProvider with ChangeNotifier { } ); - if (result1['result'] == 'success') { - final result2 = await _serversProvider!.apiClient!.getFiltering(); + if (result1.successful == true) { + final result2 = await _serversProvider!.apiClient2!.getFiltering(); - if (result2['result'] == 'success') { - _filtering = result2['data']; + if (result2.successful == true) { + _filtering = result2.content as Filtering; notifyListeners(); return true; } @@ -273,9 +274,9 @@ class FilteringProvider with ChangeNotifier { final List newRules = filtering!.userRules; newRules.add(rule); - final result = await _serversProvider!.apiClient!.setCustomRules(rules: newRules); - - if (result['result'] == 'success') { + final result = await _serversProvider!.apiClient2!.setCustomRules(rules: newRules); + + if (result.successful == true) { Filtering filteringData = filtering!; filteringData.userRules = newRules; _filtering = filteringData; @@ -287,8 +288,9 @@ class FilteringProvider with ChangeNotifier { return false; } } + Future> addList({required String name, required String url, required String type}) async { - final result1 = await _serversProvider!.apiClient!.addFilteringList( + final result1 = await _serversProvider!.apiClient2!.addFilteringList( data: { 'name': name, 'url': url, @@ -296,13 +298,13 @@ class FilteringProvider with ChangeNotifier { } ); - if (result1['result'] == 'success') { - if (result1['data'].toString().contains("OK")) { - final result2 = await _serversProvider!.apiClient!.getFiltering(); - final items = result1['data'].toString().split(' ')[1]; + 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['result'] == 'success') { - _filtering = result2['data']; + if (result2.successful == true) { + _filtering = result2.content as Filtering; notifyListeners(); return { 'success': true, @@ -325,14 +327,14 @@ class FilteringProvider with ChangeNotifier { }; } } - else if (result1['result'] == 'error' && result1['log'].statusCode == '400' && result1['log'].resBody.toString().contains("Couldn't fetch filter from url")) { + 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['result'] == 'error' && result1['log'].statusCode == '400' && result1['log'].resBody.toString().contains('Filter URL already added')) { + else if (result1.successful == false && result1.statusCode == 400 && result1.content.toString().contains('url already exists')) { notifyListeners(); return { 'success': false, @@ -355,9 +357,9 @@ class FilteringProvider with ChangeNotifier { _blockedServicesLoadStatus = LoadStatus.loading; } - final result = await _serversProvider!.apiClient!.getBlockedServices(); - if (result['result'] == 'success') { - _blockedServicesList = BlockedServices(services: result['data']); + final result = await _serversProvider!.apiClient2!.getBlockedServices(); + if (result.successful == true) { + _blockedServicesList = BlockedServices(services: result.content as List); _blockedServicesLoadStatus = LoadStatus.loaded; notifyListeners(); @@ -371,11 +373,11 @@ class FilteringProvider with ChangeNotifier { } Future updateBlockedServices(List values) async { - final result = await _serversProvider!.apiClient!.setBlockedServices( + final result = await _serversProvider!.apiClient2!.setBlockedServices( data: values ); - if (result['result'] == 'success') { + if (result.successful == true) { setBlockedServices(values); return true; } @@ -384,4 +386,75 @@ class FilteringProvider with ChangeNotifier { 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/logs_provider.dart b/lib/providers/logs_provider.dart index a3211b5..d9bc55f 100644 --- a/lib/providers/logs_provider.dart +++ b/lib/providers/logs_provider.dart @@ -114,9 +114,15 @@ 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 setSearchText(String? value) { @@ -153,7 +159,7 @@ class LogsProvider with ChangeNotifier { notifyListeners(); } - final result = await _serversProvider!.apiClient!.getLogs( + final result = await _serversProvider!.apiClient2!.getLogs( count: logsQuantity, offset: offst, olderThan: logsOlderThan, @@ -166,11 +172,11 @@ class LogsProvider with ChangeNotifier { notifyListeners(); } - if (result['result'] == 'success') { + if (result.successful == true) { _offset = inOffset != null ? inOffset+logsQuantity : offset+logsQuantity; if (loadingMore != null && loadingMore == true && logsData != null) { - LogsData newLogsData = result['data']; - newLogsData.data = [...logsData!.data, ...result['data'].data]; + LogsData newLogsData = result.content; + newLogsData.data = [...logsData!.data, ...(result.content as LogsData).data]; if (appliedFilters.clients != null) { newLogsData.data = newLogsData.data.where( (item) => appliedFilters.clients!.contains(item.client) @@ -179,7 +185,7 @@ class LogsProvider with ChangeNotifier { _logsData = newLogsData; } else { - LogsData newLogsData = result['data']; + LogsData newLogsData = result.content; if (appliedFilters.clients != null) { newLogsData.data = newLogsData.data.where( (item) => appliedFilters.clients!.contains(item.client) @@ -204,7 +210,7 @@ class LogsProvider with ChangeNotifier { resetFilters(); - final result = await _serversProvider!.apiClient!.getLogs( + final result = await _serversProvider!.apiClient2!.getLogs( count: logsQuantity ); @@ -214,8 +220,8 @@ class LogsProvider with ChangeNotifier { clients: null ); - if (result['result'] == 'success') { - _logsData = result['data']; + if (result.successful == true) { + _logsData = result.content as LogsData; _loadStatus = LoadStatus.loaded; notifyListeners(); return true; @@ -233,7 +239,7 @@ class LogsProvider with ChangeNotifier { setOffset(0); - final result = await _serversProvider!.apiClient!.getLogs( + final result = await _serversProvider!.apiClient2!.getLogs( count: logsQuantity, olderThan: logsOlderThan, responseStatus: selectedResultStatus, @@ -246,8 +252,8 @@ class LogsProvider with ChangeNotifier { clients: selectedClients ); - if (result['result'] == 'success') { - LogsData newLogsData = result['data']; + if (result.successful == true) { + LogsData newLogsData = result.content as LogsData; if (appliedFilters.clients != null) { newLogsData.data = newLogsData.data.where( (item) => appliedFilters.clients!.contains(item.client) diff --git a/lib/providers/rewrite_rules_provider.dart b/lib/providers/rewrite_rules_provider.dart index 1e3bda3..ec8ea1d 100644 --- a/lib/providers/rewrite_rules_provider.dart +++ b/lib/providers/rewrite_rules_provider.dart @@ -35,14 +35,14 @@ class RewriteRulesProvider with ChangeNotifier { } Future addDnsRewrite(RewriteRules rule) async { - final result = await _serversProvider!.apiClient!.addDnsRewriteRule( + final result = await _serversProvider!.apiClient2!.addDnsRewriteRule( data: { "domain": rule.domain, "answer": rule.answer } ); - if (result['result'] == 'success') { + if (result.successful == true) { List data = rewriteRules!; data.add(rule); setRewriteRulesData(data); @@ -55,7 +55,7 @@ class RewriteRulesProvider with ChangeNotifier { } Future editDnsRewrite(RewriteRules newRule, RewriteRules oldRule) async { - final result = await _serversProvider!.apiClient!.updateRewriteRule( + final result = await _serversProvider!.apiClient2!.updateRewriteRule( body: { "target": { "answer": oldRule.answer, @@ -68,7 +68,7 @@ class RewriteRulesProvider with ChangeNotifier { } ); - if (result['result'] == 'success') { + if (result.successful == true) { List data = rewriteRules!; final index = data.indexOf(oldRule); data[index] = newRule; @@ -82,14 +82,14 @@ class RewriteRulesProvider with ChangeNotifier { } Future deleteDnsRewrite(RewriteRules rule) async { - final result = await _serversProvider!.apiClient!.deleteDnsRewriteRule( + final result = await _serversProvider!.apiClient2!.deleteDnsRewriteRule( data: { "domain": rule.domain, "answer": rule.answer } ); - if (result['result'] == 'success') { + if (result.successful == true) { List data = rewriteRules!; data = data.where((item) => item.domain != rule.domain).toList(); setRewriteRulesData(data); @@ -108,10 +108,10 @@ class RewriteRulesProvider with ChangeNotifier { _loadStatus = LoadStatus.loading; } - final result = await _serversProvider!.apiClient!.getDnsRewriteRules(); + final result = await _serversProvider!.apiClient2!.getDnsRewriteRules(); - if (result['result'] == 'success') { - _rewriteRules = result['data']; + if (result.successful == true) { + _rewriteRules = result.content as List; _loadStatus = LoadStatus.loaded; notifyListeners(); return true; diff --git a/lib/providers/servers_provider.dart b/lib/providers/servers_provider.dart index c745260..55d6363 100644 --- a/lib/providers/servers_provider.dart +++ b/lib/providers/servers_provider.dart @@ -3,9 +3,11 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:sqflite/sqflite.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/models/update_available.dart'; -import 'package:adguard_home_manager/services/http_requests.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'; @@ -15,7 +17,8 @@ class ServersProvider with ChangeNotifier { List _serversList = []; Server? _selectedServer; - ApiClient? _apiClient; + // ApiClient? _apiClient; + ApiClientV2? _apiClient2; bool _updatingServer = false; @@ -24,8 +27,12 @@ class ServersProvider with ChangeNotifier { data: null, ); - ApiClient? get apiClient { - return _apiClient; + // ApiClient? get apiClient { + // return _apiClient; + // } + + ApiClientV2? get apiClient2 { + return _apiClient2; } List get serversList { @@ -53,7 +60,7 @@ class ServersProvider with ChangeNotifier { notifyListeners(); } - void setSelectedServer(Server server) { + void setSelectedServer(Server? server) { _selectedServer = server; notifyListeners(); } @@ -70,8 +77,13 @@ class ServersProvider with ChangeNotifier { notifyListeners(); } - void setApiClient(ApiClient client) { - _apiClient = client; + // void setApiClient(ApiClient client) { + // _apiClient = client; + // notifyListeners(); + // } + + void setApiClient2(ApiClientV2 client) { + _apiClient2 = client; notifyListeners(); } @@ -141,7 +153,8 @@ class ServersProvider with ChangeNotifier { _serversList = newServers; if (selectedServer != null &&server.id == selectedServer!.id) { - _apiClient = ApiClient(server: server); + // _apiClient = ApiClient(server: server); + _apiClient2 = ApiClientV2(server: server); } notifyListeners(); @@ -156,7 +169,7 @@ class ServersProvider with ChangeNotifier { final result = await removeServerQuery(_dbInstance!, server.id); if (result == true) { _selectedServer = null; - _apiClient = null; + // _apiClient = null; List newServers = _serversList.where((s) => s.id != server.id).toList(); _serversList = newServers; notifyListeners(); @@ -169,14 +182,16 @@ class ServersProvider with ChangeNotifier { void checkServerUpdatesAvailable({ required Server server, + ApiClientV2? apiClient }) async { + final client = apiClient ?? _apiClient2; setUpdateAvailableLoadStatus(LoadStatus.loading, true); - final result = await _apiClient!.checkServerUpdates(); - if (result['result'] == 'success') { - UpdateAvailableData data = UpdateAvailableData.fromJson(result['data']); - final gitHubResult = await _apiClient!.getUpdateChangelog(releaseTag: data.newVersion ?? data.currentVersion); - if (gitHubResult['result'] == 'success') { - data.changelog = gitHubResult['body']; + 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); @@ -186,11 +201,12 @@ class ServersProvider with ChangeNotifier { } } - Future initializateServer(Server server) async { - final serverStatus = await _apiClient!.getServerStatus(); - if (serverStatus['result'] == 'success') { + 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 ); } } @@ -222,8 +238,11 @@ class ServersProvider with ChangeNotifier { if (defaultServer != null) { _selectedServer = defaultServer; - _apiClient = ApiClient(server: defaultServer); - initializateServer(defaultServer); + // final client = ApiClient(server: defaultServer); + final client2 = ApiClientV2(server: defaultServer); + // _apiClient = client; + _apiClient2 = client2; + initializateServer(defaultServer, /*client,*/ client2); } } else { @@ -240,13 +259,13 @@ class ServersProvider with ChangeNotifier { const Duration(seconds: 2), (timer) async { if (_selectedServer != null && _selectedServer == server) { - final result = await _apiClient!.checkServerUpdates(); - if (result['result'] == 'success') { - UpdateAvailableData data = UpdateAvailableData.fromJsonUpdate(result['data']); + final result = await _apiClient2!.checkServerUpdates(); + if (result.successful == true) { + UpdateAvailableData data = UpdateAvailableData.fromJsonUpdate(result.content); if (data.currentVersion == data.newVersion) { - final gitHubResult = await _apiClient!.getUpdateChangelog(releaseTag: data.newVersion ?? data.currentVersion); - if (gitHubResult['result'] == 'success') { - data.changelog = gitHubResult['body']; + 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(); diff --git a/lib/providers/status_provider.dart b/lib/providers/status_provider.dart index 5a4ef73..527749f 100644 --- a/lib/providers/status_provider.dart +++ b/lib/providers/status_provider.dart @@ -2,11 +2,15 @@ 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/compare_versions.dart'; import 'package:adguard_home_manager/functions/time_server_disabled.dart'; class StatusProvider with ChangeNotifier { @@ -104,7 +108,7 @@ class StatusProvider with ChangeNotifier { } } - Future updateBlocking({ + Future updateBlocking({ required String block, required bool newStatus, int? time @@ -114,14 +118,14 @@ class StatusProvider with ChangeNotifier { _protectionsManagementProcess.add('general'); notifyListeners(); - final result = await _serversProvider!.apiClient!.updateGeneralProtection( + final result = await _serversProvider!.apiClient2!.updateGeneralProtection( enable: newStatus, time: time ); _protectionsManagementProcess = _protectionsManagementProcess.where((e) => e != 'general').toList(); - if (result['result'] == 'success') { + if (result.successful == true) { _serverStatus!.generalEnabled = newStatus; if (time != null) { final deadline = generateTimeDeadline(time); @@ -135,111 +139,80 @@ class StatusProvider with ChangeNotifier { stopCountdown(); } notifyListeners(); - return null; + return true; } else { - notifyListeners(); - return result['log']; + return false; } - case 'general_legacy': - _protectionsManagementProcess.add('general'); - notifyListeners(); - - final result = await _serversProvider!.apiClient!.updateGeneralProtectionLegacy(newStatus); - - _protectionsManagementProcess = _protectionsManagementProcess.where((e) => e != 'general').toList(); - - if (result['result'] == 'success') { - _serverStatus!.generalEnabled = newStatus; - notifyListeners(); - return null; - } - else { - notifyListeners(); - return result['log']; - } - - case 'filtering': _protectionsManagementProcess.add('filtering'); notifyListeners(); - final result = await _serversProvider!.apiClient!.updateFiltering( + final result = await _serversProvider!.apiClient2!.updateFiltering( enable: newStatus, ); _protectionsManagementProcess = _protectionsManagementProcess.where((e) => e != 'filtering').toList(); - if (result['result'] == 'success') { + if (result.successful == true) { _serverStatus!.filteringEnabled = newStatus; notifyListeners(); - return null; + return true; } else { - - notifyListeners(); - return result['log']; + return false; } case 'safeSearch': _protectionsManagementProcess.add('safeSearch'); notifyListeners(); - final result = serverVersionIsAhead( - currentVersion: serverStatus!.serverVersion, - referenceVersion: 'v0.107.28', - referenceVersionBeta: 'v0.108.0-b.33' - ) == true - ? await _serversProvider!.apiClient!.updateSafeSearchSettings(body: { 'enabled': newStatus }) - : await _serversProvider!.apiClient!.updateSafeSearchLegacy(newStatus); + final result = await _serversProvider!.apiClient2!.updateSafeSearchSettings(body: { 'enabled': newStatus }); _protectionsManagementProcess = _protectionsManagementProcess.where((e) => e != 'safeSearch').toList(); - if (result['result'] == 'success') { + if (result.successful == true) { _serverStatus!.safeSearchEnabled = newStatus; notifyListeners(); - return null; + return true; } else { - notifyListeners(); - return result['log']; + return false; } case 'safeBrowsing': _protectionsManagementProcess.add('safeBrowsing'); notifyListeners(); - final result = await _serversProvider!.apiClient!.updateSafeBrowsing(newStatus); + final result = await _serversProvider!.apiClient2!.updateSafeBrowsing(enable: newStatus); _protectionsManagementProcess = _protectionsManagementProcess.where((e) => e != 'safeBrowsing').toList(); - if (result['result'] == 'success') { + if (result.successful == true) { _serverStatus!.safeBrowsingEnabled = newStatus; notifyListeners(); - return null; + return true; } else { - notifyListeners(); - return result['log']; + return false; } case 'parentalControl': _protectionsManagementProcess.add('parentalControl'); notifyListeners(); - final result = await _serversProvider!.apiClient!.updateParentalControl(newStatus); + final result = await _serversProvider!.apiClient2!.updateParentalControl(enable: newStatus); _protectionsManagementProcess = _protectionsManagementProcess.where((e) => e != 'parentalControl').toList(); - if (result['result'] == 'success') { + if (result.successful == true) { _serverStatus!.parentalControlEnabled = newStatus; notifyListeners(); - return null; + return true; } else { - notifyListeners(); - return result['log']; + return false; } default: @@ -252,9 +225,9 @@ class StatusProvider with ChangeNotifier { } Future getFilteringRules() async { - final result = await _serversProvider!.apiClient!.getFilteringRules(); - if (result['result'] == 'success') { - _filteringStatus = result['data']; + final result = await _serversProvider!.apiClient2!.getFilteringRules(); + if (result.successful == true) { + _filteringStatus = result.content as FilteringStatus; notifyListeners(); return true; } @@ -264,19 +237,39 @@ class StatusProvider with ChangeNotifier { } Future getServerStatus({ - bool? withLoadingIndicator + bool? withLoadingIndicator = true, + bool? overrideCheckServerVersion }) async { if (withLoadingIndicator == true) { _loadStatus = LoadStatus.loading; } - final result = await _serversProvider!.apiClient!.getServerStatus(); - if (result['result'] == 'success') { + final result = await _serversProvider!.apiClient2!.getServerStatus(); + if (result.successful == true) { + final status = result.content as ServerStatus; setServerStatusData( - data: result['data'] + 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 { @@ -292,12 +285,12 @@ class StatusProvider with ChangeNotifier { }) async { if (_serverStatus == null) return false; - final rules = await _serversProvider!.apiClient!.getFilteringRules(); + final rules = await _serversProvider!.apiClient2!.getFilteringRules(); - if (rules['result'] == 'success') { + if (rules.successful == true) { FilteringStatus oldStatus = _serverStatus!.filteringStatus; - List newRules = rules['data'].userRules.where((d) => !d.contains(domain)).toList(); + List newRules = (rules.content as FilteringStatus).userRules.where((d) => !d.contains(domain)).toList(); if (newStatus == 'block') { newRules.add("||$domain^"); } @@ -308,9 +301,9 @@ class StatusProvider with ChangeNotifier { newObj.userRules = newRules; _filteringStatus = newObj; - final result = await _serversProvider!.apiClient!.postFilteringRules(data: {'rules': newRules}); + final result = await _serversProvider!.apiClient2!.postFilteringRules(data: {'rules': newRules}); - if (result['result'] == 'success') { + if (result.successful == true) { return true; } else { @@ -324,11 +317,11 @@ class StatusProvider with ChangeNotifier { } Future updateSafeSearchConfig(Map status) async { - final result = await _serversProvider!.apiClient!.updateSafeSearchSettings( + final result = await _serversProvider!.apiClient2!.updateSafeSearchSettings( body: status ); - if (result['result'] == 'success') { + if (result.successful == true) { ServerStatus data = serverStatus!; data.safeSearchEnabled = status['enabled'] ?? false; data.safeSeachBing = status['bing'] ?? false; diff --git a/lib/screens/clients/added_list.dart b/lib/screens/clients/added_list.dart index 6b7f33c..aa86785 100644 --- a/lib/screens/clients/added_list.dart +++ b/lib/screens/clients/added_list.dart @@ -1,7 +1,5 @@ // ignore_for_file: use_build_context_synchronously -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:animations/animations.dart'; import 'package:flutter/rendering.dart'; @@ -9,11 +7,10 @@ import 'package:flutter_split_view/flutter_split_view.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:adguard_home_manager/screens/clients/client_screen.dart'; -import 'package:adguard_home_manager/screens/clients/added_client_tile.dart'; -import 'package:adguard_home_manager/screens/clients/remove_client_modal.dart'; +import 'package:adguard_home_manager/screens/clients/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'; @@ -32,13 +29,13 @@ class AddedList extends StatefulWidget { final bool splitView; const AddedList({ - Key? key, + super.key, required this.scrollController, required this.data, required this.onClientSelected, this.selectedClient, required this.splitView - }) : super(key: key); + }); @override State createState() => _AddedListState(); @@ -77,8 +74,8 @@ class _AddedListState extends State { 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 clientsProvider.editClient(client); @@ -101,7 +98,7 @@ 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 clientsProvider.deleteClient(client); @@ -128,31 +125,13 @@ class _AddedListState extends State { } void openClientModal(Client client) { - if (width > 900 || !(Platform.isAndroid | Platform.isIOS)) { - showDialog( - barrierDismissible: false, - context: context, - builder: (BuildContext context) => ClientScreen( - onConfirm: confirmEditClient, - serverVersion: statusProvider.serverStatus!.serverVersion, - onDelete: deleteClient, - client: client, - dialog: true, - ) - ); - } - else { - Navigator.push(context, MaterialPageRoute( - fullscreenDialog: true, - builder: (BuildContext context) => ClientScreen( - onConfirm: confirmEditClient, - serverVersion: statusProvider.serverStatus!.serverVersion, - onDelete: deleteClient, - client: client, - dialog: false, - ) - )); - } + openClientFormModal( + context: context, + width: width, + client: client, + onConfirm: confirmEditClient, + onDelete: deleteClient + ); } void openDeleteModal(Client client) { @@ -163,19 +142,13 @@ class _AddedListState extends State { ) ); } - - void openOptionsModal(Client client) { - showModal( - context: context, - builder: (ctx) => OptionsModal( - onDelete: () => openDeleteModal(client), - onEdit: () => openClientModal(client), - ) - ); - } + 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; return CustomTabContentList( - noSliver: !(Platform.isAndroid || Platform.isIOS), listPadding: widget.splitView == true ? const EdgeInsets.only(top: 8) : null, @@ -198,15 +171,16 @@ class _AddedListState extends State { ], ), ), - itemsCount: widget.data.length, + itemsCount: clientsDisplay.length, contentWidget: (index) => AddedClientTile( selectedClient: widget.selectedClient, - client: widget.data[index], + client: clientsDisplay[index], onTap: widget.onClientSelected, - onLongPress: openOptionsModal, - onEdit: openClientModal, + onEdit: statusProvider.serverStatus != null + ? (c) => openClientModal(c) + : null, + onDelete: openDeleteModal, splitView: widget.splitView, - serverVersion: statusProvider.serverStatus!.serverVersion, ), noData: SizedBox( width: double.maxFinite, diff --git a/lib/screens/clients/active_client_tile.dart b/lib/screens/clients/client/active_client_tile.dart similarity index 71% rename from lib/screens/clients/active_client_tile.dart rename to lib/screens/clients/client/active_client_tile.dart index 26cb30f..7a7a825 100644 --- a/lib/screens/clients/active_client_tile.dart +++ b/lib/screens/clients/client/active_client_tile.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:contextmenu/contextmenu.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/options_modal.dart'; import 'package:adguard_home_manager/models/menu_option.dart'; import 'package:adguard_home_manager/functions/copy_clipboard.dart'; @@ -16,54 +15,29 @@ class ActiveClientTile extends StatelessWidget { final AutoClient? selectedClient; const ActiveClientTile({ - Key? key, + super.key, required this.client, required this.onTap, required this.splitView, this.selectedClient - }) : super(key: key); + }); @override Widget build(BuildContext context) { - void openOptionsModal() { - showDialog( - context: context, - builder: (context) => OptionsModal( - options: [ - MenuOption( - title: AppLocalizations.of(context)!.copyClipboard, - icon: Icons.copy_rounded, - action: () { - copyToClipboard( - value: client.name != '' - ? client.name! - : client.ip, - successMessage: AppLocalizations.of(context)!.copiedClipboard, - ); - }, - ) - ] - ), - ); - } - if (splitView == true) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 12), - child: ContextMenuArea( - builder: (context) => [ - CustomListTile( - title: AppLocalizations.of(context)!.copyClipboard, + child: OptionsMenu( + options: (_) => [ + MenuOption( icon: Icons.copy_rounded, - onTap: () { - copyToClipboard( - value: client.name != '' - ? client.name! - : client.ip, - successMessage: AppLocalizations.of(context)!.copiedClipboard, - ); - Navigator.pop(context); - }, + title: AppLocalizations.of(context)!.copyClipboard, + action: () => copyToClipboard( + value: client.name != '' + ? client.name! + : client.ip, + successMessage: AppLocalizations.of(context)!.copiedClipboard, + ) ) ], child: Material( @@ -72,10 +46,6 @@ class ActiveClientTile extends StatelessWidget { child: InkWell( borderRadius: BorderRadius.circular(28), onTap: () => onTap(client), - onLongPress: () { - Navigator.pop(context); - openOptionsModal(); - }, child: Container( width: double.maxFinite, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), @@ -128,20 +98,17 @@ class ActiveClientTile extends StatelessWidget { ); } else { - return ContextMenuArea( - builder: (context) => [ - CustomListTile( - title: AppLocalizations.of(context)!.copyClipboard, + return OptionsMenu( + options: (_) => [ + MenuOption( icon: Icons.copy_rounded, - onTap: () { - copyToClipboard( - value: client.name != '' - ? client.name! - : client.ip, - successMessage: AppLocalizations.of(context)!.copiedClipboard, - ); - Navigator.pop(context); - }, + title: AppLocalizations.of(context)!.copyClipboard, + action: () => copyToClipboard( + value: client.name != '' + ? client.name! + : client.ip, + successMessage: AppLocalizations.of(context)!.copiedClipboard, + ) ) ], child: CustomListTile( @@ -158,7 +125,6 @@ class ActiveClientTile extends StatelessWidget { ), ), onTap: () => onTap(client), - onLongPress: openOptionsModal, ), ); } diff --git a/lib/screens/clients/added_client_tile.dart b/lib/screens/clients/client/added_client_tile.dart similarity index 59% rename from lib/screens/clients/added_client_tile.dart rename to lib/screens/clients/client/added_client_tile.dart index c63c816..8c10fd7 100644 --- a/lib/screens/clients/added_client_tile.dart +++ b/lib/screens/clients/client/added_client_tile.dart @@ -1,81 +1,75 @@ 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/widgets/custom_list_tile.dart'; -import 'package:adguard_home_manager/functions/compare_versions.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 StatelessWidget { +class AddedClientTile extends StatefulWidget { final Client client; final void Function(Client) onTap; - final void Function(Client) onLongPress; - final void Function(Client) onEdit; + final void Function(Client)? onEdit; + final void Function(Client) onDelete; final Client? selectedClient; final bool? splitView; - final String serverVersion; const AddedClientTile({ - Key? key, + super.key, required this.client, required this.onTap, - required this.onLongPress, - required this.onEdit, + this.onEdit, + required this.onDelete, this.selectedClient, required this.splitView, - required this.serverVersion - }) : super(key: key); + }); + + @override + State createState() => _AddedClientTileState(); +} + +class _AddedClientTileState extends State { + bool _isHover = false; @override Widget build(BuildContext context) { final appConfigProvider = Provider.of(context); - if (splitView == true) { + if (widget.splitView == true) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: Material( color: Colors.transparent, borderRadius: BorderRadius.circular(28), - child: ContextMenuArea( - builder: (context) => [ - CustomListTile( - title: AppLocalizations.of(context)!.seeDetails, - icon: Icons.file_open_rounded, - onTap: () { - Navigator.pop(context); - onEdit(client); - } - ), - CustomListTile( - title: AppLocalizations.of(context)!.copyClipboard, + child: OptionsMenu( + options: (_) => [ + MenuOption( icon: Icons.copy_rounded, - onTap: () { - copyToClipboard( - value: client.ids.toString().replaceAll(RegExp(r'^\[|\]$'), ''), - successMessage: AppLocalizations.of(context)!.copiedClipboard, - ); - Navigator.pop(context); - } + 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: () => onTap(client), - onLongPress: () => onLongPress(client), + 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: client == selectedClient + color: widget.client == widget.selectedClient ? Theme.of(context).colorScheme.primaryContainer : null ), - child: Row( + child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( @@ -87,7 +81,7 @@ class AddedClientTile extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - client.ids.toString().replaceAll(RegExp(r'^\[|\]$'), ''), + widget.client.ids.toString().replaceAll(RegExp(r'^\[|\]$'), ''), style: TextStyle( fontSize: 16, fontWeight: FontWeight.w400, @@ -100,7 +94,7 @@ class AddedClientTile extends StatelessWidget { Icon( Icons.filter_list_rounded, size: 19, - color: client.filteringEnabled == true + color: widget.client.filteringEnabled == true ? appConfigProvider.useThemeColorForStatus == true ? Theme.of(context).colorScheme.primary : Colors.green @@ -112,7 +106,7 @@ class AddedClientTile extends StatelessWidget { Icon( Icons.vpn_lock_rounded, size: 18, - color: client.safebrowsingEnabled == true + color: widget.client.safebrowsingEnabled == true ? appConfigProvider.useThemeColorForStatus == true ? Theme.of(context).colorScheme.primary : Colors.green @@ -124,7 +118,7 @@ class AddedClientTile extends StatelessWidget { Icon( Icons.block, size: 18, - color: client.parentalEnabled == true + color: widget.client.parentalEnabled == true ? appConfigProvider.useThemeColorForStatus == true ? Theme.of(context).colorScheme.primary : Colors.green @@ -136,25 +130,13 @@ class AddedClientTile extends StatelessWidget { Icon( Icons.search_rounded, size: 19, - color: serverVersionIsAhead( - currentVersion: serverVersion, - referenceVersion: 'v0.107.28', - referenceVersionBeta: 'v0.108.0-b.33' - ) == true - ? client.safeSearch != null && client.safeSearch!.enabled == true - ? appConfigProvider.useThemeColorForStatus == true - ? Theme.of(context).colorScheme.primary - : Colors.green - : appConfigProvider.useThemeColorForStatus == true - ? Colors.grey - : Colors.red - : client.safesearchEnabled == true - ? appConfigProvider.useThemeColorForStatus == true - ? Theme.of(context).colorScheme.primary - : Colors.green - : appConfigProvider.useThemeColorForStatus == true - ? Colors.grey - : Colors.red, + 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 ) ], ) @@ -164,6 +146,14 @@ class AddedClientTile extends StatelessWidget { ], ), ), + 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, + ) + ] ], ) ), @@ -173,37 +163,30 @@ class AddedClientTile extends StatelessWidget { ); } else { - return ContextMenuArea( - builder: (context) => [ - CustomListTile( + return OptionsMenu( + options: (_) => [ + if (widget.onEdit != null) MenuOption( title: AppLocalizations.of(context)!.seeDetails, icon: Icons.file_open_rounded, - onTap: () { - Navigator.pop(context); - onEdit(client); - } + action: () => widget.onEdit!(widget.client) ), - CustomListTile( - title: AppLocalizations.of(context)!.copyClipboard, + MenuOption( icon: Icons.copy_rounded, - onTap: () { - copyToClipboard( - value: client.ids.toString().replaceAll(RegExp(r'^\[|\]$'), ''), - successMessage: AppLocalizations.of(context)!.copiedClipboard, - ); - Navigator.pop(context); - } + title: AppLocalizations.of(context)!.copyClipboard, + action: () => copyToClipboard( + value: widget.client.ids.toString().replaceAll(RegExp(r'^\[|\]$'), ''), + successMessage: AppLocalizations.of(context)!.copiedClipboard, + ) ), ], child: CustomListTile( - onLongPress: () => onLongPress(client), - onTap: () => onTap(client), - title: client.name, + onTap: () => widget.onTap(widget.client), + title: widget.client.name, subtitleWidget: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - client.ids.toString().replaceAll(RegExp(r'^\[|\]$'), ''), + widget.client.ids.toString().replaceAll(RegExp(r'^\[|\]$'), ''), style: TextStyle( color: Theme.of(context).listTileTheme.textColor ), @@ -214,7 +197,7 @@ class AddedClientTile extends StatelessWidget { Icon( Icons.filter_list_rounded, size: 19, - color: client.filteringEnabled == true + color: widget.client.filteringEnabled == true ? appConfigProvider.useThemeColorForStatus == true ? Theme.of(context).colorScheme.primary : Colors.green @@ -226,7 +209,7 @@ class AddedClientTile extends StatelessWidget { Icon( Icons.vpn_lock_rounded, size: 18, - color: client.safebrowsingEnabled == true + color: widget.client.safebrowsingEnabled == true ? appConfigProvider.useThemeColorForStatus == true ? Theme.of(context).colorScheme.primary : Colors.green @@ -238,7 +221,7 @@ class AddedClientTile extends StatelessWidget { Icon( Icons.block, size: 18, - color: client.parentalEnabled == true + color: widget.client.parentalEnabled == true ? appConfigProvider.useThemeColorForStatus == true ? Theme.of(context).colorScheme.primary : Colors.green @@ -250,25 +233,13 @@ class AddedClientTile extends StatelessWidget { Icon( Icons.search_rounded, size: 19, - color: serverVersionIsAhead( - currentVersion: serverVersion, - referenceVersion: 'v0.107.28', - referenceVersionBeta: 'v0.108.0-b.33' - ) == true - ? client.safeSearch != null && client.safeSearch!.enabled == true - ? appConfigProvider.useThemeColorForStatus == true - ? Theme.of(context).colorScheme.primary - : Colors.green - : appConfigProvider.useThemeColorForStatus == true - ? Colors.grey - : Colors.red - : client.safesearchEnabled == true - ? appConfigProvider.useThemeColorForStatus == true - ? Theme.of(context).colorScheme.primary - : Colors.green - : appConfigProvider.useThemeColorForStatus == true - ? Colors.grey - : Colors.red, + 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 ) ], ) 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/client_form.dart b/lib/screens/clients/client/client_form.dart new file mode 100644 index 0000000..e127466 --- /dev/null +++ b/lib/screens/clients/client/client_form.dart @@ -0,0 +1,288 @@ +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/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; + + 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, + }); + + @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, + ), + ), + ], + ); + } +} \ 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..cf4f485 --- /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({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return 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..f41de69 --- /dev/null +++ b/lib/screens/clients/client/client_screen.dart @@ -0,0 +1,421 @@ +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 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; + + const ClientScreen({ + super.key, + this.client, + required this.onConfirm, + this.onDelete, + required this.fullScreen + }); + + @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; + + // 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() + : ""; + } + 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 + ); + 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 Dialog.fullscreen( + child: 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: actions(), + ), + body: SafeArea( + child: ListView( + controller: _scrollController, + 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) + ), + ], + ), + ), + ), + ); + } + 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) + ), + ], + ), + ) + ], + ), + ), + ); + } + } +} + +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..f0338a1 --- /dev/null +++ b/lib/screens/clients/client/client_screen_functions.dart @@ -0,0 +1,116 @@ +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, +}) { + 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, + ), + ); +} + +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..3267128 --- /dev/null +++ b/lib/screens/clients/client/logs_list_client.dart @@ -0,0 +1,276 @@ +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 Scaffold( + appBar: AppBar( + title: Text(widget.name != null && widget.name != '' ? widget.name! : widget.ip), + centerTitle: true, + 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( + child: Builder( + builder: (context) { + switch (loadStatus) { + case 0: + return SizedBox( + width: double.maxFinite, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 30), + Text( + AppLocalizations.of(context)!.loadingLogs, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ); + + case 1: + if (logsData!.data.isNotEmpty) { + return RefreshIndicator( + onRefresh: fetchLogs, + child: ListView.builder( + controller: scrollController, + padding: const EdgeInsets.only(top: 0), + itemCount: isLoadingMore == true + ? logsData!.data.length+1 + : logsData!.data.length, + itemBuilder: (context, index) { + if (isLoadingMore == true && index == logsData!.data.length) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 20), + child: Center( + child: CircularProgressIndicator(), + ), + ); + } + else { + return LogTile( + log: logsData!.data[index], + index: index, + length: logsData!.data.length, + useAlwaysNormalTile: true, + onLogTap: (log) => { + if (width > 700) { + showDialog( + context: context, + builder: (context) => LogDetailsScreen( + log: log, + dialog: true + ) + ) + } + else { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => LogDetailsScreen( + log: log, + dialog: false + ) + ) + ) + } + }, + twoColumns: widget.splitView, + ); + } + } + ), + ); + } + else { + return Center( + child: Text( + AppLocalizations.of(context)!.noLogsDisplay, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + 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, + 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/clients/remove_client_modal.dart b/lib/screens/clients/client/remove_client_modal.dart similarity index 100% rename from lib/screens/clients/remove_client_modal.dart rename to lib/screens/clients/client/remove_client_modal.dart diff --git a/lib/screens/clients/safe_search_modal.dart b/lib/screens/clients/client/safe_search_modal.dart similarity index 100% rename from lib/screens/clients/safe_search_modal.dart rename to lib/screens/clients/client/safe_search_modal.dart diff --git a/lib/screens/clients/services_modal.dart b/lib/screens/clients/client/services_modal.dart similarity index 100% rename from lib/screens/clients/services_modal.dart rename to lib/screens/clients/client/services_modal.dart 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 91% rename from lib/screens/clients/tags_modal.dart rename to lib/screens/clients/client/tags_modal.dart index 42843b1..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(); @@ -82,12 +82,14 @@ class _TagsModalState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - widget.tags[index], - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurface, + Flexible( + child: Text( + widget.tags[index], + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface, + ), ), ), Checkbox( 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 c40a6d4..0000000 --- a/lib/screens/clients/client_screen.dart +++ /dev/null @@ -1,838 +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/safe_search_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/widgets/custom_list_tile.dart'; - -import 'package:adguard_home_manager/functions/compare_versions.dart'; -import 'package:adguard_home_manager/models/safe_search.dart'; -import 'package:adguard_home_manager/providers/clients_provider.dart'; -import 'package:adguard_home_manager/providers/status_provider.dart'; -import 'package:adguard_home_manager/models/clients.dart'; - -class ClientScreen extends StatefulWidget { - final Client? client; - final String serverVersion; - final void Function(Client) onConfirm; - final void Function(Client)? onDelete; - final bool dialog; - - const ClientScreen({ - Key? key, - this.client, - required this.serverVersion, - required this.onConfirm, - this.onDelete, - required this.dialog - }) : super(key: key); - - @override - State createState() => _ClientScreenState(); -} - -class _ClientScreenState extends State { - final Uuid uuid = const Uuid(); - bool editMode = true; - - bool validValues = false; - - TextEditingController nameController = TextEditingController(); - - List selectedTags = []; - - List> identifiersControllers = [ - { - '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 = []; - - - void checkValidValues() { - if ( - nameController.text != '' && - identifiersControllers.isNotEmpty && - identifiersControllers[0]['controller'].text != '' - ) { - setState(() => validValues = true); - } - else { - setState(() => validValues = false); - } - } - - bool version = false; - - @override - void initState() { - version = serverVersionIsAhead( - currentVersion: widget.serverVersion, - referenceVersion: 'v0.107.28', - referenceVersionBeta: 'v0.108.0-b.33' - ); - - if (widget.client != null) { - editMode = false; - - validValues = true; - - 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; - if (version == true) { - safeSearch = widget.client!.safeSearch; - } - else { - enableSafeSearch = widget.client!.safesearchEnabled ?? false; - } - 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 clientsProvider = Provider.of(context); - final statusProvider = 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: version == false ? enableSafeSearch : null, - safeSearch: version == true ? safeSearch : null, - 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; - safeSearch = defaultSafeSearch; - }); - } - else if (useGlobalSettingsFiltering == false) { - setState(() { - useGlobalSettingsFiltering = true; - - enableFiltering = null; - enableSafeBrowsing = null; - enableParentalControl = null; - enableSafeSearch = null; - safeSearch = null; - }); - } - } - - void openTagsModal() { - showDialog( - context: context, - builder: (context) => TagsModal( - selectedTags: selectedTags, - tags: clientsProvider.clients!.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!); - } - ) - ); - } - - void openSafeSearchModal() { - showDialog( - context: context, - builder: (context) => SafeSearchModal( - safeSearch: safeSearch ?? defaultSafeSearch, - disabled: !editMode, - onConfirm: (s) => setState(() => safeSearch = s) - ) - ); - } - - 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, - ) - : Padding( - padding: const EdgeInsets.symmetric( - vertical: 14, - horizontal: 12 - ), - child: Text( - "Global", - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ) - ], - ), - ), - ), - ); - } - - Widget content(bool withPaddingTop) { - return ListView( - padding: const EdgeInsets.only(top: 0), - children: [ - if (withPaddingTop == true) const SizedBox(height: 24), - if (withPaddingTop == false) const SizedBox(height: 6), - 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, - ) - ], - ), - ), - ), - ), - ), - 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 - ), - if ( - serverVersionIsAhead( - currentVersion: statusProvider.serverStatus!.serverVersion, - referenceVersion: 'v0.107.28', - referenceVersionBeta: 'v0.108.0-b.33' - ) == true - ) CustomListTile( - title: AppLocalizations.of(context)!.safeSearch, - padding: const EdgeInsets.symmetric( - horizontal: 42, - 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() - : null, - ), - if ( - serverVersionIsAhead( - currentVersion: statusProvider.serverStatus!.serverVersion, - referenceVersion: 'v0.107.28', - referenceVersionBeta: 'v0.108.0-b.33' - ) == false - ) 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, - ) - ], - ), - ), - ), - ), - ), - 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: [ - Expanded( - 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) - ], - ); - } - - if (widget.dialog == true) { - return Dialog( - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 500 - ), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - IconButton( - onPressed: () => Navigator.pop(context), - icon: const Icon(Icons.clear_rounded) - ), - const SizedBox(width: 8), - Text( - widget.client != null - ? AppLocalizations.of(context)!.client - : AppLocalizations.of(context)!.addClient, - style: const TextStyle( - fontSize: 22 - ), - ), - ], - ), - Row( - children: [ - if (widget.client == null || (widget.client != null && editMode == true)) IconButton( - onPressed: validValues == true - ? () { - createClient(); - Navigator.pop(context); - } - : null, - icon: Icon( - widget.client != null && editMode == true - ? Icons.save_rounded - : Icons.check_rounded - ), - tooltip: widget.client != null && editMode == true - ? AppLocalizations.of(context)!.save - : AppLocalizations.of(context)!.confirm, - ), - if (widget.client != null && editMode == false) IconButton( - onPressed: () => setState(() => editMode = true), - icon: const Icon(Icons.edit_rounded), - tooltip: AppLocalizations.of(context)!.edit, - ), - if (widget.client != null) IconButton( - onPressed: openDeleteClientScreen, - icon: const Icon(Icons.delete_rounded), - tooltip: AppLocalizations.of(context)!.delete, - ), - const SizedBox(width: 10), - ], - ) - ], - ), - ), - Flexible( - child: content(false) - ) - ], - ), - ), - ); - } - else { - return Scaffold( - appBar: AppBar( - leading: IconButton( - onPressed: () => Navigator.pop(context), - icon: const Icon(Icons.close) - ), - title: Text( - widget.client != null - ? AppLocalizations.of(context)!.client - : AppLocalizations.of(context)!.addClient - ), - actions: [ - if (widget.client == null || (widget.client != null && editMode == true)) IconButton( - onPressed: validValues == true - ? () { - createClient(); - Navigator.pop(context); - } - : null, - icon: Icon( - widget.client != null && editMode == true - ? Icons.save_rounded - : Icons.check_rounded - ), - tooltip: widget.client != null && editMode == true - ? AppLocalizations.of(context)!.save - : AppLocalizations.of(context)!.confirm, - ), - if (widget.client != null && editMode == false) IconButton( - onPressed: () => setState(() => editMode = true), - icon: const Icon(Icons.edit_rounded), - tooltip: AppLocalizations.of(context)!.edit, - ), - if (widget.client != null) IconButton( - onPressed: openDeleteClientScreen, - icon: const Icon(Icons.delete_rounded), - tooltip: AppLocalizations.of(context)!.delete, - ), - const SizedBox(width: 10), - ], - ), - body: content(true) - ); - } - } -} \ No newline at end of file diff --git a/lib/screens/clients/clients.dart b/lib/screens/clients/clients.dart index 1dada10..3d494ae 100644 --- a/lib/screens/clients/clients.dart +++ b/lib/screens/clients/clients.dart @@ -1,256 +1,57 @@ -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:flutter_split_view/flutter_split_view.dart'; -import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:adguard_home_manager/screens/clients/clients_list.dart'; -import 'package:adguard_home_manager/screens/clients/search_clients.dart'; -import 'package:adguard_home_manager/screens/clients/logs_list_client.dart'; -import 'package:adguard_home_manager/screens/clients/clients_desktop_view.dart'; -import 'package:adguard_home_manager/screens/clients/added_list.dart'; +import 'package:adguard_home_manager/screens/clients/clients_lists.dart'; -import 'package:adguard_home_manager/providers/clients_provider.dart'; -import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/constants/enums.dart'; import 'package:adguard_home_manager/models/clients.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; class Clients extends StatefulWidget { - const Clients({Key? key}) : super(key: key); + const Clients({super.key}); @override State createState() => _ClientsState(); } class _ClientsState extends State with TickerProviderStateMixin { - late TabController tabController; - final ScrollController scrollController = ScrollController(); - - bool searchMode = false; - final TextEditingController searchController = TextEditingController(); - - @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) - ); - } - 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); - final clientsProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - - final width = MediaQuery.of(context).size.width; - - PreferredSizeWidget tabBar() { - return TabBar( - controller: tabController, - unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant, - tabs: [ - Tab( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.devices), - const SizedBox(width: 8), - Text(AppLocalizations.of(context)!.activeClients) - ], - ), - ), - Tab( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.add_rounded), - const SizedBox(width: 8), - Text(AppLocalizations.of(context)!.added) - ], - ), - ), - ] - ); - } - - Widget tabBarView(bool sliver) { - return TabBarView( - controller: tabController, - children: [ - ClientsList( - scrollController: scrollController, - data: clientsProvider.loadStatus == LoadStatus.loaded - ? clientsProvider.filteredActiveClients : [], - onClientSelected: (client) => Navigator.push(context, MaterialPageRoute( - builder: (context) => LogsListClient( - ip: client.ip, - serversProvider: serversProvider, - appConfigProvider: appConfigProvider - ) - )), - splitView: false, - sliver: sliver, - ), - AddedList( - scrollController: scrollController, - data: clientsProvider.loadStatus == LoadStatus.loaded - ? clientsProvider.filteredAddedClients : [], - onClientSelected: (client) => Navigator.push(context, MaterialPageRoute( - builder: (context) => LogsListClient( - ip: client.ids[0], - serversProvider: serversProvider, - appConfigProvider: appConfigProvider - ) - )), - splitView: false, - ), - ] - ); - } - - if (width > 900) { - return SplitView.material( - hideDivider: true, - flexWidth: const FlexWidth(mainViewFlexWidth: 1, secondaryViewFlexWidth: 2), - placeholder: Center( - child: Padding( - padding: const EdgeInsets.all(24), - child: Text( - AppLocalizations.of(context)!.selectClientLeftColumn, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSurfaceVariant + return Scaffold( + body: LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth > 1000) { + return SplitView.material( + hideDivider: true, + flexWidth: const FlexWidth(mainViewFlexWidth: 1, secondaryViewFlexWidth: 2), + placeholder: Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Text( + AppLocalizations.of(context)!.selectClientLeftColumn, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ), + ), ), - ), - ), - ), - child: ClientsDesktopView( - serversProvider: serversProvider, - appConfigProvider: appConfigProvider, - ) - ); - } - else { - if (!(Platform.isAndroid || Platform.isIOS)) { - return DefaultTabController( - length: 2, - child: Scaffold( - appBar: AppBar( - title: Text(AppLocalizations.of(context)!.clients), - centerTitle: false, - actions: [ - if (clientsProvider.loadStatus == LoadStatus.loaded) ...[ - 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() - ), - body: tabBarView(false), - ), - ); - } - else { - return DefaultTabController( - length: 2, - child: NestedScrollView( - controller: scrollController, - headerSliverBuilder: ((context, innerBoxIsScrolled) { - return [ - SliverOverlapAbsorber( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - sliver: SliverAppBar( - title: searchMode == true - ? Row( - children: [ - IconButton( - onPressed: () { - setState(() { - searchMode = false; - searchController.text = ""; - clientsProvider.setSearchTermClients(null); - }); - }, - icon: const Icon(Icons.arrow_back_rounded) - ), - 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 - ), - ), - ) - ], - ) - : Text(AppLocalizations.of(context)!.clients), - pinned: true, - floating: true, - centerTitle: false, - forceElevated: innerBoxIsScrolled, - 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() - ), - ) - ]; - }), - body: tabBarView(true) - ) - ); - } - } + child: const ClientsLists( + splitView: true, + ) + ); + } + else { + return const ClientsLists( + splitView: false, + ); + } + }, + ), + ); } } \ No newline at end of file diff --git a/lib/screens/clients/clients_desktop_view.dart b/lib/screens/clients/clients_desktop_view.dart deleted file mode 100644 index d6707ad..0000000 --- a/lib/screens/clients/clients_desktop_view.dart +++ /dev/null @@ -1,244 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:flutter_split_view/flutter_split_view.dart'; -import 'package:provider/provider.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -import 'package:adguard_home_manager/screens/clients/logs_list_client.dart'; -import 'package:adguard_home_manager/screens/clients/added_list.dart'; -import 'package:adguard_home_manager/screens/clients/clients_list.dart'; - -import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/providers/clients_provider.dart'; -import 'package:adguard_home_manager/constants/enums.dart'; -import 'package:adguard_home_manager/models/clients.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; - - -class ClientsDesktopView extends StatefulWidget { - final ServersProvider serversProvider; - final AppConfigProvider appConfigProvider; - - const ClientsDesktopView({ - Key? key, - required this.serversProvider, - required this.appConfigProvider, - }) : super(key: key); - - @override - State createState() => _ClientsDesktopViewState(); -} - -class _ClientsDesktopViewState extends State with TickerProviderStateMixin { - late TabController tabController; - final ScrollController scrollController = ScrollController(); - - AutoClient? selectedActiveClient; - Client? selectedAddedClient; - - bool searchMode = false; - final TextEditingController searchController = TextEditingController(); - - @override - void initState() { - super.initState(); - tabController = TabController( - initialIndex: 0, - length: 2, - vsync: this, - ); - tabController.addListener(() => widget.appConfigProvider.setSelectedClientsTab(tabController.index)); - } - - @override - Widget build(BuildContext context) { - final serversProvider = Provider.of(context); - final clientsProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - - PreferredSizeWidget tabBar() { - return TabBar( - controller: tabController, - unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant, - tabs: [ - Tab( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.devices), - const SizedBox(width: 8), - Text(AppLocalizations.of(context)!.activeClients) - ], - ), - ), - Tab( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.add_rounded), - const SizedBox(width: 8), - Text(AppLocalizations.of(context)!.added) - ], - ), - ), - ] - ); - } - - Widget tabBarView(bool sliver) { - return TabBarView( - controller: tabController, - children: [ - ClientsList( - scrollController: scrollController, - data: clientsProvider.loadStatus == LoadStatus.loaded - ? clientsProvider.filteredActiveClients : [], - onClientSelected: (client) => setState(() { - selectedAddedClient = null; - selectedActiveClient = client; - SplitView.of(context).setSecondary( - LogsListClient( - ip: client.ip, - name: client.name, - serversProvider: serversProvider, - appConfigProvider: appConfigProvider, - ) - ); - }), - selectedClient: selectedActiveClient, - splitView: true, - sliver: sliver, - ), - AddedList( - scrollController: scrollController, - data: clientsProvider.loadStatus == LoadStatus.loaded - ? clientsProvider.filteredAddedClients : [], - onClientSelected: (client) => setState(() { - selectedActiveClient = null; - selectedAddedClient = client; - SplitView.of(context).setSecondary( - LogsListClient( - ip: client.ids[0], - name: client.name, - serversProvider: serversProvider, - appConfigProvider: appConfigProvider, - ) - ); - }), - selectedClient: selectedAddedClient, - splitView: true, - ), - ] - ); - } - - Widget title() { - if (searchMode == true) { - return Row( - children: [ - IconButton( - onPressed: () { - setState(() { - searchMode = false; - searchController.text = ""; - clientsProvider.setSearchTermClients(null); - }); - }, - icon: const Icon(Icons.arrow_back_rounded) - ), - 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 - ), - ), - ) - ], - ); - } - else { - return Text(AppLocalizations.of(context)!.clients); - } - } - - if (!(Platform.isAndroid || Platform.isIOS)) { - return DefaultTabController( - length: 2, - child: Scaffold( - appBar: AppBar( - title: title(), - centerTitle: false, - actions: [ - if (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() - ), - body: tabBarView(false), - ), - ); - } - else { - return DefaultTabController( - length: 2, - child: NestedScrollView( - controller: scrollController, - headerSliverBuilder: ((context, innerBoxIsScrolled) { - return [ - SliverOverlapAbsorber( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - sliver: SliverAppBar( - title: title(), - pinned: true, - floating: true, - centerTitle: false, - forceElevated: innerBoxIsScrolled, - actions: [ - if (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() - ), - ) - ]; - }), - body: tabBarView(true) - ) - ); - } - } -} \ No newline at end of file diff --git a/lib/screens/clients/clients_list.dart b/lib/screens/clients/clients_list.dart index 4ecd98c..bdff4ee 100644 --- a/lib/screens/clients/clients_list.dart +++ b/lib/screens/clients/clients_list.dart @@ -2,7 +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/screens/clients/active_client_tile.dart'; +import 'package:adguard_home_manager/screens/clients/client/active_client_tile.dart'; import 'package:adguard_home_manager/widgets/tab_content_list.dart'; @@ -10,22 +10,18 @@ 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 List data; final void Function(AutoClient) onClientSelected; final AutoClient? selectedClient; final bool splitView; - final bool sliver; const ClientsList({ - Key? key, - required this.scrollController, + super.key, required this.data, required this.onClientSelected, this.selectedClient, required this.splitView, - required this.sliver - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -35,7 +31,6 @@ class ClientsList extends StatelessWidget { listPadding: splitView == true ? const EdgeInsets.only(top: 8) : null, - noSliver: !sliver, loadingGenerator: () => SizedBox( width: double.maxFinite, height: MediaQuery.of(context).size.height-171, diff --git a/lib/screens/clients/clients_lists.dart b/lib/screens/clients/clients_lists.dart new file mode 100644 index 0000000..bdc56b8 --- /dev/null +++ b/lib/screens/clients/clients_lists.dart @@ -0,0 +1,226 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_split_view/flutter_split_view.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/screens/clients/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) { + SplitView.of(context).push(w); + } + 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) { + SplitView.of(context).push(w); + } + 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 8d73050..64fd098 100644 --- a/lib/screens/clients/fab.dart +++ b/lib/screens/clients/fab.dart @@ -1,18 +1,16 @@ // 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/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/classes/process_modal.dart'; -import 'package:adguard_home_manager/providers/status_provider.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; class ClientsFab extends StatelessWidget { @@ -20,14 +18,14 @@ class ClientsFab extends StatelessWidget { @override Widget build(BuildContext context) { - final appConfigProvider = 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); + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.addingClient); final result = await clientsProvider.addClient(client); @@ -51,32 +49,21 @@ class ClientsFab extends StatelessWidget { } void openAddClient() { - if (width > 900 || !(Platform.isAndroid | Platform.isIOS)) { - showDialog( - barrierDismissible: false, - context: context, - builder: (BuildContext context) => ClientScreen( - onConfirm: confirmAddClient, - serverVersion: statusProvider.serverStatus!.serverVersion, - dialog: true, - ) - ); - } - else { - Navigator.push(context, MaterialPageRoute( - fullscreenDialog: true, - builder: (BuildContext context) => ClientScreen( - onConfirm: confirmAddClient, - serverVersion: statusProvider.serverStatus!.serverVersion, - dialog: false, - ) - )); - } + 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/logs_list_client.dart b/lib/screens/clients/logs_list_client.dart deleted file mode 100644 index 0ddc99a..0000000 --- a/lib/screens/clients/logs_list_client.dart +++ /dev/null @@ -1,268 +0,0 @@ -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/log_details_screen.dart'; - -import 'package:adguard_home_manager/models/logs.dart'; -import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; - -class LogsListClient extends StatefulWidget { - final String ip; - final String? name; - final ServersProvider serversProvider; - final AppConfigProvider appConfigProvider; - - const LogsListClient({ - Key? key, - required this.ip, - this.name, - required this.serversProvider, - required this.appConfigProvider - }) : super(key: key); - - @override - State 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.apiClient!.getLogs( - count: logsQuantity, - offset: offst, - search: '"${widget.ip}"' - ) - ); - - final result = await cancelableRequest?.value; - - if (result != null) { - if (loadingMore != null && loadingMore == true && mounted) { - setState(() => isLoadingMore = false); - } - - if (mounted) { - if (result['result'] == 'success') { - setState(() => offset = inOffset != null ? inOffset+logsQuantity : offset+logsQuantity); - if (loadingMore != null && loadingMore == true && logsData != null) { - LogsData newLogsData = result['data']; - newLogsData.data = [...logsData!.data, ...result['data'].data]; - setState(() => logsData = newLogsData); - } - else { - LogsData newLogsData = result['data']; - setState(() => logsData = newLogsData); - } - setState(() => loadStatus = 1); - } - else { - setState(() => loadStatus = 2); - widget.appConfigProvider.addLog(result['log']); - } - } - } - } - - - void scrollListener() { - if (scrollController.position.extentAfter < 500 && isLoadingMore == false) { - fetchLogs(loadingMore: true); - } - if (scrollController.position.pixels > 0) { - setState(() => showDivider = false); - } - else { - setState(() => showDivider = true); - } - } - - @override - void initState() { - scrollController = ScrollController()..addListener(scrollListener); - fetchLogs(inOffset: 0); - setState(() => previousIp = widget.ip); - super.initState(); - } - - @override - Widget build(BuildContext context) { - final width = MediaQuery.of(context).size.width; - - if (widget.ip != previousIp) { - setState(() => loadStatus = 0); - fetchLogs(inOffset: 0); - setState(() => previousIp = widget.ip); - } - - Widget status() { - switch (loadStatus) { - case 0: - return SizedBox( - width: double.maxFinite, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const CircularProgressIndicator(), - const SizedBox(height: 30), - Text( - AppLocalizations.of(context)!.loadingLogs, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ) - ], - ), - ); - - case 1: - if (logsData!.data.isNotEmpty) { - return RefreshIndicator( - onRefresh: fetchLogs, - child: ListView.builder( - controller: scrollController, - padding: const EdgeInsets.only(top: 0), - itemCount: isLoadingMore == true - ? logsData!.data.length+1 - : logsData!.data.length, - itemBuilder: (context, index) { - if (isLoadingMore == true && index == logsData!.data.length) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 20), - child: Center( - child: CircularProgressIndicator(), - ), - ); - } - else { - return LogTile( - log: logsData!.data[index], - index: index, - length: logsData!.data.length, - useAlwaysNormalTile: true, - onLogTap: (log) => { - if (width > 700) { - showDialog( - context: context, - builder: (context) => LogDetailsScreen( - log: log, - dialog: true - ) - ) - } - else { - Navigator.push(context, MaterialPageRoute( - builder: (context) => LogDetailsScreen( - log: log, - dialog: false - ) - )) - } - } - ); - } - } - ), - ); - } - else { - return Center( - child: Text( - AppLocalizations.of(context)!.noLogsDisplay, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22, - 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, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ) - ], - ), - ); - - default: - return const SizedBox(); - } - } - - return Scaffold( - appBar: AppBar( - title: Text(widget.name != null && widget.name != '' ? widget.name! : widget.ip), - centerTitle: true, - actions: [ - if (!(Platform.isAndroid || Platform.isIOS)) ...[ - IconButton( - onPressed: fetchLogs, - icon: const Icon(Icons.refresh_rounded), - tooltip: AppLocalizations.of(context)!.refresh, - ), - const SizedBox(width: 8) - ] - ], - ), - body: status(), - ); - } -} \ 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 4d2c754..e5b1451 100644 --- a/lib/screens/clients/search_clients.dart +++ b/lib/screens/clients/search_clients.dart @@ -1,21 +1,19 @@ // ignore_for_file: use_build_context_synchronously -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:animations/animations.dart'; import 'package:provider/provider.dart'; 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/models/menu_option.dart'; import 'package:adguard_home_manager/classes/process_modal.dart'; -import 'package:adguard_home_manager/functions/compare_versions.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'; @@ -23,7 +21,7 @@ import 'package:adguard_home_manager/models/clients.dart'; import 'package:adguard_home_manager/providers/status_provider.dart'; class SearchClients extends StatefulWidget { - const SearchClients({Key? key}) : super(key: key); + const SearchClients({super.key}); @override State createState() => _SearchClientsState(); @@ -89,7 +87,7 @@ class _SearchClientsState extends State { 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 clientsProvider.deleteClient(client); @@ -113,7 +111,7 @@ class _SearchClientsState extends State { } void confirmEditClient(Client client) async { - ProcessModal processModal = ProcessModal(context: context); + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.addingClient); final result = await clientsProvider.editClient(client); @@ -137,31 +135,12 @@ class _SearchClientsState extends State { } void openClientModal(Client client) { - if (width > 900 || !(Platform.isAndroid | Platform.isIOS)) { - showDialog( - barrierDismissible: false, - context: context, - builder: (BuildContext context) => ClientScreen( - onConfirm: confirmEditClient, - serverVersion: statusProvider.serverStatus!.serverVersion, - onDelete: deleteClient, - client: client, - dialog: true, - ) - ); - } - else { - Navigator.push(context, MaterialPageRoute( - fullscreenDialog: true, - builder: (BuildContext context) => ClientScreen( - onConfirm: confirmEditClient, - serverVersion: statusProvider.serverStatus!.serverVersion, - onDelete: deleteClient, - client: client, - dialog: false, - ) - )); - } + openClientFormModal( + context: context, + width: width, + onConfirm: confirmEditClient, + onDelete: deleteClient + ); } void openDeleteModal(Client client) { @@ -173,15 +152,15 @@ class _SearchClientsState 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( @@ -246,92 +225,96 @@ class _SearchClientsState 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: serverVersionIsAhead( - currentVersion: statusProvider.serverStatus!.serverVersion, - referenceVersion: 'v0.107.28', - referenceVersionBeta: 'v0.108.0-b.33' - ) == true - ? 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 - : clientsScreen[index].safesearchEnabled == true + 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/connect.dart b/lib/screens/connect/connect.dart index 3528718..01ac7cc 100644 --- a/lib/screens/connect/connect.dart +++ b/lib/screens/connect/connect.dart @@ -11,7 +11,7 @@ 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(); @@ -61,26 +61,28 @@ class _ConnectState extends State { appBar: AppBar( title: Text(AppLocalizations.of(context)!.connect), ), - body: 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 - ? 70 : 20 - : -70, - right: 20, - child: const FabConnect() - ) - ], + 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 47cb65e..05ed1a2 100644 --- a/lib/screens/connect/fab.dart +++ b/lib/screens/connect/fab.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:adguard_home_manager/widgets/add_server_modal.dart'; -import 'package:adguard_home_manager/widgets/version_warning_modal.dart'; +import 'package:adguard_home_manager/widgets/add_server/add_server_functions.dart'; class FabConnect extends StatelessWidget { const FabConnect({Key? key}) : super(key: key); @@ -12,37 +11,7 @@ class FabConnect extends StatelessWidget { void openAddServerModal() async { await Future.delayed(const Duration(seconds: 0), (() => { - if (width > 700) { - showDialog( - context: context, - barrierDismissible: false, - builder: (context) => AddServerModal( - window: true, - onUnsupportedVersion: (version) => showDialog( - context: context, - builder: (ctx) => VersionWarningModal( - version: version - ), - barrierDismissible: false - ), - ), - ) - } - else { - Navigator.push(context, MaterialPageRoute( - fullscreenDialog: true, - builder: (BuildContext context) => AddServerModal( - window: false, - onUnsupportedVersion: (version) => showDialog( - context: context, - builder: (ctx) => VersionWarningModal( - version: version - ), - barrierDismissible: false - ), - ) - )) - } + openServerFormModal(context: context, width: width) })); } diff --git a/lib/screens/filters/add_button.dart b/lib/screens/filters/add_button.dart index 3cbedc9..7dd4d8c 100644 --- a/lib/screens/filters/add_button.dart +++ b/lib/screens/filters/add_button.dart @@ -6,8 +6,8 @@ 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/add_custom_rule.dart'; -import 'package:adguard_home_manager/screens/filters/add_list_modal.dart'; +import 'package:adguard_home_manager/screens/filters/modals/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'; @@ -32,7 +32,7 @@ class AddFiltersButton extends StatelessWidget { final width = MediaQuery.of(context).size.width; void confirmAddRule(String rule) async { - ProcessModal processModal = ProcessModal(context: context); + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.addingRule); final result = await filteringProvider.addCustomRule(rule); @@ -56,31 +56,34 @@ class AddFiltersButton extends StatelessWidget { } void openAddCustomRule() { - if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) { - showDialog( - context: context, - builder: (context) => AddCustomRule( - onConfirm: confirmAddRule, - dialog: true, - ), - barrierDismissible: false - ); - } - else { - Navigator.of(context).push( - MaterialPageRoute( - fullscreenDialog: true, - builder: (context) => AddCustomRule( - onConfirm: confirmAddRule, - dialog: false, + 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 confirmAddList({required String name, required String url, required String type}) async { - ProcessModal processModal = ProcessModal(context: context); + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.addingList); final result = await filteringProvider.addList(name: name, url: url, type: type); @@ -131,6 +134,7 @@ class AddFiltersButton extends StatelessWidget { else { showModalBottomSheet( context: context, + useRootNavigator: true, builder: (ctx) => AddListModal( type: type, onConfirm: confirmAddList, diff --git a/lib/screens/filters/add_list_modal.dart b/lib/screens/filters/add_list_modal.dart deleted file mode 100644 index fceebf9..0000000 --- a/lib/screens/filters/add_list_modal.dart +++ /dev/null @@ -1,230 +0,0 @@ -import 'dart:io'; - -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; - final bool dialog; - - const AddListModal({ - Key? key, - required this.type, - this.list, - this.onConfirm, - this.onEdit, - required this.dialog - }) : 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) { - Widget content() { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: SingleChildScrollView( - child: Wrap( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( - children: [ - Padding( - padding: const EdgeInsets.only(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) - ], - ); - } - - if (widget.dialog == true) { - return Dialog( - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 400 - ), - child: content() - ), - ); - } - else { - return Padding( - padding: MediaQuery.of(context).viewInsets, - child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28) - ), - color: Theme.of(context).dialogBackgroundColor - ), - child: content() - ), - ); - } - } -} \ No newline at end of file diff --git a/lib/screens/filters/blocked_services_screen.dart b/lib/screens/filters/blocked_services_screen.dart deleted file mode 100644 index 75d941b..0000000 --- a/lib/screens/filters/blocked_services_screen.dart +++ /dev/null @@ -1,262 +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/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 dialog; - - const BlockedServicesScreen({ - Key? key, - required this.dialog - }) : super(key: key); - - @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(context: context); - processModal.open(AppLocalizations.of(context)!.updating); - - final result = await filteringProvider.updateBlockedServices(values); - - processModal.close(); - - if (result == true) { - showSnacbkar( - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.blockedServicesUpdated, - color: Colors.green - ); - } - else { - showSnacbkar( - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.blockedServicesNotUpdated, - color: Colors.red - ); - } - } - - Widget body() { - 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(); - } - } - - if (widget.dialog == true) { - return Dialog( - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 400 - ), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - IconButton( - onPressed: () => Navigator.pop(context), - icon: const Icon(Icons.clear_rounded), - tooltip: AppLocalizations.of(context)!.close, - ), - const SizedBox(width: 8), - Text( - AppLocalizations.of(context)!.blockedServices, - style: const TextStyle( - fontSize: 22 - ), - ) - ], - ), - IconButton( - onPressed: updateBlockedServices, - icon: const Icon( - Icons.save_rounded - ), - tooltip: AppLocalizations.of(context)!.save, - ), - ], - ), - ), - Expanded( - child: body() - ), - ], - ) - ), - ); - } - else { - return Scaffold( - appBar: AppBar( - title: Text(AppLocalizations.of(context)!.blockedServices), - actions: [ - IconButton( - onPressed: updateBlockedServices, - icon: const Icon( - Icons.save_rounded - ), - tooltip: AppLocalizations.of(context)!.save, - ), - const SizedBox(width: 10) - ], - ), - body: RefreshIndicator( - onRefresh: () async { - final result = await filteringProvider.loadBlockedServices(); - if (result == false) { - showSnacbkar( - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.blockedServicesListNotLoaded, - color: Colors.red - ); - } - }, - 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 90e93ab..0000000 --- a/lib/screens/filters/check_host_modal.dart +++ /dev/null @@ -1,259 +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/providers/servers_provider.dart'; - -class CheckHostModal extends StatefulWidget { - final bool dialog; - - const CheckHostModal({ - Key? key, - required this.dialog - }) : 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 { - setState(() => resultWidget = checking()); - - final result = await serversProvider.apiClient!.checkHostFiltered(host: domainController.text); - - if (mounted) { - 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 { - 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 - ), - ) - ], - )); - } - } - } - - Widget content() { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: SingleChildScrollView( - child: Wrap( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( - children: [ - Padding( - padding: const EdgeInsets.only(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 - ), - ), - ), - ], - ), - ) - ], - ) - ], - ); - } - - if (widget.dialog == true) { - return Dialog( - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 400 - ), - child: content() - ), - ); - } - else { - return Padding( - padding: MediaQuery.of(context).viewInsets, - child: Container( - width: double.maxFinite, - decoration: BoxDecoration( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28), - ), - color: Theme.of(context).dialogBackgroundColor - ), - child: content() - ), - ); - } - } -} \ 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..d5579a6 --- /dev/null +++ b/lib/screens/filters/details/add_list_modal.dart @@ -0,0 +1,257 @@ +import 'dart:io'; + +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) { + 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 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..3065588 --- /dev/null +++ b/lib/screens/filters/details/check_host_modal.dart @@ -0,0 +1,265 @@ +// 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/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) { + 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 { + 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/list_details_screen.dart b/lib/screens/filters/details/list_details_screen.dart similarity index 78% rename from lib/screens/filters/list_details_screen.dart rename to lib/screens/filters/details/list_details_screen.dart index 42464ec..bbc7c41 100644 --- a/lib/screens/filters/list_details_screen.dart +++ b/lib/screens/filters/details/list_details_screen.dart @@ -7,10 +7,11 @@ 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/add_list_modal.dart'; -import 'package:adguard_home_manager/screens/filters/delete_list_modal.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'; @@ -24,11 +25,11 @@ class ListDetailsScreen extends StatefulWidget { final bool dialog; const ListDetailsScreen({ - Key? key, + super.key, required this.listId, required this.type, required this.dialog - }) : super(key: key); + }); @override State createState() => _ListDetailsScreenState(); @@ -76,15 +77,20 @@ class _ListDetailsScreenState extends State { // ------- // } - void updateList(FilteringListActions action) async { - ProcessModal processModal = ProcessModal(context: context); + void updateList({ + required FilteringListActions action, + required Filter filterList, + }) async { + ProcessModal processModal = ProcessModal(); processModal.open( - list!.enabled == true - ? AppLocalizations.of(context)!.disablingList - : AppLocalizations.of(context)!.enablingList, + action == FilteringListActions.edit + ? AppLocalizations.of(context)!.savingList + : action == FilteringListActions.disable + ? AppLocalizations.of(context)!.disablingList + : AppLocalizations.of(context)!.enablingList, ); final result = await filteringProvider.updateList( - list: list, + list: filterList, type: widget.type, action: action ); @@ -153,6 +159,11 @@ class _ListDetailsScreenState extends State { 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, @@ -204,7 +215,8 @@ class _ListDetailsScreenState extends State { list: list, type: widget.type, onEdit: ({required Filter list, required String type}) async => updateList( - FilteringListActions.edit + action: FilteringListActions.edit, + filterList: list ), dialog: true, ), @@ -213,11 +225,13 @@ class _ListDetailsScreenState extends State { else { showModalBottomSheet( context: context, + useRootNavigator: true, builder: (ctx) => AddListModal( list: list, type: widget.type, onEdit: ({required Filter list, required String type}) async => updateList( - FilteringListActions.edit + action: FilteringListActions.edit, + filterList: list ), dialog: false, ), @@ -235,7 +249,7 @@ class _ListDetailsScreenState extends State { context: context, builder: (c) => DeleteListModal( onConfirm: () async { - ProcessModal processModal = ProcessModal(context: context); + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.deletingList); final result = await filteringProvider.deleteList( listUrl: list!.url, @@ -302,9 +316,10 @@ class _ListDetailsScreenState extends State { children: [ IconButton( onPressed: () => updateList( - list!.enabled == true + action: list!.enabled == true ? FilteringListActions.disable - : FilteringListActions.enable + : FilteringListActions.enable, + filterList: list ), icon: Icon( list.enabled == true @@ -343,46 +358,54 @@ class _ListDetailsScreenState extends State { ); } else { - return Scaffold( - appBar: AppBar( - title: Text(AppLocalizations.of(context)!.listDetails), - actions: list != null ? actions() : null, - ), - body: Stack( - children: [ - if (list != null) ListView( - children: content(), + return Dialog.fullscreen( + child: Scaffold( + appBar: AppBar( + leading: CloseButton( + onPressed: () => Navigator.pop(context), ), - if (list == null) Center( - child: Text( - AppLocalizations.of(context)!.listNotAvailable, - style: const TextStyle( - fontSize: 24, + title: Text(AppLocalizations.of(context)!.listDetails), + actions: list != null ? actions() : null, + ), + body: SafeArea( + child: Stack( + children: [ + if (list != null) ListView( + children: content(), ), - ), + 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, + ), + ), + ) + ], ), - 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( - list!.enabled == true - ? FilteringListActions.disable - : FilteringListActions.enable - ), - child: Icon( - list.enabled == true - ? Icons.gpp_bad_rounded - : Icons.verified_user_rounded, - ), - ), - ) - ], + ), ), ); } diff --git a/lib/screens/filters/filters.dart b/lib/screens/filters/filters.dart index 00cc214..c0a7e20 100644 --- a/lib/screens/filters/filters.dart +++ b/lib/screens/filters/filters.dart @@ -6,13 +6,13 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:adguard_home_manager/screens/filters/check_host_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/list_details_screen.dart'; -import 'package:adguard_home_manager/screens/filters/remove_custom_rule_modal.dart'; -import 'package:adguard_home_manager/screens/filters/blocked_services_screen.dart'; -import 'package:adguard_home_manager/screens/filters/update_interval_lists_modal.dart'; +import 'package:adguard_home_manager/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'; @@ -51,13 +51,11 @@ class _FiltersState extends State { final width = MediaQuery.of(context).size.width; void updateLists() async { - ProcessModal processModal = ProcessModal(context: context); + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.updatingLists); - final result = await filteringProvider.updateLists(); - + if (!mounted) return; processModal.close(); - if (result['success'] == true) { showSnacbkar( appConfigProvider: appConfigProvider, @@ -87,6 +85,7 @@ class _FiltersState extends State { else { showModalBottomSheet( context: context, + useRootNavigator: true, builder: (context) => const CheckHostModal( dialog: false, ), @@ -98,7 +97,7 @@ class _FiltersState extends State { } void enableDisableFiltering() async { - ProcessModal processModal = ProcessModal(context: context); + ProcessModal processModal = ProcessModal(); processModal.open( statusProvider.serverStatus!.filteringEnabled == true ? AppLocalizations.of(context)!.disableFiltering @@ -126,7 +125,7 @@ class _FiltersState extends State { } void setUpdateFrequency(int value) async { - ProcessModal processModal = ProcessModal(context: context); + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.changingUpdateFrequency); final result = await filteringProvider.changeUpdateFrequency(value); @@ -149,31 +148,14 @@ class _FiltersState extends State { } } - void openBlockedServicesModal() { + void openBlockedServices() { Future.delayed(const Duration(seconds: 0), () { - if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) { - showDialog( - context: context, - builder: (context) => const BlockedServicesScreen( - dialog: true, - ), - barrierDismissible: false - ); - } - else { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const BlockedServicesScreen( - dialog: false, - ), - ) - ); - } + openBlockedServicesModal(context: context, width: width); }); } void removeCustomRule(String rule) async { - ProcessModal processModal = ProcessModal(context: context); + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.deletingRule); final result = await filteringProvider.removeCustomRule(rule); @@ -206,28 +188,31 @@ class _FiltersState extends State { } void openListDetails(Filter filter, String type) { - if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) { - showDialog( - context: context, - builder: (context) => ListDetailsScreen( - listId: filter.id, - type: type, - dialog: true, - ), - barrierDismissible: false - ); - } - else { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => ListDetailsScreen( - listId: filter.id, - type: type, - dialog: false, - ) - ) - ); - } + showGeneralDialog( + context: context, + barrierColor: !(width > 900 || !(Platform.isAndroid | Platform.isIOS)) + ?Colors.transparent + : Colors.black54, + transitionBuilder: (context, anim1, anim2, child) { + return SlideTransition( + position: Tween( + begin: const Offset(0, 1), + end: const Offset(0, 0) + ).animate( + CurvedAnimation( + parent: anim1, + curve: Curves.easeInOutCubicEmphasized + ) + ), + child: child, + ); + }, + pageBuilder: (context, animation, secondaryAnimation) => ListDetailsScreen( + listId: filter.id, + type: type, + dialog: width > 900 || !(Platform.isAndroid | Platform.isIOS), + ), + ); } List actions() { @@ -286,6 +271,7 @@ class _FiltersState extends State { else { showModalBottomSheet( context: context, + useRootNavigator: true, builder: (context) => UpdateIntervalListsModal( interval: filteringProvider.filtering!.interval, onChange: setUpdateFrequency, @@ -312,7 +298,7 @@ class _FiltersState extends State { ) ), PopupMenuItem( - onTap: openBlockedServicesModal, + onTap: openBlockedServices, child: Row( children: [ const Icon(Icons.block), @@ -341,20 +327,24 @@ class _FiltersState extends State { } } - if (width > 1200) { - return FiltersTripleColumn( - onRemoveCustomRule: openRemoveCustomRuleModal, - onOpenDetailsModal: openListDetails, - actions: actions(), - ); - } - else { - return FiltersTabsView( - appConfigProvider: appConfigProvider, - actions: actions(), - onRemoveCustomRule: openRemoveCustomRuleModal, - onOpenDetailsModal: openListDetails, - ); - } + 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 3fdf3d5..f4cca03 100644 --- a/lib/screens/filters/filters_list.dart +++ b/lib/screens/filters/filters_list.dart @@ -132,7 +132,7 @@ class _FiltersListState extends State { TextButton.icon( onPressed: () async { final result = await filteringProvider.fetchFilters(); - if (result == false) { + if (result == false && mounted) { showSnacbkar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.errorLoadFilters, @@ -172,7 +172,7 @@ class _FiltersListState extends State { loadStatus: widget.loadStatus, onRefresh: () async { final result = await filteringProvider.fetchFilters(); - if (result == false) { + if (result == false && mounted) { showSnacbkar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.errorLoadFilters, diff --git a/lib/screens/filters/filters_tabs_view.dart b/lib/screens/filters/filters_tabs_view.dart index 81cd67a..bc96bc8 100644 --- a/lib/screens/filters/filters_tabs_view.dart +++ b/lib/screens/filters/filters_tabs_view.dart @@ -5,6 +5,7 @@ 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'; @@ -17,12 +18,12 @@ class FiltersTabsView extends StatefulWidget { final void Function(Filter, String) onOpenDetailsModal; const FiltersTabsView({ - Key? key, + super.key, required this.appConfigProvider, required this.actions, required this.onOpenDetailsModal, required this.onRemoveCustomRule - }) : super(key: key); + }); @override State createState() => _FiltersTabsViewState(); @@ -47,6 +48,8 @@ class _FiltersTabsViewState extends State with TickerProviderSt Widget build(BuildContext context) { final filteringProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + return DefaultTabController( length: 3, child: NestedScrollView( @@ -62,10 +65,12 @@ class _FiltersTabsViewState extends State with TickerProviderSt 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( @@ -73,7 +78,7 @@ class _FiltersTabsViewState extends State with TickerProviderSt children: [ const Icon(Icons.verified_user_rounded), const SizedBox(width: 8), - Text(AppLocalizations.of(context)!.whitelists,) + Text(AppLocalizations.of(context)!.whitelists) ], ), ), diff --git a/lib/screens/filters/filters_triple_column.dart b/lib/screens/filters/filters_triple_column.dart index de0ffb0..7d15639 100644 --- a/lib/screens/filters/filters_triple_column.dart +++ b/lib/screens/filters/filters_triple_column.dart @@ -7,12 +7,13 @@ 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/widgets/options_modal.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'; @@ -27,16 +28,18 @@ class FiltersTripleColumn extends StatelessWidget { final List actions; const FiltersTripleColumn({ - Key? key, + super.key, required this.onRemoveCustomRule, required this.onOpenDetailsModal, required this.actions, - }) : super(key: key); + }); @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'^@@.*$'); @@ -70,266 +73,17 @@ class FiltersTripleColumn extends StatelessWidget { else { return null; } - } - - Widget content() { - 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: [ - Text( - AppLocalizations.of(context)!.customRules, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w500 - ), - ), - AddFiltersButton( - type: '', - widget: (fn) => IconButton( - onPressed: fn, - icon: const Icon(Icons.add_rounded), - 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: CustomListTile( - onLongPress: () => showDialog( - context: context, - builder: (context) => OptionsModal( - options: [ - MenuOption( - title: AppLocalizations.of(context)!.copyClipboard, - icon: Icons.copy_rounded, - action: () => copyToClipboard( - value: filteringProvider.filtering!.userRules[index], - successMessage: AppLocalizations.of(context)!.copiedClipboard, - ) - ) - ] - ) - ), - title: filteringProvider.filtering!.userRules[index], - subtitleWidget: generateSubtitle(filteringProvider.filtering!.userRules[index]), - trailing: IconButton( - onPressed: () => onRemoveCustomRule(filteringProvider.filtering!.userRules[index]), - icon: const Icon(Icons.delete) - ), - ), - ), - ), - ) - ], - ), - ), - ], - ); - - case LoadStatus.error: - return SizedBox.expand( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon( - Icons.error, - color: Colors.red, - size: 50, - ), - const SizedBox(height: 30), - Text( - AppLocalizations.of(context)!.filtersNotLoaded, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ) - ], - ), - ], - ), - ); - - default: - return const SizedBox(); - } - } + } return Scaffold( appBar: AppBar( + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, title: Text(AppLocalizations.of(context)!.filters), actions: [ IconButton( onPressed: () async { final result = await filteringProvider.fetchFilters(); - if (result == false) { + if (result == false && context.mounted) { showSnacbkar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.errorLoadFilters, @@ -343,7 +97,255 @@ class FiltersTripleColumn extends StatelessWidget { ...actions ], ), - body: content(), + 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: [ + Text( + AppLocalizations.of(context)!.customRules, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500 + ), + ), + AddFiltersButton( + type: '', + widget: (fn) => IconButton( + onPressed: fn, + icon: const Icon(Icons.add_rounded), + 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_options_menu.dart b/lib/screens/filters/list_options_menu.dart index 1d644a0..c35d080 100644 --- a/lib/screens/filters/list_options_menu.dart +++ b/lib/screens/filters/list_options_menu.dart @@ -1,14 +1,18 @@ // ignore_for_file: use_build_context_synchronously -import 'package:adguard_home_manager/classes/process_modal.dart'; +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_modal.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'; @@ -22,19 +26,21 @@ class ListOptionsMenu extends StatelessWidget { final String listType; const ListOptionsMenu({ - Key? key, + super.key, required this.list, required this.child, required this.listType, - }) : super(key: key); + }); @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(context: context); + ProcessModal processModal = ProcessModal(); processModal.open( list.enabled == true ? AppLocalizations.of(context)!.disablingList @@ -50,7 +56,8 @@ class ListOptionsMenu extends StatelessWidget { ); processModal.close(); - + + if (!context.mounted) return; if (result == true) { showSnacbkar( appConfigProvider: appConfigProvider, @@ -67,6 +74,32 @@ class ListOptionsMenu extends StatelessWidget { } } + 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( @@ -92,35 +125,61 @@ class ListOptionsMenu extends StatelessWidget { ); } ), + 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( - onLongPress: () => showDialog( - context: context, - builder: (context) => OptionsModal( - 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 - ) - ), - ] - ) - ), - child: child + 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 + ) ), ), ); diff --git a/lib/screens/filters/add_custom_rule.dart b/lib/screens/filters/modals/add_custom_rule.dart similarity index 86% rename from lib/screens/filters/add_custom_rule.dart rename to lib/screens/filters/modals/add_custom_rule.dart index dfb5753..abe68bc 100644 --- a/lib/screens/filters/add_custom_rule.dart +++ b/lib/screens/filters/modals/add_custom_rule.dart @@ -1,4 +1,5 @@ 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/functions/open_url.dart'; @@ -6,13 +7,13 @@ import 'package:adguard_home_manager/constants/urls.dart'; class AddCustomRule extends StatefulWidget { final void Function(String) onConfirm; - final bool dialog; + final bool fullScreen; const AddCustomRule({ - Key? key, + super.key, required this.onConfirm, - required this.dialog - }) : super(key: key); + required this.fullScreen + }); @override State createState() => _AddCustomRuleState(); @@ -124,25 +125,26 @@ class _AddCustomRuleState extends State { ), ), Container(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), + 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) => setState(() => preset = BlockingPresets.values[v]), + colors: SegmentedButtonSlideColors( + barColor: Theme.of(context).colorScheme.primary.withOpacity(0.2), + backgroundSelectedColor: Theme.of(context).colorScheme.primary, + foregroundSelectedColor: Theme.of(context).colorScheme.onPrimary, + foregroundUnselectedColor: Theme.of(context).colorScheme.onSurface, + hoverColor: Theme.of(context).colorScheme.onSurfaceVariant, + ), + textOverflow: TextOverflow.ellipsis, + fontSize: 14, + height: 40, + margin: const EdgeInsets.symmetric( + horizontal: 24, ), ), Container(height: 20), @@ -328,7 +330,34 @@ class _AddCustomRuleState extends State { ]; } - if (widget.dialog == true) { + if (widget.fullScreen == true) { + return Dialog.fullscreen( + child: Scaffold( + appBar: AppBar( + leading: CloseButton(onPressed: () => Navigator.pop(context)), + 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: SafeArea( + child: ListView( + children: content(), + ), + ) + ), + ); + } + else { return Dialog( child: ConstrainedBox( constraints: const BoxConstraints( @@ -383,27 +412,5 @@ class _AddCustomRuleState extends State { ), ); } - else { - return Scaffold( - appBar: AppBar( - title: Text(AppLocalizations.of(context)!.addCustomRule), - actions: [ - IconButton( - onPressed: checkValidValues() == true - ? () { - Navigator.pop(context); - widget.onConfirm(buildRule()); - } - : null, - icon: const Icon(Icons.check) - ), - const SizedBox(width: 10) - ], - ), - body: ListView( - children: content(), - ) - ); - } } } \ 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..b95dd4f --- /dev/null +++ b/lib/screens/filters/modals/blocked_services_screen.dart @@ -0,0 +1,317 @@ +// 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) { + showSnacbkar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.blockedServicesUpdated, + color: Colors.green + ); + } + else { + showSnacbkar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.blockedServicesNotUpdated, + color: Colors.red + ); + } + } + + if (widget.fullScreen == true) { + return Dialog.fullscreen( + child: Scaffold( + appBar: AppBar( + 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( + child: RefreshIndicator( + onRefresh: () async { + final result = await filteringProvider.loadBlockedServices(); + if (result == false) { + showSnacbkar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.blockedServicesListNotLoaded, + color: Colors.red + ); + } + }, + child: _Content( + values: values, + updateValues: updateValues, + ) + ), + ), + ), + ); + } + 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: _Content( + values: values, + updateValues: updateValues, + ) + ), + ], + ) + ), + ); + } + } +} + +class _Content extends StatelessWidget { + final List values; + final void Function(bool value, BlockedService item) updateValues; + + const _Content({ + required this.values, + required this.updateValues, + }); + + @override + Widget build(BuildContext context) { + final filteringProvider = Provider.of(context); + + 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/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 100% rename from lib/screens/filters/remove_custom_rule_modal.dart rename to lib/screens/filters/modals/remove_custom_rule_modal.dart 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..f6504a5 --- /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: 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), + ), + ], + ), + ), + 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..944d515 --- /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({ + Key? key, + required this.selectedBlacklists, + required this.selectedWhitelists, + required this.onDelete, + }) : super(key: key); + + @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..fdb809c --- /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({ + Key? key, + required this.selectedBlacklists, + required this.selectedWhitelists, + required this.onDelete, + }) : super(key: key); + + @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..b423f77 --- /dev/null +++ b/lib/screens/filters/selection/selection_lists.dart @@ -0,0 +1,312 @@ +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({ + Key? key, + required this.lists, + required this.selectedLists, + required this.onSelect, + required this.selectAll, + required this.unselectAll, + }) : super(key: key); + + @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({ + Key? key, + required this.list, + required this.onSelect, + required this.isSelected, + }) : super(key: key); + + @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..1475454 --- /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 + ); + if (!mounted) return; + processModal.close(); + 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 + ); + if (!mounted) return; + processModal.close(); + 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 c168793..0000000 --- a/lib/screens/filters/update_interval_lists_modal.dart +++ /dev/null @@ -1,304 +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; - final bool dialog; - - const UpdateIntervalListsModal({ - Key? key, - required this.interval, - required this.onChange, - required this.dialog - }) : 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); - - Widget content() { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: SingleChildScrollView( - child: Wrap( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( - children: [ - Padding( - padding: const EdgeInsets.only(top: 24), - child: Icon( - Icons.update_rounded, - size: 24, - color: Theme.of(context).listTileTheme.iconColor - ), - ), - Container( - 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: _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), - ), - ), - ), - ), - ), - FractionallySizedBox( - widthFactor: 0.5, - child: Padding( - padding: const EdgeInsets.only(left: 6), - 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), - ), - ), - ), - ), - ), - FractionallySizedBox( - widthFactor: 0.5, - child: Padding( - padding: const EdgeInsets.only(right: 6), - 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), - ), - ), - ), - ), - ), - FractionallySizedBox( - widthFactor: 0.5, - child: Padding( - padding: const EdgeInsets.only(left: 6), - 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), - ), - ), - ), - ), - ), - FractionallySizedBox( - widthFactor: 0.5, - child: Padding( - padding: const EdgeInsets.only(right: 6), - 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), - ), - ), - ), - ), - ), - FractionallySizedBox( - widthFactor: 0.5, - child: Padding( - padding: const EdgeInsets.only(left: 6), - 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), - ), - ], - ), - ), - if (Platform.isIOS) const SizedBox(height: 16) - ], - ); - } - - if (widget.dialog == true) { - return Dialog( - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 500 - ), - child: content() - ), - ); - } - else { - return Padding( - padding: mediaQueryData.viewInsets, - child: Container( - height: Platform.isIOS ? 406 : 390, - decoration: BoxDecoration( - color: Theme.of(context).dialogBackgroundColor, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28) - ), - ), - child: content() - ), - ); - } - } -} \ No newline at end of file diff --git a/lib/screens/home/appbar.dart b/lib/screens/home/appbar.dart index 8660f54..52ff350 100644 --- a/lib/screens/home/appbar.dart +++ b/lib/screens/home/appbar.dart @@ -1,9 +1,12 @@ +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/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'; @@ -15,9 +18,9 @@ class HomeAppBar extends StatelessWidget { final bool innerBoxScrolled; const HomeAppBar({ - Key? key, + super.key, required this.innerBoxScrolled - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -25,6 +28,8 @@ class HomeAppBar extends StatelessWidget { final statusProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + final Server? server = serversProvider.selectedServer; void navigateServers() { @@ -40,6 +45,7 @@ class HomeAppBar extends StatelessWidget { floating: true, centerTitle: false, forceElevated: innerBoxScrolled, + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, leading: Stack( children: [ Center( @@ -114,6 +120,14 @@ class HomeAppBar extends StatelessWidget { ], ), 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( diff --git a/lib/screens/home/chart.dart b/lib/screens/home/chart.dart index 682d5b9..0093ae8 100644 --- a/lib/screens/home/chart.dart +++ b/lib/screens/home/chart.dart @@ -1,39 +1,51 @@ 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, - required this.hoursInterval - }) : super(key: key); + 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); - final bool isEmpty = data.every((i) => i == 0); + final bool isEmpty = widget.data.every((i) => i == 0); if (!(appConfigProvider.hideZeroValues == true && isEmpty == true)) { List dateTimes = []; - DateTime currentDate = DateTime.now().subtract(Duration(hours: hoursInterval*data.length+1)); - for (var i = 0; i < data.length; i++) { - currentDate = currentDate.add(Duration(hours: hoursInterval)); + DateTime currentDate = DateTime.now().subtract(Duration(hours: widget.hoursInterval*widget.data.length+1)); + for (var i = 0; i < widget.data.length; i++) { + currentDate = currentDate.add(Duration(hours: widget.hoursInterval)); dateTimes.add(currentDate); } @@ -49,60 +61,87 @@ class HomeChart extends StatelessWidget { ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: isEmpty + ? CrossAxisAlignment.center + : CrossAxisAlignment.start, children: [ Flexible( - child: Text( - label, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.onSurface + 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, + ), ) + ], + ) ], ), ), @@ -110,13 +149,35 @@ class HomeChart extends StatelessWidget { width: double.maxFinite, height: 150, child: CustomLineChart( - data: data, - color: color, + data: widget.data, + color: widget.color, dates: dateTimes, - daysInterval: hoursInterval == 24, + 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 + ), + ) + ], + ), + ) ], ), ), diff --git a/lib/screens/home/combined_chart.dart b/lib/screens/home/combined_chart.dart index d7328d1..89519db 100644 --- a/lib/screens/home/combined_chart.dart +++ b/lib/screens/home/combined_chart.dart @@ -79,13 +79,13 @@ class CombinedHomeChart extends StatelessWidget { replacedSafeBrowsing: appConfigProvider.hideZeroValues == true ? removeZero(statusProvider.serverStatus!.stats.replacedSafebrowsing) != null ? CombinedChartItem( - label: AppLocalizations.of(context)!.malwarePhisingBlocked, + label: AppLocalizations.of(context)!.malwarePhishingBlocked, color: Colors.green, data: statusProvider.serverStatus!.stats.replacedSafebrowsing ) : null : CombinedChartItem( - label: AppLocalizations.of(context)!.malwarePhisingBlocked, + label: AppLocalizations.of(context)!.malwarePhishingBlocked, color: Colors.green, data: statusProvider.serverStatus!.stats.replacedSafebrowsing ) , @@ -104,51 +104,6 @@ class CombinedHomeChart extends StatelessWidget { ) , ); - Widget legend({ - required String label, - required Color color, - required String primaryValue, - String? secondaryValue - }) { - 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 - ), - ) - ], - ) - ], - ); - } - final hoursInterval = statusProvider.serverStatus!.stats.timeUnits == "days" ? 24 : 1; List dateTimes = []; @@ -193,28 +148,28 @@ class CombinedHomeChart extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - legend( + _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( + 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( + 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( + if (data.replacedParental != null) _Legend( label: data.replacedParental!.label, color: data.replacedParental!.color, primaryValue: intFormat(statusProvider.serverStatus!.stats.numReplacedParental, Platform.localeName), @@ -259,28 +214,28 @@ class CombinedHomeChart extends StatelessWidget { ), ), const SizedBox(height: 16), - legend( + _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( + 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( + 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( + if (data.replacedParental != null) _Legend( label: data.replacedParental!.label, color: data.replacedParental!.color, primaryValue: intFormat(statusProvider.serverStatus!.stats.numReplacedParental, Platform.localeName), @@ -302,4 +257,60 @@ class CombinedHomeChart extends StatelessWidget { return const SizedBox(); } } +} + +class _Legend extends StatelessWidget { + final String label; + final Color color; + final String primaryValue; + final String? secondaryValue; + + const _Legend({ + Key? key, + required this.label, + required this.color, + required this.primaryValue, + this.secondaryValue + }) : super(key: key); + + @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 0e7db97..bd77be7 100644 --- a/lib/screens/home/fab.dart +++ b/lib/screens/home/fab.dart @@ -1,7 +1,7 @@ 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/status_provider.dart'; import 'package:adguard_home_manager/constants/enums.dart'; @@ -28,6 +28,7 @@ class HomeFab extends StatelessWidget { showModalBottomSheet( context: context, isScrollControlled: true, + useRootNavigator: true, builder: (context) => const ManagementModal( dialog: false, ), diff --git a/lib/screens/home/home.dart b/lib/screens/home/home.dart index a37eee7..f34a186 100644 --- a/lib/screens/home/home.dart +++ b/lib/screens/home/home.dart @@ -8,12 +8,14 @@ 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'; @@ -21,7 +23,7 @@ import 'package:adguard_home_manager/providers/app_config_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(); @@ -32,8 +34,14 @@ class _HomeState extends State { late bool isVisible; @override - initState(){ - Provider.of(context, listen: false).getServerStatus(); + 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(); @@ -58,230 +66,10 @@ class _HomeState extends State { Widget build(BuildContext context) { final statusProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final logsProvider = Provider.of(context); final width = MediaQuery.of(context).size.width; - Widget 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)!.loadingStatus, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ) - ], - ), - ); - } - - Widget loadError() { - 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, - ), - ) - ], - ), - ); - } - - List listItems() { - return [ - 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, - ), - ), - 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, - ), - ), - FractionallySizedBox( - widthFactor: width > 700 ? 0.5 : 1, - child: HomeChart( - data: statusProvider.serverStatus!.stats.replacedSafebrowsing, - label: AppLocalizations.of(context)!.malwarePhisingBlocked, - 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, - ), - ), - 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, - ), - ), - ], - ), - - if (appConfigProvider.combinedChartHome == true) const Padding( - padding: EdgeInsets.symmetric(horizontal: 16), - child: CombinedHomeChart(), - ), - - if (width <= 700) ...appConfigProvider.homeTopItemsOrder.asMap().entries.map((item) { - Widget list() { - switch (item.value) { - case HomeTopItems.queriedDomains: - return TopItems( - label: AppLocalizations.of(context)!.topQueriedDomains, - data: statusProvider.serverStatus!.stats.topQueriedDomains, - type: 'topQueriedDomains', - ); - - case HomeTopItems.blockedDomains: - return TopItems( - label: AppLocalizations.of(context)!.topBlockedDomains, - data: statusProvider.serverStatus!.stats.topBlockedDomains, - type: 'topBlockedDomains', - ); - - case HomeTopItems.recurrentClients: - return TopItems( - label: AppLocalizations.of(context)!.topClients, - data: statusProvider.serverStatus!.stats.topClients, - type: 'topClients', - clients: true, - ); - - default: - return const SizedBox(); - } - } - - return Column( - children: [ - list(), - if (item.key < appConfigProvider.homeTopItemsOrder.length - 1) ...[ - 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 (width > 700) Column( - children: [ - Wrap( - alignment: WrapAlignment.center, - children: appConfigProvider.homeTopItemsOrder.map((item) { - switch (item) { - case HomeTopItems.queriedDomains: - return Padding( - padding: const EdgeInsets.only(bottom: 16), - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 500 - ), - child: TopItems( - label: AppLocalizations.of(context)!.topQueriedDomains, - data: statusProvider.serverStatus!.stats.topQueriedDomains, - type: 'topQueriedDomains', - ), - ), - ); - - case HomeTopItems.blockedDomains: - return Padding( - padding: const EdgeInsets.only(bottom: 16), - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 500 - ), - child: TopItems( - label: AppLocalizations.of(context)!.topBlockedDomains, - data: statusProvider.serverStatus!.stats.topBlockedDomains, - type: 'topBlockedDomains', - ), - ), - ); - - case HomeTopItems.recurrentClients: - return Padding( - padding: const EdgeInsets.only(bottom: 16), - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 500 - ), - child: TopItems( - label: AppLocalizations.of(context)!.topClients, - data: statusProvider.serverStatus!.stats.topClients, - type: 'topClients', - ), - ), - ); - - default: - return const SizedBox(); - } - }).toList(), - ), - ], - ) - ]; - } - return Scaffold( body: SafeArea( top: false, @@ -304,7 +92,7 @@ class _HomeState extends State { displacement: 95, onRefresh: () async { final result = await statusProvider.getServerStatus(); - if (result == false) { + if (mounted && result == false) { showSnacbkar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.serverStatusNotRefreshed, @@ -318,13 +106,153 @@ class _HomeState extends State { handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), ), if (statusProvider.loadStatus == LoadStatus.loading) SliverFillRemaining( - child: loading(), + 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: listItems() + 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), + ], ), if (statusProvider.loadStatus == LoadStatus.error) SliverFillRemaining( - child: loadError(), + 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, + ), + ) + ], + ), + ) ), ], ) diff --git a/lib/screens/home/management_modal.dart b/lib/screens/home/management_modal.dart deleted file mode 100644 index aaeaab7..0000000 --- a/lib/screens/home/management_modal.dart +++ /dev/null @@ -1,462 +0,0 @@ -// 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/functions/snackbar.dart'; -import 'package:adguard_home_manager/functions/compare_versions.dart'; -import 'package:adguard_home_manager/providers/status_provider.dart'; -import 'package:adguard_home_manager/functions/format_time.dart'; -import 'package:adguard_home_manager/providers/app_config_provider.dart'; - -class ManagementModal extends StatefulWidget { - final bool dialog; - - const ManagementModal({ - Key? key, - required this.dialog - }) : super(key: key); - - @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 (result != null) { - if (result != false) { - appConfigProvider.addLog(result); - } - showSnacbkar( - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.invalidUsernamePassword, - color: Colors.red - ); - } - } - - void disableWithCountdown(int time) async { - updateBlocking(value: false, filter: 'general', time: time); - expandableController.toggle(); - } - - Widget mainSwitch() { - Widget topRow({required bool legacyMode}) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - if (legacyMode == false) ...[ - 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), - ], - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - AppLocalizations.of(context)!.allProtections, - 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)}" - ) - ] - ], - ), - ], - ), - 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: legacyMode == true ? 'general_legacy' : 'general' - ); - } : null, - ) - ] - ); - } - - Widget bottomRow() { - return Container( - height: 40, - margin: const EdgeInsets.only(top: 8), - child: ListView( - scrollDirection: Axis.horizontal, - children: [ - ActionChip( - label: Text(AppLocalizations.of(context)!.seconds(30)), - onPressed: statusProvider.protectionsManagementProcess.contains('general') == false && statusProvider.serverStatus!.generalEnabled == true - ? () => 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 - ? () => 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 - ? () => 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 - ? () => 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 - ? () => disableWithCountdown(86399000) - : null, - ), - ], - ), - ); - } - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: serverVersionIsAhead( - currentVersion: statusProvider.serverStatus!.serverVersion, - referenceVersion: 'v0.107.28', - referenceVersionBeta: 'v0.108.0-b.33' - ) == true - ? 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), - expanded: Column( - children: [ - topRow(legacyMode: false), - bottomRow(), - const SizedBox(height: 8) - ], - ) - ), - ), - ), - ) - ) - : Material( - color: Colors.transparent, - borderRadius: BorderRadius.circular(28), - child: InkWell( - onTap: statusProvider.protectionsManagementProcess.contains('general') == false - ? () => updateBlocking( - value: !statusProvider.serverStatus!.generalEnabled, - filter: 'general_legacy' - ) : 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).primaryColor.withOpacity(0.1) - ), - child: topRow(legacyMode: true) - ), - ), - ) - ); - } - - 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, - ) - ], - ), - ), - ), - ); - } - - Widget header() { - return 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, - ), - ), - ), - ], - ), - ], - ); - } - - List toggles() { - return [ - mainSwitch(), - Container(height: 10), - smallSwitch( - AppLocalizations.of(context)!.ruleFiltering, - Icons.filter_list_rounded, - statusProvider.serverStatus!.filteringEnabled, - (value) => updateBlocking(value: value, filter: 'filtering'), - statusProvider.protectionsManagementProcess.contains('filtering') - ), - smallSwitch( - AppLocalizations.of(context)!.safeBrowsing, - Icons.vpn_lock_rounded, - statusProvider.serverStatus!.safeBrowsingEnabled, - (value) => updateBlocking(value: value, filter: 'safeBrowsing'), - statusProvider.protectionsManagementProcess.contains('safeBrowsing') - ), - smallSwitch( - AppLocalizations.of(context)!.parentalFiltering, - Icons.block, - statusProvider.serverStatus!.parentalControlEnabled, - (value) => updateBlocking(value: value, filter: 'parentalControl'), - statusProvider.protectionsManagementProcess.contains('parentalControl') - ), - smallSwitch( - AppLocalizations.of(context)!.safeSearch, - Icons.search_rounded, - statusProvider.serverStatus!.safeSearchEnabled, - (value) => updateBlocking(value: value, filter: 'safeSearch'), - statusProvider.protectionsManagementProcess.contains('safeSearch') - ), - ]; - } - - if (widget.dialog == true) { - return Dialog( - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 400 - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: SingleChildScrollView( - child: Wrap( - children: [ - header(), - ...toggles() - ], - ), - ), - ), - Padding( - padding: const EdgeInsets.all(24), - child: Row( - 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: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: SingleChildScrollView( - child: Wrap( - children: [ - header(), - ...toggles() - ], - ), - ), - ), - Padding( - padding: const EdgeInsets.all(24), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(AppLocalizations.of(context)!.close), - ), - ], - ), - ), - if (Platform.isIOS) const SizedBox(height: 16) - ], - ), - ); - } - } -} \ 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..b18586b --- /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) { + showSnacbkar( + 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..4e34d68 --- /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({ + Key? key, + required this.label, + required this.icon, + required this.value, + required this.onChange, + required this.disabled, + }) : super(key: key); + + @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 c130564..b716c82 100644 --- a/lib/screens/home/server_status.dart +++ b/lib/screens/home/server_status.dart @@ -9,17 +9,19 @@ class ServerStatusWidget extends StatelessWidget { final ServerStatus serverStatus; const ServerStatusWidget({ - Key? key, + super.key, required this.serverStatus, - }) : super(key: key); + }); @override Widget build(BuildContext context) { final width = MediaQuery.of(context).size.width; - - return Container( + 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, @@ -30,40 +32,39 @@ class ServerStatusWidget extends StatelessWidget { ), ), const SizedBox(height: 16), - SizedBox( - height: width > 700 ? 66 : 146, - child: GridView( - padding: const EdgeInsets.all(0), - physics: const NeverScrollableScrollPhysics(), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: width > 700 ? 4 : 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 - ), - ], + 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 2f57106..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) { diff --git a/lib/screens/home/top_items.dart b/lib/screens/home/top_items.dart deleted file mode 100644 index c43ba3a..0000000 --- a/lib/screens/home/top_items.dart +++ /dev/null @@ -1,229 +0,0 @@ -// 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/widgets/domain_options.dart'; -import 'package:adguard_home_manager/screens/top_items/top_items_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/status_provider.dart'; -import 'package:adguard_home_manager/providers/logs_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 statusProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - final logsProvider = Provider.of(context); - - final width = MediaQuery.of(context).size.width; - - Widget rowItem(Map item) { - String? name; - if (clients != null && clients == true) { - try { - name = statusProvider.serverStatus!.clients.firstWhere((c) => c.ids.contains(item.keys.toList()[0])).name; - } catch (e) { - // ---- // - } - } - - return Material( - color: Colors.transparent, - child: DomainOptions( - item: item.keys.toList()[0], - isClient: type == 'topClients', - isBlocked: type == 'topBlockedDomains', - onTap: () { - if (type == 'topQueriedDomains' || type == 'topBlockedDomains') { - logsProvider.setSearchText(item.keys.toList()[0]); - logsProvider.setSelectedClients(null); - logsProvider.setAppliedFilters( - AppliedFiters( - selectedResultStatus: 'all', - searchText: item.keys.toList()[0], - clients: null - ) - ); - appConfigProvider.setSelectedScreen(2); - } - else if (type == 'topClients') { - logsProvider.setSearchText(null); - logsProvider.setSelectedClients([item.keys.toList()[0]]); - logsProvider.setAppliedFilters( - AppliedFiters( - selectedResultStatus: 'all', - searchText: null, - clients: [item.keys.toList()[0]] - ) - ); - } - }, - 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 statusProvider.serverStatus!.stats.topQueriedDomains; - - case 'topBlockedDomains': - return statusProvider.serverStatus!.stats.topBlockedDomains; - - case 'topClients': - return statusProvider.serverStatus!.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: () => { - if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) { - showDialog( - context: context, - barrierDismissible: false, - builder: (context) => TopItemsModal( - type: type, - title: label, - isClient: clients, - data: generateData(), - ) - ) - } - else { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => TopItemsScreen( - type: type, - 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..b323eb0 --- /dev/null +++ b/lib/screens/home/top_items/row_item.dart @@ -0,0 +1,288 @@ +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 StatefulWidget { + 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 + State createState() => _RowItemState(); +} + +class _RowItemState extends State with TickerProviderStateMixin { + late AnimationController expandController; + late Animation animation; + + @override + void initState() { + super.initState(); + prepareAnimations(); + _runExpandCheck(); + } + + void prepareAnimations() { + expandController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 250) + ); + animation = CurvedAnimation( + parent: expandController, + curve: Curves.ease, + ); + } + + void _runExpandCheck() { + if (widget.showColor) { + expandController.forward(); + } + else { + expandController.reverse(); + } + } + + @override + void didUpdateWidget(oldWidget) { + super.didUpdateWidget(oldWidget); + _runExpandCheck(); + } + + @override + void dispose() { + expandController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final statusProvider = Provider.of(context); + + String? name; + if (widget.clients == true) { + try { + name = statusProvider.serverStatus!.clients.firstWhere((c) => c.ids.contains(widget.domain)).name; + } catch (e) { + // ---- // + } + } + + return Material( + color: Colors.transparent, + child: OptionsMenu( + value: widget.domain, + options: widget.options, + onTap: widget.onTapEntry, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 8 + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Row( + children: [ + SizeTransition( + axisAlignment: 1.0, + sizeFactor: animation, + axis: Axis.horizontal, + child: Container( + margin: const EdgeInsets.only(right: 16), + width: 12, + height: 12, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + color: widget.chartColor + ), + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.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( + widget.number, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface + ), + ) + ], + ), + ), + ), + ); + } +} + +class OthersRowItem extends StatefulWidget { + final List> items; + final bool showColor; + + const OthersRowItem({ + super.key, + required this.items, + required this.showColor, + }); + + @override + State createState() => _OthersRowItemState(); +} + +class _OthersRowItemState extends State with SingleTickerProviderStateMixin { + late AnimationController expandController; + late Animation animation; + + @override + void initState() { + super.initState(); + prepareAnimations(); + _runExpandCheck(); + } + + void prepareAnimations() { + expandController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 250) + ); + animation = CurvedAnimation( + parent: expandController, + curve: Curves.ease, + ); + } + + void _runExpandCheck() { + if (widget.showColor) { + expandController.forward(); + } + else { + expandController.reverse(); + } + } + + @override + void didUpdateWidget(oldWidget) { + super.didUpdateWidget(oldWidget); + _runExpandCheck(); + } + + @override + void dispose() { + expandController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (widget.items.length <= 5) { + return const SizedBox(); + } + + return SizeTransition( + axisAlignment: 1.0, + sizeFactor: animation, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 8 + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Row( + children: [ + Container( + margin: const EdgeInsets.only(right: 16), + width: 12, + height: 12, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + color: Colors.grey + ), + ), + 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( + widget.items.sublist(5, widget.items.length).map((e) => e.values.first.toInt()) + ).reduce((a, b) => a + b).toString(), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface + ), + ) + ], + ), + ), + ); + } +} \ 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..758a0b6 --- /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: null + ) + ); + 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) { + showSnacbkar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.userFilteringRulesUpdated, + color: Colors.green + ); + } + else { + showSnacbkar( + 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) { + showSnacbkar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.clientAddedSuccessfully, + color: Colors.green + ); + } + else if (result.successful == false && result.content == 'client_another_list') { + showSnacbkar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.clientAnotherList, + color: Colors.red + ); + } + else { + showSnacbkar( + 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..1070953 --- /dev/null +++ b/lib/screens/home/top_items/top_items_screen.dart @@ -0,0 +1,380 @@ +// 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/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 } + +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; + 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) { + 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(); + + if (widget.isFullscreen == true) { + return Dialog.fullscreen( + child: 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 + ), + autofocus: true, + ), + ) + : 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, + ), + 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( + child: _Content( + buildValue: widget.buildValue, + isClient: widget.isClient, + onTapEntry: widget.onTapEntry, + options: widget.options, + screenData: sortedValues, + total: total, + withProgressBar: widget.withProgressBar, + ), + ), + ), + ); + } + 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: _Content( + buildValue: widget.buildValue, + isClient: widget.isClient, + onTapEntry: widget.onTapEntry, + options: widget.options, + screenData: sortedValues, + total: total, + withProgressBar: widget.withProgressBar, + ), + ), + ], + ), + ), + ); + } + } +} + +class _Content extends StatelessWidget { + final List> screenData; + 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 _Content({ + required this.screenData, + 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); + + if (screenData.isNotEmpty) { + return ListView.builder( + padding: const EdgeInsets.only(top: 0), + itemCount: screenData.length, + itemBuilder: (context, index) { + String? name; + if (isClient != null && isClient == true) { + try { + name = statusProvider.serverStatus!.clients.firstWhere((c) => c.ids.contains(screenData[index].keys.toList()[0])).name; + } catch (e) { + // ---- // + } + } + + return OptionsMenu( + options: options, + value: screenData[index].keys.toList()[0], + onTap: onTapEntry != null + ? (v) { + onTapEntry!(v); + Navigator.pop(context); + } + : null, + child: CustomListTile( + title: screenData[index].keys.toList()[0], + trailing: Text( + buildValue(screenData[index].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((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), + ], + ), + ], + ) + ), + ); + } + ); + } + else { + return 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/screens/home/top_items/top_items_section.dart b/lib/screens/home/top_items/top_items_section.dart new file mode 100644 index 0000000..bc021cf --- /dev/null +++ b/lib/screens/home/top_items/top_items_section.dart @@ -0,0 +1,384 @@ +// 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/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/models/menu_option.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; + +class TopItemsSection extends StatefulWidget { + 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 + State createState() => _TopItemsState(); +} + +class _TopItemsState extends State { + bool _showChart = true; + + final colors = [ + Colors.red, + Colors.green, + Colors.blue, + Colors.orange, + Colors.teal, + Colors.grey + ]; + + @override + void initState() { + _showChart = Provider.of(context, listen: false).showTopItemsChart; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width; + + final withChart = widget.type != HomeTopItems.avgUpstreamResponseTime; + + Map chartData() { + Map values = {}; + widget.data.sublist(0, widget.data.length > 5 ? 5 : widget.data.length).forEach((element) { + values = { + ...values, + element.keys.first: element.values.first.toDouble() + }; + }); + if (widget.data.length > 5) { + final int rest = List.from( + widget.data.sublist(5, widget.data.length).map((e) => e.values.first.toInt()) + ).reduce((a, b) => a + b); + values = { + ...values, + AppLocalizations.of(context)!.others: rest.toDouble() + }; + } + return values; + } + + final Widget noItems = Padding( + padding: const EdgeInsets.only( + bottom: 20, + top: 10 + ), + child: Text( + AppLocalizations.of(context)!.noItems, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ); + + return SizedBox( + child: Column( + children: [ + if (widget.data.isEmpty) noItems, + if (widget.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: chartData(), + colors: colors + ) + ), + ) + ), + Expanded( + flex: 2, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only( + top: 8, + bottom: 16 + ), + child: Text( + widget.label, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500 + ), + ), + ), + _ItemsList( + colors: colors, + data: widget.data, + clients: widget.type == HomeTopItems.recurrentClients, + type: widget.type, + showChart: withChart == true ? _showChart : false, + buildValue: widget.buildValue, + menuOptions: widget.menuOptions, + onTapEntry: widget.onTapEntry, + ), + if (withChart == true) OthersRowItem( + items: widget.data, + showColor: true, + ) + ] + ), + ) + ], + ), + ), + if (widget.data.isNotEmpty && width <= 700) Builder( + builder: (context) { + if (widget.withChart == true) { + return Column( + children: [ + ExpansionPanelList( + expandedHeaderPadding: const EdgeInsets.all(0), + elevation: 0, + expansionCallback: (_, isExpanded) => setState(() => _showChart = isExpanded), + animationDuration: const Duration(milliseconds: 250), + children: [ + ExpansionPanel( + headerBuilder: (context, isExpanded) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + mainAxisAlignment: width <= 700 + ? MainAxisAlignment.spaceBetween + : MainAxisAlignment.center, + children: [ + Flexible( + child: Text( + widget.label, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface + ), + ), + ), + ], + ), + ), + body: Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Column( + children: [ + SizedBox( + height: 150, + child: CustomPieChart( + data: chartData(), + colors: colors + ) + ), + const SizedBox(height: 16), + ], + ), + ), + isExpanded: _showChart + ), + ], + ), + Padding( + padding: const EdgeInsets.only(top: 8), + child: _ItemsList( + colors: colors, + data: widget.data, + clients: widget.type == HomeTopItems.recurrentClients, + type: widget.type, + showChart: _showChart, + buildValue: widget.buildValue, + menuOptions: widget.menuOptions, + onTapEntry: widget.onTapEntry, + ), + ), + if (widget.withChart == true) OthersRowItem( + items: widget.data, + showColor: _showChart, + ), + const SizedBox(height: 16), + ], + ); + } + else { + return Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 18), + child: Row( + mainAxisAlignment: width <= 700 + ? MainAxisAlignment.spaceBetween + : MainAxisAlignment.center, + children: [ + Flexible( + child: Text( + widget.label, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface + ), + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16), + child: _ItemsList( + colors: colors, + data: widget.data, + clients: widget.type == HomeTopItems.recurrentClients, + type: widget.type, + showChart: false, + buildValue: widget.buildValue, + menuOptions: widget.menuOptions, + onTapEntry: widget.onTapEntry, + ), + ), + if (widget.withChart == true) OthersRowItem( + items: widget.data, + showColor: false, + ), + const SizedBox(height: 16), + ], + ); + } + }, + ), + + if (widget.data.length > 5) ...[ + Padding( + padding: const EdgeInsets.only(right: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => { + showGeneralDialog( + context: context, + barrierColor: !(width > 700 || !(Platform.isAndroid | Platform.isIOS)) + ?Colors.transparent + : Colors.black54, + transitionBuilder: (context, anim1, anim2, child) { + return SlideTransition( + position: Tween( + begin: const Offset(0, 1), + end: const Offset(0, 0) + ).animate( + CurvedAnimation( + parent: anim1, + curve: Curves.easeInOutCubicEmphasized + ) + ), + child: child, + ); + }, + pageBuilder: (context, animation, secondaryAnimation) => TopItemsScreen( + type: widget.type, + title: widget.label, + isClient: widget.type == HomeTopItems.recurrentClients, + data: widget.data, + withProgressBar: widget.withProgressBar, + buildValue: widget.buildValue, + options: widget.menuOptions, + onTapEntry: widget.onTapEntry, + isFullscreen: !(width > 700 || !(Platform.isAndroid | Platform.isIOS)), + ) + ) + }, + 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() + ); + } +} \ 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 fcd9d74..0000000 --- a/lib/screens/logs/clients_modal.dart +++ /dev/null @@ -1,206 +0,0 @@ -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/providers/clients_provider.dart'; -import 'package:adguard_home_manager/providers/logs_provider.dart'; - -class ClientsModal extends StatefulWidget { - final List? value; - final bool dialog; - - const ClientsModal({ - Key? key, - required this.value, - required this.dialog - }) : 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 clientsProvider = 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.only( - left: 24, - top: 8, - right: 12, - bottom: 8 - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: 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 = clientsProvider.clients!.autoClients.map((item) => item.ip).toList(); - }); - } - - void unselectAll() { - setState(() { - selectedClients = []; - }); - } - - Widget content() { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Column( - children: [ - Padding( - padding: const EdgeInsets.only( - top: 24, - bottom: 16, - ), - child: Icon( - Icons.smartphone_rounded, - size: 24, - color: Theme.of(context).listTileTheme.iconColor - ), - ), - Text( - AppLocalizations.of(context)!.clients, - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurface - ), - ), - const SizedBox(height: 16), - ], - ), - Flexible( - child: ListView.builder( - itemCount: clientsProvider.clients!.autoClients.length, - itemBuilder: (context, index) => listItem( - label: clientsProvider.clients!.autoClients[index].ip, - onChanged: () { - if (selectedClients.contains(clientsProvider.clients!.autoClients[index].ip)) { - setState(() { - selectedClients = selectedClients.where( - (item) => item != clientsProvider.clients!.autoClients[index].ip - ).toList(); - }); - } - else { - setState(() { - selectedClients.add(clientsProvider.clients!.autoClients[index].ip); - }); - } - } - ) - ) - ), - Padding( - padding: const EdgeInsets.all(24), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - TextButton( - onPressed: selectedClients.length == clientsProvider.clients!.autoClients.length - ? () => unselectAll() - : () => selectAll(), - child: Text( - selectedClients.length == clientsProvider.clients!.autoClients.length - ? AppLocalizations.of(context)!.unselectAll - : AppLocalizations.of(context)!.selectAll - ) - ), - TextButton( - onPressed: apply, - child: Text(AppLocalizations.of(context)!.apply) - ) - ], - ), - ), - if (Platform.isIOS) const SizedBox(height: 16) - ], - ); - } - - if (widget.dialog == true) { - return Dialog( - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 500 - ), - child: content() - ), - ); - } - else { - return ConstrainedBox( - constraints: BoxConstraints( - maxHeight: height-50 - ), - child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28) - ), - color: Theme.of(context).dialogBackgroundColor - ), - child: content() - ), - ); - } - } -} \ No newline at end of file diff --git a/lib/screens/logs/configuration/config_widgets.dart b/lib/screens/logs/configuration/config_widgets.dart new file mode 100644 index 0000000..1fa59f9 --- /dev/null +++ b/lib/screens/logs/configuration/config_widgets.dart @@ -0,0 +1,276 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/screens/logs/configuration/logs_config_modal.dart'; + +class LogsConfigOptions extends StatelessWidget { + final bool generalSwitch; + final void Function(bool) updateGeneralSwitch; + final bool anonymizeClientIp; + final void Function(bool) updateAnonymizeClientIp; + final List retentionItems; + final double? retentionTime; + final void Function(double?) updateRetentionTime; + final void Function() onClear; + final void Function() onConfirm; + + const LogsConfigOptions({ + super.key, + required this.generalSwitch, + required this.updateGeneralSwitch, + required this.anonymizeClientIp, + required this.updateAnonymizeClientIp, + required this.retentionItems, + required this.retentionTime, + required this.updateRetentionTime, + required this.onClear, + required this.onConfirm + }); + + @override + Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width; + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: SingleChildScrollView( + child: Wrap( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 24), + child: Icon( + Icons.settings, + size: 24, + color: Theme.of(context).listTileTheme.iconColor + ), + ), + const SizedBox(height: 16), + Text( + AppLocalizations.of(context)!.logsSettings, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + color: Theme.of(context).colorScheme.onSurface + ), + ), + const SizedBox(height: 16), + ], + ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Material( + color: Theme.of(context).colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(28), + child: InkWell( + onTap: () => updateGeneralSwitch(!generalSwitch), + borderRadius: BorderRadius.circular(28), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 8 + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + AppLocalizations.of(context)!.enableLog, + style: const TextStyle( + fontSize: 18, + ), + ), + Switch( + value: generalSwitch, + onChanged: updateGeneralSwitch, + ) + ], + ), + ), + ), + ), + ), + Container(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 14), + child: Column( + children: [ + Material( + color: Colors.transparent, + child: InkWell( + onTap: () => updateAnonymizeClientIp(!anonymizeClientIp), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 30), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + AppLocalizations.of(context)!.anonymizeClientIp, + style: const TextStyle( + fontSize: 16 + ), + ), + ), + Switch( + value: anonymizeClientIp, + onChanged: updateAnonymizeClientIp, + ) + ], + ), + ), + ), + ), + Container(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: DropdownButtonFormField( + items: retentionItems.map((item) => DropdownMenuItem( + value: item.value, + child: Text(item.label), + )).toList(), + value: retentionTime, + onChanged: (value) => updateRetentionTime(value as double), + decoration: InputDecoration( + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + label: Text(AppLocalizations.of(context)!.retentionTime) + ), + borderRadius: BorderRadius.circular(20), + ), + ), + ], + ), + ) + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(24), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (width > 500) TextButton( + onPressed: () { + Navigator.pop(context); + onClear(); + }, + child: Text(AppLocalizations.of(context)!.clearLogs) + ), + if (width <= 500) IconButton( + onPressed: () { + Navigator.pop(context); + onClear(); + }, + icon: const Icon(Icons.delete_rounded), + tooltip: AppLocalizations.of(context)!.clearLogs, + ), + Row( + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.cancel) + ), + const SizedBox(width: 20), + TextButton( + onPressed: retentionTime != null + ? () { + Navigator.pop(context); + onConfirm(); + } + : null, + child: Text( + AppLocalizations.of(context)!.confirm, + style: TextStyle( + color: retentionTime != null + ? Theme.of(context).colorScheme.primary + : Colors.grey + ), + ) + ), + ], + ) + ], + ), + ), + if (Platform.isIOS) const SizedBox(height: 16) + ], + ); + } +} + +class ConfigLogsLoading extends StatelessWidget { + const ConfigLogsLoading({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 30), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + AppLocalizations.of(context)!.loadingLogsSettings, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ), + ) + ], + ), + ); + } +} + +class ConfigLogsError extends StatelessWidget { + const ConfigLogsError({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.error, + color: Colors.red, + size: 50, + ), + const SizedBox(height: 30), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + AppLocalizations.of(context)!.logSettingsNotLoaded, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ), + ) + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/logs/configuration/logs_config_modal.dart b/lib/screens/logs/configuration/logs_config_modal.dart new file mode 100644 index 0000000..5c2ba44 --- /dev/null +++ b/lib/screens/logs/configuration/logs_config_modal.dart @@ -0,0 +1,187 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/screens/logs/configuration/config_widgets.dart'; + +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; + +class RetentionItem { + final String label; + final double value; + + const RetentionItem({ + required this.label, + required this.value, + }); +} + +class LogsConfigModal extends StatefulWidget { + final BuildContext context; + final void Function(Map) onConfirm; + final void Function() onClear; + final bool dialog; + final String serverVersion; + + const LogsConfigModal({ + super.key, + required this.context, + required this.onConfirm, + required this.onClear, + required this.dialog, + required this.serverVersion + }); + + @override + State createState() => _LogsConfigModalState(); +} + +class _LogsConfigModalState extends State { + bool generalSwitch = false; + bool anonymizeClientIp = false; + double? retentionTime; + + List retentionItems = []; + + LoadStatus loadStatus = LoadStatus.loading; + + void loadData() async { + final serversProvider = Provider.of(context, listen: false); + + final result = await serversProvider.apiClient2!.getQueryLogInfo(); + + if (mounted) { + if (result.successful == true) { + setState(() { + generalSwitch = result.content['enabled']; + anonymizeClientIp = result.content['anonymize_client_ip']; + retentionTime = result.content['interval'] != null + ? double.parse(result.content['interval'].toString()) + : null; + loadStatus = LoadStatus.loaded; + }); + } + else { + setState(() => loadStatus = LoadStatus.error); + } + } + } + + @override + void initState() { + retentionItems = [ + RetentionItem( + label: AppLocalizations.of(widget.context)!.hours6, + value: 21600000 + ), + RetentionItem( + label: AppLocalizations.of(widget.context)!.hours24, + value: 86400000 + ), + RetentionItem( + label: AppLocalizations.of(widget.context)!.days7, + value: 604800000 + ), + RetentionItem( + label: AppLocalizations.of(widget.context)!.days30, + value: 2592000000 + ), + RetentionItem( + label: AppLocalizations.of(widget.context)!.days90, + value: 7776000000 + ), + ]; + + loadData(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + if (widget.dialog == true) { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500 + ), + child: Builder( + builder: (context) { + switch (loadStatus) { + case LoadStatus.loading: + return const ConfigLogsLoading(); + + case LoadStatus.loaded: + return LogsConfigOptions( + generalSwitch: generalSwitch, + updateGeneralSwitch: (v) => setState(() => generalSwitch = v), + anonymizeClientIp: anonymizeClientIp, + updateAnonymizeClientIp: (v) => setState(() => anonymizeClientIp = v), + retentionItems: retentionItems, + retentionTime: retentionTime, + updateRetentionTime: (v) => setState(() => retentionTime = v), + onClear: () => widget.onClear(), + onConfirm: () => widget.onConfirm({ + "enabled": generalSwitch, + "interval": retentionTime, + "anonymize_client_ip": anonymizeClientIp + }) + ); + + case LoadStatus.error: + return const ConfigLogsError(); + + default: + return const SizedBox(); + } + }, + ) + ), + ); + } + else { + return Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28) + ), + color: Theme.of(context).dialogBackgroundColor + ), + child: SafeArea( + child: Builder( + builder: (context) { + switch (loadStatus) { + case LoadStatus.loading: + return const ConfigLogsLoading(); + + case LoadStatus.loaded: + return LogsConfigOptions( + generalSwitch: generalSwitch, + updateGeneralSwitch: (v) => setState(() => generalSwitch = v), + anonymizeClientIp: anonymizeClientIp, + updateAnonymizeClientIp: (v) => setState(() => anonymizeClientIp = v), + retentionItems: retentionItems, + retentionTime: retentionTime, + updateRetentionTime: (v) => setState(() => retentionTime = v), + onClear: () => widget.onClear(), + onConfirm: () => widget.onConfirm({ + "enabled": generalSwitch, + "interval": retentionTime, + "anonymize_client_ip": anonymizeClientIp + }) + ); + + case LoadStatus.error: + return const ConfigLogsError(); + + default: + return const SizedBox(); + } + }, + ), + ) + ); + } + } +} \ No newline at end of file diff --git a/lib/screens/logs/log_details_screen.dart b/lib/screens/logs/details/log_details_screen.dart similarity index 97% rename from lib/screens/logs/log_details_screen.dart rename to lib/screens/logs/details/log_details_screen.dart index 859d5e7..42918dd 100644 --- a/lib/screens/logs/log_details_screen.dart +++ b/lib/screens/logs/details/log_details_screen.dart @@ -6,8 +6,9 @@ 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/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/constants/urls.dart'; import 'package:adguard_home_manager/classes/process_modal.dart'; @@ -24,16 +25,18 @@ class LogDetailsScreen extends StatelessWidget { final bool dialog; const LogDetailsScreen({ - Key? key, + super.key, required this.log, required this.dialog - }) : super(key: key); + }); @override Widget build(BuildContext context) { final appConfigProvider = Provider.of(context); final statusProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + Filter? getList(int id) { try { return statusProvider.filteringStatus!.filters.firstWhere((filter) => filter.id == id, orElse: () { @@ -56,7 +59,7 @@ class LogDetailsScreen extends StatelessWidget { } void blockUnblock(String domain, String newStatus) async { - final ProcessModal processModal = ProcessModal(context: context); + final ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.savingUserFilters); final rules = await statusProvider.blockUnblockDomain( @@ -288,6 +291,7 @@ class LogDetailsScreen extends StatelessWidget { floating: true, centerTitle: false, forceElevated: innerBoxIsScrolled, + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, title: Text(AppLocalizations.of(context)!.logDetails), actions: [ IconButton( @@ -318,7 +322,6 @@ class LogDetailsScreen extends StatelessWidget { ], body: SafeArea( top: false, - bottom: false, child: Builder( builder: (context) => CustomScrollView( slivers: [ diff --git a/lib/screens/logs/log_list_tile.dart b/lib/screens/logs/details/log_list_tile.dart similarity index 100% rename from lib/screens/logs/log_list_tile.dart rename to lib/screens/logs/details/log_list_tile.dart diff --git a/lib/screens/logs/filter_status_modal.dart b/lib/screens/logs/filter_status_modal.dart deleted file mode 100644 index aec5aac..0000000 --- a/lib/screens/logs/filter_status_modal.dart +++ /dev/null @@ -1,216 +0,0 @@ -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/providers/logs_provider.dart'; - -class FilterStatusModal extends StatefulWidget { - final String value; - final bool dialog; - - const FilterStatusModal({ - Key? key, - required this.value, - required this.dialog - }) : 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); - - 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 - ) - ], - ), - ), - ), - ); - } - - Widget content() { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: SingleChildScrollView( - child: Wrap( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( - children: [ - Padding( - padding: const EdgeInsets.only( - 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 - ), - ), - ], - ) - ], - ), - Container(height: 16), - 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) - ) - ], - ), - ), - if (Platform.isIOS) const SizedBox(height: 16) - ], - ); - } - - if (widget.dialog == true) { - return Dialog( - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 400 - ), - child: content() - ), - ); - } - else { - return Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28) - ), - color: Theme.of(context).dialogBackgroundColor - ), - child: content() - ); - } - } -} \ 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..84728f8 --- /dev/null +++ b/lib/screens/logs/filters/clients_modal.dart @@ -0,0 +1,235 @@ +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/providers/clients_provider.dart'; +import 'package:adguard_home_manager/providers/logs_provider.dart'; + +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 selectedClients = []; + + @override + void initState() { + setState(() => selectedClients = widget.value ?? []); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final height = MediaQuery.of(context).size.height; + + if (widget.dialog == true) { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500 + ), + child: _ModalContent( + selectedClients: selectedClients, + onClientsSelected: (v) => setState(() => selectedClients = v), + ) + ), + ); + } + else { + return ConstrainedBox( + constraints: BoxConstraints( + maxHeight: height-50 + ), + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28) + ), + color: Theme.of(context).dialogBackgroundColor + ), + child: SafeArea( + child: _ModalContent( + selectedClients: selectedClients, + onClientsSelected: (v) => setState(() => selectedClients = v), + ), + ) + ), + ); + } + } +} + +class _ModalContent extends StatelessWidget { + final List selectedClients; + final void Function(List) onClientsSelected; + + const _ModalContent({ + required this.selectedClients, + required this.onClientsSelected, + }); + + @override + Widget build(BuildContext context) { + final clientsProvider = Provider.of(context); + final logsProvider = Provider.of(context); + + void apply() async { + logsProvider.setSelectedClients( + selectedClients.isNotEmpty ? selectedClients : null + ); + + Navigator.pop(context); + } + + void selectAll() { + onClientsSelected( + clientsProvider.clients!.autoClients.map((item) => item.ip).toList() + ); + } + + void unselectAll() { + onClientsSelected([]); + } + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Column( + children: [ + Padding( + padding: const EdgeInsets.only( + top: 24, + bottom: 16, + ), + child: Icon( + Icons.smartphone_rounded, + size: 24, + color: Theme.of(context).listTileTheme.iconColor + ), + ), + Text( + AppLocalizations.of(context)!.clients, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface + ), + ), + const SizedBox(height: 16), + ], + ), + Flexible( + child: ListView.builder( + itemCount: clientsProvider.clients!.autoClients.length, + itemBuilder: (context, index) => _ListItem( + label: clientsProvider.clients!.autoClients[index].ip, + checkboxActive: selectedClients.contains(clientsProvider.clients!.autoClients[index].ip), + onChanged: (isSelected) { + if (isSelected == true) { + onClientsSelected([ + ...selectedClients, + clientsProvider.clients!.autoClients[index].ip + ]); + } + else { + onClientsSelected( + selectedClients.where( + (item) => item != clientsProvider.clients!.autoClients[index].ip + ).toList() + ); + } + } + ) + ) + ), + Padding( + padding: const EdgeInsets.all(24), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + onPressed: selectedClients.length == clientsProvider.clients!.autoClients.length + ? () => unselectAll() + : () => selectAll(), + child: Text( + selectedClients.length == clientsProvider.clients!.autoClients.length + ? AppLocalizations.of(context)!.unselectAll + : AppLocalizations.of(context)!.selectAll + ) + ), + TextButton( + onPressed: apply, + child: Text(AppLocalizations.of(context)!.apply) + ) + ], + ), + ), + if (Platform.isIOS) const SizedBox(height: 16) + ], + ); + } +} + +class _ListItem extends StatelessWidget { + final String label; + final bool checkboxActive; + final void Function(bool) onChanged; + + const _ListItem({ + required this.label, + required this.checkboxActive, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () => onChanged(!checkboxActive), + child: Padding( + padding: const EdgeInsets.only( + left: 24, + top: 8, + right: 12, + bottom: 8 + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + label, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface + ), + ), + ), + Checkbox( + value: checkboxActive, + onChanged: (v) => onChanged(!checkboxActive), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5) + ), + ) + ], + ), + ), + ), + ); + } +} \ 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..c08d9a8 --- /dev/null +++ b/lib/screens/logs/filters/filter_status_modal.dart @@ -0,0 +1,262 @@ +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/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); + + void apply() async { + logsProvider.setSelectedResultStatus(value: selectedResultStatus); + + Navigator.pop(context); + } + + if (widget.dialog == true) { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 400 + ), + child: _Content( + onApply: apply, + updateSelectedResultStatus: (v) => setState(() => selectedResultStatus = v), + selectedResultStatus: selectedResultStatus, + ) + ), + ); + } + else { + return Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28) + ), + color: Theme.of(context).dialogBackgroundColor + ), + child: SafeArea( + child: _Content( + onApply: apply, + updateSelectedResultStatus: (v) => setState(() => selectedResultStatus = v), + selectedResultStatus: selectedResultStatus, + ), + ) + ); + } + } +} + +class _Content extends StatelessWidget { + final String selectedResultStatus; + final void Function(String) updateSelectedResultStatus; + final void Function() onApply; + + const _Content({ + required this.selectedResultStatus, + required this.updateSelectedResultStatus, + required this.onApply, + }); + + @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, + 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 + ), + ), + ], + ) + ], + ), + Container(height: 16), + _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 + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(24), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: onApply, + child: Text(AppLocalizations.of(context)!.apply) + ) + ], + ), + ), + if (Platform.isIOS) const SizedBox(height: 16) + ], + ); + } +} + +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..aec6b27 --- /dev/null +++ b/lib/screens/logs/filters/logs_filters_modal.dart @@ -0,0 +1,321 @@ +// 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 StatefulWidget { + final bool dialog; + + const LogsFiltersModal({ + super.key, + required this.dialog + }); + + @override + State createState() => _LogsFiltersModalState(); +} + +class _LogsFiltersModalState extends State { + TextEditingController searchController = TextEditingController(); + + @override + void initState() { + searchController.text = Provider.of(context, listen: false).searchText ?? ''; + super.initState(); + } + + @override + Widget build(BuildContext context) { + if (widget.dialog == true) { + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500 + ), + child: _FiltersList( + searchController: searchController, + onClearSearch: () => setState(() => searchController.text = "") + ) + ) + ), + ); + } + 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: _FiltersList( + searchController: searchController, + onClearSearch: () => setState(() => searchController.text = "") + ), + ) + ), + ); + } + } +} + +class _FiltersList extends StatelessWidget { + final TextEditingController searchController; + final void Function() onClearSearch; + + const _FiltersList({ + Key? key, + required this.searchController, + required this.onClearSearch, + }) : super(key: key); + + @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 + ); + } + } + + 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, + 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), + ], + ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Row( + children: [ + Expanded( + child: TextFormField( + controller: searchController, + onChanged: logsProvider.setSearchText, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.search_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + labelText: AppLocalizations.of(context)!.search, + suffixIcon: IconButton( + onPressed: () { + onClearSearch(); + logsProvider.setSearchText(null); + }, + icon: const Icon(Icons.clear) + ), + ), + ), + ) + ], + ), + ), + Container(height: 16), + CustomListTile( + title: AppLocalizations.of(context)!.client, + subtitle: logsProvider.selectedClients != null + ? "${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: () { + searchController.text = ""; + 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/log_tile.dart b/lib/screens/logs/log_tile.dart index bd2b1a9..f5db5ee 100644 --- a/lib/screens/logs/log_tile.dart +++ b/lib/screens/logs/log_tile.dart @@ -2,9 +2,15 @@ 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/domain_options.dart'; +import 'package:adguard_home_manager/widgets/options_menu.dart'; +import 'package:adguard_home_manager/providers/status_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/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/models/logs.dart'; @@ -17,22 +23,23 @@ class LogTile extends StatelessWidget { 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, this.isLogSelected, required this.onLogTap, - this.useAlwaysNormalTile - }) : super(key: key); + 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); Widget logStatusWidget({ required IconData icon, @@ -83,16 +90,63 @@ class LogTile extends StatelessWidget { } } - if (width > 1100 && !(useAlwaysNormalTile == true)) { + 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) { + showSnacbkar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.userFilteringRulesUpdated, + color: Colors.green + ); + } + else { + showSnacbkar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.userFilteringRulesNotUpdated, + color: Colors.red + ); + } + } + + final domainBlocked = isDomainBlocked(log.reason); + + if (twoColumns && !(useAlwaysNormalTile == true)) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: InkWell( borderRadius: BorderRadius.circular(28), - child: DomainOptions( - onTap: () => onLogTap(log), + child: OptionsMenu( + onTap: (_) => onLogTap(log), borderRadius: BorderRadius.circular(28), - item: log.question.name, - isBlocked: isDomainBlocked(log.reason), + 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 (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), @@ -250,10 +304,30 @@ class LogTile extends StatelessWidget { else { return Material( color: Colors.transparent, - child: DomainOptions( - onTap: () => onLogTap(log), - item: log.question.name, - isBlocked: isDomainBlocked(log.reason), + 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 (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), diff --git a/lib/screens/logs/logs.dart b/lib/screens/logs/logs.dart index 6d9cef6..560db9c 100644 --- a/lib/screens/logs/logs.dart +++ b/lib/screens/logs/logs.dart @@ -1,27 +1,11 @@ // 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/logs_filters_modal.dart'; -import 'package:adguard_home_manager/screens/logs/logs_config_modal.dart'; -import 'package:adguard_home_manager/screens/logs/log_tile.dart'; -import 'package:adguard_home_manager/screens/logs/log_details_screen.dart'; +import 'package:adguard_home_manager/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/functions/compare_versions.dart'; -import 'package:adguard_home_manager/providers/clients_provider.dart'; -import 'package:adguard_home_manager/constants/enums.dart'; -import 'package:adguard_home_manager/providers/status_provider.dart'; -import 'package:adguard_home_manager/providers/logs_provider.dart'; -import 'package:adguard_home_manager/providers/app_config_provider.dart'; import 'package:adguard_home_manager/models/logs.dart'; -import 'package:adguard_home_manager/providers/servers_provider.dart'; class Logs extends StatefulWidget { const Logs({Key? key}) : super(key: key); @@ -31,554 +15,46 @@ class Logs extends StatefulWidget { } class _LogsState extends State { - bool showDivider = true; - - Log? selectedLog; - - 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) { - showSnacbkar( - 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) { - showSnacbkar( - 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(); - } + Log? _selectedLog; @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); - - final width = MediaQuery.of(context).size.width; - - void updateConfig(Map data) async { - ProcessModal processModal = ProcessModal(context: context); - processModal.open(AppLocalizations.of(context)!.updatingSettings); - - final result = serverVersionIsAhead( - currentVersion: statusProvider.serverStatus!.serverVersion, - referenceVersion: 'v0.107.28', - referenceVersionBeta: 'v0.108.0-b.33' - ) == true - ? await serversProvider.apiClient!.updateQueryLogParameters(data: data) - : await serversProvider.apiClient!.updateQueryLogParametersLegacy(data: data); - - processModal.close(); - - if (result['result'] == 'success') { - showSnacbkar( - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.logsConfigUpdated, - color: Colors.green - ); - } - else { - appConfigProvider.addLog(result['log']); - - showSnacbkar( - 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 serversProvider.apiClient!.clearLogs(); - - processModal.close(); - - if (result['result'] == 'success') { - showSnacbkar( - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.logsCleared, - color: Colors.green - ); - } - else { - showSnacbkar( - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.logsNotCleared, - color: Colors.red - ); - } - } - - - void openFilersModal() { - if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) { - showDialog( - context: context, - builder: (context) => const LogsFiltersModal( - dialog: true, - ), - barrierDismissible: false - ); - } - else { - showModalBottomSheet( - context: context, - builder: (context) => const LogsFiltersModal( - dialog: false, - ), - 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 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, - ), - ) - ], - ), - ) + return LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth > 800) { + return Material( + color: Colors.transparent, + child: Row( + children: [ + Expanded( + flex: 2, + child: LogsListWidget( + twoColumns: true, + selectedLog: _selectedLog, + onLogSelected: (log) => setState(() => _selectedLog = log), ) - ], - ), - ) - ); - - 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: selectedLog != null && selectedLog == logsProvider.logsData!.data[index], - onLogTap: (log) { - if (width <= 1100) { - Navigator.push(context, MaterialPageRoute( - builder: (context) => LogDetailsScreen( - log: log, - dialog: false, - ) - )); - } - setState(() => selectedLog = log); - } - ); - } - 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(); - } - } - - Widget logsScreen() { - return Scaffold( - body: NestedScrollView( - headerSliverBuilder: (context, innerBoxIsScrolled) => [ - SliverOverlapAbsorber( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - sliver: SliverAppBar.large( - pinned: true, - floating: true, - centerTitle: false, - forceElevated: innerBoxIsScrolled, - title: Text(AppLocalizations.of(context)!.logs), - expandedHeight: logsProvider.appliedFilters.searchText != null || logsProvider.appliedFilters.selectedResultStatus != 'all' || logsProvider.appliedFilters.clients != null - ? 170 : null, - actions: [ - if (!(Platform.isAndroid || Platform.isIOS)) IconButton( - onPressed: () => logsProvider.fetchLogs(inOffset: 0), - icon: const Icon(Icons.refresh_rounded), - tooltip: AppLocalizations.of(context)!.refresh, - ), - logsProvider.loadStatus == LoadStatus.loaded - ? IconButton( - onPressed: openFilersModal, - icon: const Icon(Icons.filter_list_rounded), - tooltip: AppLocalizations.of(context)!.filters, + Expanded( + flex: 3, + child: _selectedLog != null + ? LogDetailsScreen( + log: _selectedLog!, + dialog: false, ) - : const SizedBox(), - if (statusProvider.serverStatus != null) IconButton( - tooltip: AppLocalizations.of(context)!.settings, - onPressed: () => { - if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) { - showDialog( - context: context, - builder: (context) => LogsConfigModal( - onConfirm: updateConfig, - onClear: clearQueries, - dialog: true, - serverVersion: statusProvider.serverStatus!.serverVersion, - ), - barrierDismissible: false - ) - } - else { - showModalBottomSheet( - context: context, - builder: (context) => LogsConfigModal( - onConfirm: updateConfig, - onClear: clearQueries, - dialog: false, - serverVersion: statusProvider.serverStatus!.serverVersion, - ), - backgroundColor: Colors.transparent, - isScrollControlled: true - ) - } - }, - icon: const Icon(Icons.settings) - ), - const SizedBox(width: 5), - ], - bottom: logsProvider.appliedFilters.searchText != null || logsProvider.appliedFilters.selectedResultStatus != 'all' || logsProvider.appliedFilters.clients != null - ? 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('all'); - logsProvider.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, - searchText: logsProvider.appliedFilters.searchText, - clients: null - ) - ); - logsProvider.setSelectedClients(null); - logsProvider.fetchLogs( - inOffset: 0, - responseStatus: logsProvider.appliedFilters.selectedResultStatus - ); - }, - ), - ], - const SizedBox(width: 15), - ], - ), - ) - ) - : null, - ), - ) - ], - body: generateBody() - ), - ); - } - - if (width > 1100) { - return Material( - color: Colors.transparent, - child: Row( - children: [ - Expanded( - flex: 1, - child: logsScreen() + : const SizedBox() + ) + ], ), - Expanded( - flex: 2, - child: selectedLog != null - ? LogDetailsScreen( - log: selectedLog!, - dialog: false, - ) - : const SizedBox() - ) - ], - ), - ); - } - else { - return logsScreen(); - } + ); + } + 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 2f16721..0000000 --- a/lib/screens/logs/logs_config_modal.dart +++ /dev/null @@ -1,413 +0,0 @@ -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/functions/compare_versions.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; - final bool dialog; - final String serverVersion; - - const LogsConfigModal({ - Key? key, - required this.onConfirm, - required this.onClear, - required this.dialog, - required this.serverVersion - }) : 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, - dialog: dialog, - serverVersion: serverVersion, - ); - } -} - -class LogsConfigModalWidget extends StatefulWidget { - final ServersProvider serversProvider; - final AppConfigProvider appConfigProvider; - final BuildContext context; - final void Function(Map) onConfirm; - final void Function() onClear; - final bool dialog; - final String serverVersion; - - const LogsConfigModalWidget({ - Key? key, - required this.serversProvider, - required this.appConfigProvider, - required this.context, - required this.onConfirm, - required this.onClear, - required this.dialog, - required this.serverVersion - }) : 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 serversProvider = Provider.of(context, listen: false); - - final result = serverVersionIsAhead( - currentVersion: widget.serverVersion, - referenceVersion: 'v0.107.28', - referenceVersionBeta: 'v0.108.0-b.33' - ) == true - ? await serversProvider.apiClient!.getQueryLogInfo() - : await serversProvider.apiClient!.getQueryLogInfoLegacy(); - - 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 = serverVersionIsAhead( - currentVersion: widget.serverVersion, - referenceVersion: 'v0.107.28', - referenceVersionBeta: 'v0.108.0-b.33' - ) == true ? [ - { - 'label': AppLocalizations.of(widget.context)!.hours6, - 'value': 21600000 - }, - { - 'label': AppLocalizations.of(widget.context)!.hours24, - 'value': 86400000 - }, - { - 'label': AppLocalizations.of(widget.context)!.days7, - 'value': 604800000 - }, - { - 'label': AppLocalizations.of(widget.context)!.days30, - 'value': 2592000000 - }, - { - 'label': AppLocalizations.of(widget.context)!.days90, - 'value': 7776000000 - }, - ] : [ - { - '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 Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - const CircularProgressIndicator(), - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Text( - AppLocalizations.of(context)!.loadingLogsSettings, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant - ), - ), - ) - ], - ), - ); - - case 1: - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: SingleChildScrollView( - child: Wrap( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( - children: [ - Padding( - padding: const EdgeInsets.only(top: 24), - child: Icon( - Icons.settings, - size: 24, - color: Theme.of(context).listTileTheme.iconColor - ), - ), - const SizedBox(height: 16), - Text( - AppLocalizations.of(context)!.logsSettings, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSurface - ), - ), - const SizedBox(height: 16), - ], - ), - ], - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Material( - color: Theme.of(context).colorScheme.primary.withOpacity(0.1), - borderRadius: BorderRadius.circular(28), - child: InkWell( - onTap: () => 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), - ) - ], - ), - ), - ), - ), - ), - Container(height: 16), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 14), - child: Column( - children: [ - 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), - ) - ], - ), - ), - ), - ), - Container(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 - ), - ) - ), - ], - ) - ], - ), - ), - if (Platform.isIOS) const SizedBox(height: 16) - ], - ); - - case 2: - return Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.error, - color: Colors.red, - size: 50, - ), - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Text( - AppLocalizations.of(context)!.logSettingsNotLoaded, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant - ), - ), - ) - ], - ); - - default: - return const SizedBox(); - } - } - - if (widget.dialog == true) { - return Dialog( - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 500 - ), - child: generateBody() - ), - ); - } - else { - return Container( - height: Platform.isIOS ? 436 : 420, - decoration: BoxDecoration( - 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 5251d9a..0000000 --- a/lib/screens/logs/logs_filters_modal.dart +++ /dev/null @@ -1,322 +0,0 @@ -// 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/clients_modal.dart'; -import 'package:adguard_home_manager/screens/logs/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({ - Key? key, - required this.dialog - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final logsProvider = Provider.of(context); - - return LogsFiltersModalWidget( - logsProvider: logsProvider, - dialog: dialog, - ); - } -} - -class LogsFiltersModalWidget extends StatefulWidget { - final LogsProvider logsProvider; - final bool dialog; - - const LogsFiltersModalWidget({ - Key? key, - required this.logsProvider, - required this.dialog - }) : super(key: key); - - @override - State createState() => _LogsFiltersModalWidgetState(); -} - -class _LogsFiltersModalWidgetState extends State { - TextEditingController searchController = TextEditingController(); - - @override - void initState() { - searchController.text = widget.logsProvider.searchText ?? ''; - super.initState(); - } - - @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, - builder: (context) => FilterStatusModal( - value: logsProvider.selectedResultStatus, - dialog: false, - ), - isScrollControlled: true, - backgroundColor: Colors.transparent - ); - } - } - - void openSelectClients() { - if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) { - showDialog( - context: context, - builder: (context) => ClientsModal( - value: logsProvider.selectedClients, - dialog: true, - ), - barrierDismissible: false - ); - } - else { - showModalBottomSheet( - context: context, - builder: (context) => ClientsModal( - value: logsProvider.selectedClients, - dialog: false, - ), - isScrollControlled: true, - backgroundColor: Colors.transparent - ); - } - } - - Widget content() { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: SingleChildScrollView( - child: Wrap( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( - children: [ - Padding( - padding: const EdgeInsets.only( - 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: searchController, - onChanged: logsProvider.setSearchText, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.search_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - labelText: AppLocalizations.of(context)!.search, - suffixIcon: IconButton( - onPressed: () { - setState(() { - searchController.text = ''; - }); - logsProvider.setSearchText(null); - }, - icon: const Icon(Icons.clear) - ), - ), - ), - ) - ], - ), - ), - Container(height: 16), - CustomListTile( - title: AppLocalizations.of(context)!.client, - subtitle: logsProvider.selectedClients != null - ? "${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("all") - ), - FilterChip( - selected: logsProvider.selectedResultStatus == "processed" || - logsProvider.selectedResultStatus == "whitelisted", - label: Text(AppLocalizations.of(context)!.allowed), - onSelected: (_) => logsProvider.setSelectedResultStatus("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("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: () { - searchController.text = ""; - 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) - ], - ); - } - - if (widget.dialog == true) { - return Padding( - padding: MediaQuery.of(context).viewInsets, - child: Dialog( - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 500 - ), - child: content() - ) - ), - ); - } - else { - return Padding( - padding: MediaQuery.of(context).viewInsets, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).dialogBackgroundColor, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28) - ) - ), - child: content() - ), - ); - } - } -} \ 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..ced94ca --- /dev/null +++ b/lib/screens/logs/logs_list.dart @@ -0,0 +1,287 @@ +// 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 { + 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) { + showSnacbkar( + 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) { + showSnacbkar( + 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 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, + ) + ) + ); + } + 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..8aeb752 --- /dev/null +++ b/lib/screens/logs/logs_list_appbar.dart @@ -0,0 +1,310 @@ +// 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/logs_filters_modal.dart'; +import 'package:adguard_home_manager/screens/logs/configuration/logs_config_modal.dart'; + +import 'package:adguard_home_manager/classes/process_modal.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; +import 'package:adguard_home_manager/functions/desktop_mode.dart'; +import 'package:adguard_home_manager/functions/snackbar.dart'; +import 'package:adguard_home_manager/models/applied_filters.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; +import 'package:adguard_home_manager/providers/status_provider.dart'; +import 'package:adguard_home_manager/providers/logs_provider.dart'; + +class LogsListAppBar extends StatelessWidget { + 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 statusProvider = Provider.of(context); + final serversProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + + final width = MediaQuery.of(context).size.width; + + void updateConfig(Map data) async { + ProcessModal processModal = ProcessModal(); + processModal.open(AppLocalizations.of(context)!.updatingSettings); + + final result = await serversProvider.apiClient2!.updateQueryLogParameters(data: data); + + processModal.close(); + + if (result.successful == true) { + showSnacbkar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.logsConfigUpdated, + color: Colors.green + ); + } + else { + showSnacbkar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.logsConfigNotUpdated, + color: Colors.red + ); + } + } + + void clearQueries() async { + ProcessModal processModal = ProcessModal(); + processModal.open(AppLocalizations.of(context)!.updatingSettings); + + final result = await serversProvider.apiClient2!.clearLogs(); + + processModal.close(); + + if (result.successful == true) { + showSnacbkar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.logsCleared, + color: Colors.green + ); + } + else { + showSnacbkar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.logsNotCleared, + color: Colors.red + ); + } + } + + + void openFilersModal() { + if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + 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 + ); + } + } + + 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), + expandedHeight: logsProvider.appliedFilters.searchText != null || logsProvider.appliedFilters.selectedResultStatus != 'all' || logsProvider.appliedFilters.clients != null + ? 170 : null, + actions: [ + if (!(Platform.isAndroid || Platform.isIOS)) IconButton( + onPressed: () => logsProvider.fetchLogs(inOffset: 0), + icon: const Icon(Icons.refresh_rounded), + tooltip: AppLocalizations.of(context)!.refresh, + ), + logsProvider.loadStatus == LoadStatus.loaded + ? IconButton( + onPressed: openFilersModal, + icon: const Icon(Icons.filter_list_rounded), + tooltip: AppLocalizations.of(context)!.filters, + ) + : const SizedBox(), + if (statusProvider.serverStatus != null) IconButton( + tooltip: AppLocalizations.of(context)!.settings, + onPressed: () => { + if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) { + showDialog( + context: context, + builder: (context) => LogsConfigModal( + context: context, + onConfirm: updateConfig, + onClear: clearQueries, + dialog: true, + serverVersion: statusProvider.serverStatus!.serverVersion, + ), + barrierDismissible: false + ) + } + else { + showModalBottomSheet( + context: context, + useRootNavigator: true, + builder: (context) => LogsConfigModal( + context: context, + onConfirm: updateConfig, + onClear: clearQueries, + dialog: false, + serverVersion: statusProvider.serverStatus!.serverVersion, + ), + backgroundColor: Colors.transparent, + isScrollControlled: true + ) + } + }, + icon: const Icon(Icons.settings) + ), + const SizedBox(width: 5), + ], + bottom: logsProvider.appliedFilters.searchText != null || logsProvider.appliedFilters.selectedResultStatus != 'all' || logsProvider.appliedFilters.clients != null + ? 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 != 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, + searchText: logsProvider.appliedFilters.searchText, + clients: null + ) + ); + logsProvider.setSelectedClients(null); + logsProvider.fetchLogs( + inOffset: 0, + responseStatus: logsProvider.appliedFilters.selectedResultStatus + ); + }, + ), + ], + const SizedBox(width: 15), + ], + ), + ) + ) + : null, + ); + } +} \ No newline at end of file diff --git a/lib/screens/servers/servers.dart b/lib/screens/servers/servers.dart index 8a557fb..25bca06 100644 --- a/lib/screens/servers/servers.dart +++ b/lib/screens/servers/servers.dart @@ -6,9 +6,8 @@ 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/version_warning_modal.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'; @@ -17,9 +16,9 @@ class Servers extends StatefulWidget { final double? breakingWidth; const Servers({ - Key? key, + super.key, this.breakingWidth - }) : super(key: key); + }); @override State createState() => _ServersState(); @@ -69,37 +68,7 @@ class _ServersState extends State { void openAddServerModal() async { await Future.delayed(const Duration(seconds: 0), (() => { - if (width > 700) { - showDialog( - context: context, - barrierDismissible: false, - builder: (context) => AddServerModal( - window: true, - onUnsupportedVersion: (version) => showDialog( - context: context, - builder: (ctx) => VersionWarningModal( - version: version - ), - barrierDismissible: false - ), - ), - ) - } - else { - Navigator.push(context, MaterialPageRoute( - fullscreenDialog: true, - builder: (BuildContext context) => AddServerModal( - window: false, - onUnsupportedVersion: (version) => showDialog( - context: context, - builder: (ctx) => VersionWarningModal( - version: version - ), - barrierDismissible: false - ), - ) - )) - } + openServerFormModal(context: context, width: width) })); } @@ -108,29 +77,31 @@ class _ServersState extends State { title: Text(AppLocalizations.of(context)!.servers), centerTitle: false, ), - body: 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), + 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 beb34fd..9c1e986 100644 --- a/lib/screens/settings/access_settings/access_settings.dart +++ b/lib/screens/settings/access_settings/access_settings.dart @@ -6,126 +6,67 @@ 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/functions/desktop_mode.dart'; import 'package:adguard_home_manager/constants/enums.dart'; import 'package:adguard_home_manager/providers/clients_provider.dart'; class AccessSettings extends StatefulWidget { - const AccessSettings({Key? key}) : super(key: key); + const AccessSettings({super.key}); @override State createState() => _AccessSettingsState(); } class _AccessSettingsState extends State with TickerProviderStateMixin { - final ScrollController scrollController = ScrollController(); - late TabController tabController; + late ScrollController _scrollController; + late TabController _tabController; @override void initState() { 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 clientsProvider = Provider.of(context); - - Widget body() { - return TabBarView( - controller: tabController, - children: [ - ClientsList( - type: 'allowed', - scrollController: scrollController, - loadStatus: clientsProvider.loadStatus, - data: clientsProvider.loadStatus == LoadStatus.loaded - ? clientsProvider.clients!.clientsAllowedBlocked!.allowedClients : [], - ), - ClientsList( - type: 'disallowed', - scrollController: scrollController, - loadStatus: clientsProvider.loadStatus, - data: clientsProvider.loadStatus == LoadStatus.loaded - ? clientsProvider.clients!.clientsAllowedBlocked!.disallowedClients : [], - ), - ClientsList( - type: 'domains', - scrollController: scrollController, - loadStatus: clientsProvider.loadStatus, - data: clientsProvider.loadStatus == LoadStatus.loaded - ? clientsProvider.clients!.clientsAllowedBlocked!.blockedHosts : [], - ), - ] - ); - } - - PreferredSizeWidget tabBar() { - return TabBar( - controller: tabController, - isScrollable: true, - unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant, - tabs: [ - Tab( - child: Row( - children: [ - const Icon(Icons.check), - const SizedBox(width: 8), - Text(AppLocalizations.of(context)!.allowedClients) - ], - ), - ), - Tab( - child: Row( - children: [ - const Icon(Icons.block), - const SizedBox(width: 8), - Text(AppLocalizations.of(context)!.disallowedClients) - ], - ), - ), - Tab( - child: Row( - children: [ - const Icon(Icons.link_rounded), - const SizedBox(width: 8), - Text(AppLocalizations.of(context)!.disallowedDomains) - ], - ), - ), - ] - ); - } + final width = MediaQuery.of(context).size.width; if (Platform.isAndroid || Platform.isIOS) { return Scaffold( body: DefaultTabController( length: 3, child: NestedScrollView( - controller: scrollController, + 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, - bottom: tabBar() + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, + bottom: _Tabs(tabController: _tabController) ), ), ) ]; }), - body: body() + body: _TabsView( + tabController: _tabController, + scrollController: _scrollController + ) ) ), ); @@ -135,10 +76,108 @@ class _AccessSettingsState extends State with TickerProviderStat appBar: AppBar( title: Text(AppLocalizations.of(context)!.accessSettings), centerTitle: false, - bottom: tabBar() + bottom: PreferredSize( + preferredSize: const Size(double.maxFinite, 50), + child: _Tabs(tabController: _tabController) + ) ), - body: body(), + 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 14ba98a..bb0385a 100644 --- a/lib/screens/settings/access_settings/add_client_modal.dart +++ b/lib/screens/settings/access_settings/add_client_modal.dart @@ -1,162 +1,23 @@ 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, required this.dialog, - }) : super(key: key); + }); - @override - State createState() => _AddClientModalState(); -} - -class _AddClientModalState extends State { - TextEditingController fieldController = TextEditingController(); - - bool validData = false; - - void checkValidValues() { - if (fieldController.text != '') { - setState(() => validData = true); - } - else { - setState(() => validData = false); - } - } - @override Widget build(BuildContext context) { - IconData icon() { - switch (widget.type) { - case 'allowed': - return Icons.check; - - case 'disallowed': - return Icons.block; - - case 'domains': - return Icons.link_rounded; - - default: - return Icons.check; - } - } - - String title() { - switch (widget.type) { - case 'allowed': - return AppLocalizations.of(context)!.allowClient; - - case 'disallowed': - return AppLocalizations.of(context)!.disallowClient; - - case 'domains': - return AppLocalizations.of(context)!.disallowedDomains; - - default: - return ""; - } - } - - Widget content() { - return Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: SingleChildScrollView( - child: Wrap( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - icon(), - size: 24, - color: Theme.of(context).listTileTheme.iconColor - ), - ], - ), - 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 - ), - ), - ], - ), - ), - TextFormField( - controller: fieldController, - onChanged: (_) => checkValidValues(), - decoration: InputDecoration( - prefixIcon: const Icon(Icons.link_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10) - ) - ), - helperText: widget.type == 'allowed' || widget.type == 'disallowed' - ? AppLocalizations.of(context)!.addClientFieldDescription : null, - labelText: widget.type == 'allowed' || widget.type == 'disallowed' - ? AppLocalizations.of(context)!.clientIdentifier - : AppLocalizations.of(context)!.domain, - ), - ), - ], - ), - ), - ), - 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 - ), - ) - ), - ], - ), - ), - ], - ), - ); - } - - if (widget.dialog == true) { + if (dialog == true) { return Padding( padding: MediaQuery.of(context).viewInsets, child: Dialog( @@ -164,7 +25,10 @@ class _AddClientModalState extends State { constraints: const BoxConstraints( maxWidth: 400 ), - child: content() + child: _Content( + type: type, + onConfirm: onConfirm, + ) ), ), ); @@ -180,9 +44,165 @@ class _AddClientModalState extends State { topRight: Radius.circular(28) ) ), - child: content() + child: SafeArea( + child: _Content( + type: type, + onConfirm: onConfirm, + ), + ) ), ); } } +} + +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() { + if (fieldController.text != '') { + setState(() => validData = true); + } + else { + setState(() => validData = false); + } + } + + @override + Widget build(BuildContext context) { + IconData icon() { + switch (widget.type) { + case AccessSettingsList.allowed: + return Icons.check; + + case AccessSettingsList.disallowed: + return Icons.block; + + case AccessSettingsList.domains: + return Icons.link_rounded; + + default: + return Icons.check; + } + } + + String title() { + switch (widget.type) { + case AccessSettingsList.allowed: + return AppLocalizations.of(context)!.allowClient; + + case AccessSettingsList.disallowed: + return AppLocalizations.of(context)!.disallowClient; + + case AccessSettingsList.domains: + return AppLocalizations.of(context)!.disallowedDomains; + + default: + return ""; + } + } + + return Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: SingleChildScrollView( + child: Wrap( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icon(), + size: 24, + color: Theme.of(context).listTileTheme.iconColor + ), + ], + ), + 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 + ), + ), + ], + ), + ), + TextFormField( + controller: fieldController, + onChanged: (_) => checkValidValues(), + decoration: InputDecoration( + prefixIcon: const Icon(Icons.link_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + helperText: widget.type == AccessSettingsList.allowed || widget.type == AccessSettingsList.disallowed + ? AppLocalizations.of(context)!.addClientFieldDescription : null, + labelText: widget.type == AccessSettingsList.allowed || widget.type == AccessSettingsList.disallowed + ? AppLocalizations.of(context)!.clientIdentifier + : AppLocalizations.of(context)!.domain, + ), + ), + ], + ), + ), + ), + 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 + ), + ) + ), + ], + ), + ), + ], + ), + ); + } } \ No newline at end of file diff --git a/lib/screens/settings/access_settings/clients_list.dart b/lib/screens/settings/access_settings/clients_list.dart index 2466e11..0b83a4d 100644 --- a/lib/screens/settings/access_settings/clients_list.dart +++ b/lib/screens/settings/access_settings/clients_list.dart @@ -8,7 +8,7 @@ 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'; @@ -18,18 +18,18 @@ 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 LoadStatus loadStatus; final List data; const ClientsList({ - Key? key, + super.key, required this.type, required this.scrollController, required this.loadStatus, required this.data, - }) : super(key: key); + }); @override State createState() => _ClientsListState(); @@ -70,7 +70,7 @@ class _ClientsListState extends State { Future refetchClients() async { final result = await clientsProvider.fetchClients(); - if (result == false) { + if (result == false && mounted) { showSnacbkar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.clientsNotLoaded, @@ -79,38 +79,38 @@ class _ClientsListState extends State { } } - void confirmRemoveItem(String client, String type) async { + void confirmRemoveItem(String client, AccessSettingsList type) async { Map> body = { "allowed_clients": clientsProvider.clients!.clientsAllowedBlocked?.allowedClients ?? [], "disallowed_clients": clientsProvider.clients!.clientsAllowedBlocked?.disallowedClients ?? [], "blocked_hosts": clientsProvider.clients!.clientsAllowedBlocked?.blockedHosts ?? [], }; - if (type == 'allowed') { + if (type == AccessSettingsList.allowed) { body['allowed_clients'] = body['allowed_clients']!.where((c) => c != client).toList(); } - else if (type == 'disallowed') { + else if (type == AccessSettingsList.disallowed) { body['disallowed_clients'] = body['disallowed_clients']!.where((c) => c != client).toList(); } - else if (type == 'domains') { + 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 clientsProvider.removeClientList(client, type); processModal.close(); - if (result['success'] == true) { + if (result.successful == true) { showSnacbkar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.clientRemovedSuccessfully, color: Colors.green ); } - else if (result['success'] == false && result['error'] == 'client_another_list') { + else if (result.successful == false && result.content == 'client_another_list') { showSnacbkar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.clientAnotherList, @@ -120,7 +120,7 @@ class _ClientsListState extends State { else { showSnacbkar( 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 @@ -128,22 +128,22 @@ class _ClientsListState extends State { } } - void confirmAddItem(String item, String type) async { - ProcessModal processModal = ProcessModal(context: context); + 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['success'] == true) { + if (result.successful == true) { showSnacbkar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.clientAddedSuccessfully, color: Colors.green ); } - else if (result['success'] == false && result['error'] == 'client_another_list') { + else if (result.successful == false && result.content == 'client_another_list') { showSnacbkar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.clientAnotherList, @@ -153,7 +153,7 @@ class _ClientsListState extends State { else { showSnacbkar( 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 @@ -163,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: @@ -179,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: @@ -361,6 +361,7 @@ class _ClientsListState extends State { else { showModalBottomSheet( context: context, + useRootNavigator: true, builder: (context) => AddClientModal( type: widget.type, onConfirm: confirmAddItem, diff --git a/lib/screens/settings/advanced_setings.dart b/lib/screens/settings/advanced_setings.dart index 66ff49b..9483cd8 100644 --- a/lib/screens/settings/advanced_setings.dart +++ b/lib/screens/settings/advanced_setings.dart @@ -6,21 +6,25 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/widgets/custom_list_tile.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); + 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) { showSnacbkar( appConfigProvider: appConfigProvider, @@ -40,32 +44,35 @@ 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: (value) => updateSettings( - newStatus: value, + 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 + ) ), - onTap: () => updateSettings( - newStatus: !appConfigProvider.overrideSslCheck, - function: appConfigProvider.setOverrideSslCheck - ), - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - left: 20, - right: 10 - ) - ), - ], + ], + ), ) ); } diff --git a/lib/screens/settings/customization/customization.dart b/lib/screens/settings/customization/customization.dart index 98a7121..fb4526b 100644 --- a/lib/screens/settings/customization/customization.dart +++ b/lib/screens/settings/customization/customization.dart @@ -1,4 +1,3 @@ -import 'package:adguard_home_manager/functions/generate_color_translation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -9,11 +8,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) { @@ -56,157 +57,162 @@ 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.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); + } + ); + } + }, ), ), + Padding( + padding: const EdgeInsets.only( + left: 25, + top: 10 + ), + child: Text( + colorTranslation(context, selectedColor), + style: TextStyle( + color: Theme.of(context).listTileTheme.iconColor, + fontSize: 16 + ), + ), + ) + ], + CustomSwitchListTile( + value: useThemeColorInsteadGreenRed, + onChanged: (value) { + setState(() => useThemeColorInsteadGreenRed = value); + appConfigProvider.setUseThemeColorForStatus(value); + }, + title: AppLocalizations.of(context)!.useThemeColorStatus, + subtitle: AppLocalizations.of(context)!.useThemeColorStatusDescription, ) ], - 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/dhcp/add_static_lease_modal.dart b/lib/screens/settings/dhcp/add_static_lease_modal.dart index a7f2f5f..c80e848 100644 --- a/lib/screens/settings/dhcp/add_static_lease_modal.dart +++ b/lib/screens/settings/dhcp/add_static_lease_modal.dart @@ -3,21 +3,60 @@ 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, required this.dialog - }) : super(key: key); + }); @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(); @@ -67,20 +106,20 @@ class _AddStaticLeaseModalState extends State { @override Widget build(BuildContext context) { - Widget content() { - 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: [ - Column( + 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), @@ -101,139 +140,113 @@ class _AddStaticLeaseModalState extends State { ), ], ), - ], - ), - ), - 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, ), - ), + ], ), - 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, - ), - ), - ), - Padding( - padding: const EdgeInsets.only( - left: 24, right: 24, top: 12 - ), - 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.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 + 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) ) - ); - } - : null, - child: Text( - AppLocalizations.of(context)!.confirm, - style: TextStyle( - color: validData == true - ? Theme.of(context).colorScheme.primary - : Colors.grey + ), + errorText: macError, + labelText: AppLocalizations.of(context)!.macAddress, + ), + ), + ), + 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, + ), + ), + ), + Padding( + padding: const EdgeInsets.only( + left: 24, right: 24, top: 12 + ), + 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, ), ), ), ], ), - ) - ], - ); - } - - if (widget.dialog == true) { - return Dialog( - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 400 ), - child: content(), ), - ); - } - else { - return Padding( - padding: MediaQuery.of(context).viewInsets, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).dialogBackgroundColor, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28) - ) + 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 + ), + ), + ), + ], ), - child: content() - ), - ); - } + ) + ], + ); } } \ No newline at end of file diff --git a/lib/screens/settings/dhcp/dhcp.dart b/lib/screens/settings/dhcp/dhcp.dart index a93a338..75d1856 100644 --- a/lib/screens/settings/dhcp/dhcp.dart +++ b/lib/screens/settings/dhcp/dhcp.dart @@ -7,11 +7,14 @@ import 'package:flutter_split_view/flutter_split_view.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:adguard_home_manager/widgets/section_label.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/functions/snackbar.dart'; import 'package:adguard_home_manager/constants/enums.dart'; import 'package:adguard_home_manager/providers/dhcp_provider.dart'; @@ -21,7 +24,7 @@ import 'package:adguard_home_manager/providers/app_config_provider.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart'; class DhcpScreen extends StatefulWidget { - const DhcpScreen({Key? key}) : super(key: key); + const DhcpScreen({super.key}); @override State createState() => _DhcpScreenState(); @@ -54,24 +57,25 @@ class _DhcpScreenState extends State { void loadDhcpStatus() async { final result = await Provider.of(context, listen: false).loadDhcpStatus(); - if (mounted && result == true) { - final dhcpProvider = Provider.of(context, listen: false); - if (dhcpProvider.dhcp != null) { - 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(); - } - } - }); + if (!mounted || result == false) return; + + 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(); + } } - } + }); + checkDataValid(); } @@ -191,10 +195,9 @@ class _DhcpScreenState extends State { 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 serversProvider.apiClient!.saveDhcpConfig( + final result = await serversProvider.apiClient2!.saveDhcpConfig( data: { "enabled": enabled, "interface_name": selectedInterface!.name, @@ -212,10 +215,9 @@ class _DhcpScreenState extends State { } } ); - + if (!mounted) return; processModal.close(); - - if (result['result'] == 'success') { + if (result.successful == true) { showSnacbkar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.settingsSaved, @@ -233,16 +235,13 @@ class _DhcpScreenState 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 serversProvider.apiClient!.resetDhcpConfig(); - + final result = await serversProvider.apiClient2!.resetDhcpConfig(); + if (!mounted) return; processModal.close(); - - if (result['result'] == 'success') { + if (result.successful == true) { clearAll(); - showSnacbkar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.configRestored, @@ -261,17 +260,17 @@ class _DhcpScreenState 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 serversProvider.apiClient!.restoreAllLeases(); + final result = await serversProvider.apiClient2!.restoreAllLeases(); processModal.close(); - if (result['result'] == 'success') { + if (result.successful == true) { DhcpModel data = dhcpProvider.dhcp!; - data.dhcpStatus.staticLeases = []; - data.dhcpStatus.leases = []; + data.dhcpStatus!.staticLeases = []; + data.dhcpStatus!.leases = []; dhcpProvider.setDhcpData(data); showSnacbkar( @@ -337,6 +336,7 @@ class _DhcpScreenState extends State { else { showModalBottomSheet( context: context, + useRootNavigator: true, builder: (context) => SelectInterfaceModal( interfaces: dhcpProvider.dhcp!.networkInterfaces, onSelect: (i) => setState(() { @@ -345,504 +345,27 @@ class _DhcpScreenState extends State { }), dialog: false, ), - isScrollControlled: true + isScrollControlled: true, + useSafeArea: true, + backgroundColor: Colors.transparent ); } }); } - Widget generateBody() { - 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, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ) - ], - ), - ); - - case LoadStatus.loaded: - if (selectedInterface != null) { - return SingleChildScrollView( - child: Wrap( - 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, - ), - ], - ), - ), - ), - ), - ), - if (selectedInterface!.ipv4Addresses.isNotEmpty) ...[ - SectionLabel( - label: AppLocalizations.of(context)!.ipv4settings, - padding: const EdgeInsets.only( - top: 24, left: 16, right: 16, bottom: 8 - ) - ), - FractionallySizedBox( - widthFactor: width > 900 ? 0.5 : 1, - child: Padding( - padding: width > 900 - ? const EdgeInsets.only(top: 12, bottom: 12, left: 16, right: 8) - : const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: TextFormField( - controller: ipv4StartRangeController, - onChanged: (value) => validateIpV4(value, 'ipv4StartRangeError', AppLocalizations.of(context)!.ipNotValid), - 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, - ), - ), - ), - FractionallySizedBox( - widthFactor: width > 900 ? 0.5 : 1, - child: Padding( - padding: width > 900 - ? const EdgeInsets.only(top: 12, bottom: 12, left: 8, right: 16) - : const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: TextFormField( - controller: ipv4EndRangeController, - onChanged: (value) => validateIpV4(value, 'ipv4EndRangeError', AppLocalizations.of(context)!.ipNotValid), - 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, - ), - ), - ), - FractionallySizedBox( - widthFactor: width > 900 ? 0.5 : 1, - child: Padding( - padding: width > 900 - ? const EdgeInsets.only(top: 12, bottom: 12, left: 16, right: 8) - : const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: TextFormField( - controller: ipv4SubnetMaskController, - onChanged: (value) => validateIpV4(value, 'ipv4SubnetMaskError', AppLocalizations.of(context)!.subnetMaskNotValid), - 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, - ), - ), - ), - FractionallySizedBox( - widthFactor: width > 900 ? 0.5 : 1, - child: Padding( - padding: width > 900 - ? const EdgeInsets.only(top: 12, bottom: 12, left: 8, right: 16) - : const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: TextFormField( - controller: ipv4GatewayController, - onChanged: (value) => validateIpV4(value, 'ipv4GatewayError', AppLocalizations.of(context)!.gatewayNotValid), - 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, - ), - ), - ), - FractionallySizedBox( - widthFactor: 1, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - 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: const EdgeInsets.all(16) - ), - FractionallySizedBox( - widthFactor: width > 900 ? 0.5 : 1, - child: Padding( - padding: width > 900 - ? const EdgeInsets.only(top: 8, bottom: 12, left: 16, right: 8) - : const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: TextFormField( - controller: ipv6StartRangeController, - onChanged: (value) => validateIpV4(value, 'ipv6StartRangeError', AppLocalizations.of(context)!.ipNotValid), - 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, - ), - ), - ), - FractionallySizedBox( - widthFactor: width > 900 ? 0.5 : 1, - child: Padding( - padding: width > 900 - ? const EdgeInsets.only(top: 8, bottom: 12, left: 8, right: 16) - : const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: TextFormField( - controller: ipv6EndRangeController, - onChanged: (value) => validateIpV4(value, 'ipv6EndRangeError', AppLocalizations.of(context)!.ipNotValid), - 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, - ), - ), - ), - FractionallySizedBox( - widthFactor: 1, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - 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, - padding: const EdgeInsets.all(16), - ), - if (width <= 900) Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - Navigator.push(context, 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.push(context, 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)) { - SplitView.of(context).push( - DhcpLeases( - items: dhcpProvider.dhcp!.dhcpStatus.leases, - staticLeases: false, - ) - ); - } - else { - Navigator.push(context, 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)) { - SplitView.of(context).push( - DhcpLeases( - items: dhcpProvider.dhcp!.dhcpStatus.staticLeases, - staticLeases: true, - ) - ); - } - else { - Navigator.push(context, 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(); - } + if ( + dhcpProvider.loadStatus == LoadStatus.loaded && + dhcpProvider.dhcp != null && + dhcpProvider.dhcp!.dhcpAvailable == false + ) { + 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 @@ -888,7 +411,376 @@ class _DhcpScreenState 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)) { + SplitView.of(context).push( + 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)) { + SplitView.of(context).push( + 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 3763704..9bb1fe3 100644 --- a/lib/screens/settings/dhcp/dhcp_leases.dart +++ b/lib/screens/settings/dhcp/dhcp_leases.dart @@ -2,6 +2,7 @@ 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'; @@ -21,10 +22,10 @@ class DhcpLeases extends StatelessWidget { final bool staticLeases; const DhcpLeases({ - Key? key, + super.key, required this.items, required this.staticLeases, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -34,7 +35,7 @@ class DhcpLeases extends StatelessWidget { 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 dhcpProvider.deleteLease(lease); @@ -58,28 +59,28 @@ 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 dhcpProvider.createLease(lease); processModal.close(); - if (result['success'] == true) { + if (result.successful == true) { showSnacbkar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.staticLeaseCreated, color: Colors.green ); } - else if (result['success'] == false && result['error'] == 'already_exists' ) { + else if (result.successful == false && result.content == "already_exists") { showSnacbkar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.staticLeaseExists, color: Colors.red ); } - else if (result['success'] == false && result['error'] == 'server_not_configured' ) { + else if (result.successful == false && result.content == "server_not_configured") { showSnacbkar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.serverNotConfigured, @@ -108,6 +109,7 @@ class DhcpLeases extends StatelessWidget { else { showModalBottomSheet( context: context, + useRootNavigator: true, builder: (context) => AddStaticLeaseModal( onConfirm: createLease, dialog: false, @@ -120,6 +122,7 @@ class DhcpLeases extends StatelessWidget { return Scaffold( appBar: AppBar( + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, title: Text( staticLeases == true ? AppLocalizations.of(context)!.dhcpStatic @@ -127,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 01b7e06..a9c9c67 100644 --- a/lib/screens/settings/dhcp/select_interface_modal.dart +++ b/lib/screens/settings/dhcp/select_interface_modal.dart @@ -3,6 +3,8 @@ 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/models/dhcp.dart'; class SelectInterfaceModal extends StatelessWidget { @@ -11,213 +13,147 @@ class SelectInterfaceModal extends StatelessWidget { final bool dialog; const SelectInterfaceModal({ - Key? key, + super.key, required this.interfaces, required this.onSelect, required this.dialog - }) : super(key: key); + }); @override Widget build(BuildContext context) { - Widget content() { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: SingleChildScrollView( - child: Wrap( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( - children: [ - Padding( - padding: const EdgeInsets.only(top: 24), - child: Icon( + 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( - AppLocalizations.of(context)!.selectInterface, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSurface - ), - ), - ], - ), - ], + 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), + ], + ), + ), + 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) + ], + ), + ), + ); + } + else { + return GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: DraggableScrollableSheet( + initialChildSize: 0.6, + minChildSize: 0.3, + maxChildSize: 1, + builder: (context, controller) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28), + ), + ), + child: Column( + children: [ + Container( + margin: const EdgeInsets.all(16), + width: 36, + height: 4, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Colors.grey + ), ), - 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, - children: [ - Text( - interfaces[index].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( - 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 != null && 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: [ - Flexible( - child: Text( - "${AppLocalizations.of(context)!.ipv4addresses}: ${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: [ - Flexible( - child: Text( - "${AppLocalizations.of(context)!.ipv6addresses}: ${interfaces[index].ipv6Addresses.join(', ')}", - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.onSurfaceVariant - ), - ), - ) - ], - ), - ] - ], + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Column( + children: [ + Icon( + Icons.settings_ethernet_rounded, + size: 24, + color: Theme.of(context).listTileTheme.iconColor + ), + const SizedBox(height: 16), + Text( + AppLocalizations.of(context)!.selectInterface, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + color: Theme.of(context).colorScheme.onSurface ), ), + ], + ), + ), + Expanded( + child: SafeArea( + child: ListView.builder( + controller: controller, + itemCount: interfaces.length, + itemBuilder: (context, index) => DhcpInterfaceItem( + networkInterface: interfaces[index], + onSelect: onSelect + ) ), ) ), ], ), - ), - ), - 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) - ], - ); - } - - if (dialog == true) { - return Dialog( - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 500 - ), - child: content() + ); + }, ), ); } - else { - return Container( - decoration: BoxDecoration( - color: Theme.of(context).dialogBackgroundColor, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28) - ) - ), - child: content() - ); - } } } \ 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 055042d..c970f34 100644 --- a/lib/screens/settings/dns/bootstrap_dns.dart +++ b/lib/screens/settings/dns/bootstrap_dns.dart @@ -1,5 +1,6 @@ // 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'; @@ -10,7 +11,7 @@ import 'package:adguard_home_manager/functions/snackbar.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; class BootstrapDnsScreen extends StatefulWidget { - const BootstrapDnsScreen({Key? key}) : super(key: key); + const BootstrapDnsScreen({super.key}); @override State createState() => _BootstrapDnsScreenState(); @@ -66,8 +67,10 @@ class _BootstrapDnsScreenState extends State { 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 dnsProvider.saveBootstrapDnsConfig({ @@ -76,14 +79,14 @@ class _BootstrapDnsScreenState extends State { processModal.close(); - if (result['success'] == true) { + if (result.successful == true) { showSnacbkar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.dnsConfigSaved, color: Colors.green ); } - else if (result['success'] == false && result['error'] == 400) { + else if (result.successful == false && result.statusCode == 400) { showSnacbkar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.someValueNotValid, @@ -102,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 @@ -113,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: [ - 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) - ) + 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) + ) + ], + ), + )).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) + ), + ], + ), + const SizedBox(height: 20) + ], + ), ), ); } diff --git a/lib/screens/settings/dns/cache_config.dart b/lib/screens/settings/dns/cache_config.dart index cd2968b..40a99f8 100644 --- a/lib/screens/settings/dns/cache_config.dart +++ b/lib/screens/settings/dns/cache_config.dart @@ -7,6 +7,7 @@ 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'; @@ -15,7 +16,7 @@ import 'package:adguard_home_manager/functions/snackbar.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; class CacheConfigDnsScreen extends StatefulWidget { - const CacheConfigDnsScreen({Key? key}) : super(key: key); + const CacheConfigDnsScreen({super.key}); @override State createState() => _CacheConfigDnsScreenState(); @@ -58,7 +59,7 @@ class _CacheConfigDnsScreenState extends State { cacheSizeController.text = dnsProvider.dnsInfo!.cacheSize.toString(); overrideMinTtlController.text = dnsProvider.dnsInfo!.cacheTtlMin.toString(); overrideMaxTtlController.text = dnsProvider.dnsInfo!.cacheTtlMax.toString(); - optimisticCache = dnsProvider.dnsInfo!.cacheOptimistic; + optimisticCache = dnsProvider.dnsInfo!.cacheOptimistic ?? false; validData = true; super.initState(); } @@ -69,8 +70,10 @@ class _CacheConfigDnsScreenState extends State { 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 dnsProvider.saveCacheCacheConfig({ @@ -82,14 +85,14 @@ class _CacheConfigDnsScreenState extends State { processModal.close(); - if (result['success'] == true) { + if (result.successful == true) { showSnacbkar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.dnsConfigSaved, color: Colors.green ); } - else if (result['success'] == false && result['error'] == 400) { + else if (result.successful== false && result.statusCode == 400) { showSnacbkar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.someValueNotValid, @@ -135,7 +138,7 @@ class _CacheConfigDnsScreenState extends State { void clearCache() async { final result = await clearDnsCache(context, serversProvider.selectedServer!); - if (result == true) { + if (result.successful == true) { showSnacbkar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.dnsCacheCleared, @@ -154,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 @@ -165,81 +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: 16) - ], + ), + 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 a9dd60d..92d4c0f 100644 --- a/lib/screens/settings/dns/comment_modal.dart +++ b/lib/screens/settings/dns/comment_modal.dart @@ -1,141 +1,30 @@ 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({ - Key? key, + super.key, this.comment, required this.onConfirm, required this.dialog - }) : super(key: key); - - @override - State createState() => _CommentModalState(); -} - -class _CommentModalState extends State { - final TextEditingController commentController = TextEditingController(); - - bool validData = false; - - @override - void initState() { - if (widget.comment != null) { - commentController.text = widget.comment!.replaceFirst(RegExp(r'#(\s)?'), ""); - } - super.initState(); - } + }); @override Widget build(BuildContext context) { - Widget content() { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: SingleChildScrollView( - child: Wrap( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( - children: [ - Padding( - padding: const EdgeInsets.only(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) - ) - ), - 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 - ), - ) - ), - ], - ), - ) - ], - ); - } - - if (widget.dialog == true) { + if (dialog == true) { return Dialog( child: ConstrainedBox( constraints: const BoxConstraints( maxWidth: 400 ), - child: content() + child: _Content( + comment: comment, + onConfirm: onConfirm, + ) ), ); } @@ -150,9 +39,139 @@ class _CommentModalState extends State { ), color: Theme.of(context).dialogBackgroundColor ), - child: content() + child: SafeArea( + child: _Content( + comment: comment, + onConfirm: onConfirm, + ), + ) ), ); } } +} + +class _Content extends StatefulWidget { + final String? comment; + final void Function(String) onConfirm; + + const _Content({ + required this.comment, + required this.onConfirm + }); + + @override + State<_Content> createState() => __ContentState(); +} + +class __ContentState extends State<_Content> { + final TextEditingController commentController = TextEditingController(); + + bool validData = false; + + @override + void initState() { + if (widget.comment != null) { + commentController.text = widget.comment!.replaceFirst(RegExp(r'#(\s)?'), ""); + } + 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( + 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) + ) + ), + 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 eac3c2c..4a7c78a 100644 --- a/lib/screens/settings/dns/dns.dart +++ b/lib/screens/settings/dns/dns.dart @@ -1,14 +1,12 @@ // ignore_for_file: use_build_context_synchronously -import 'dart:io'; - -import 'package:adguard_home_manager/constants/enums.dart'; -import 'package:adguard_home_manager/providers/dns_provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_split_view/flutter_split_view.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:adguard_home_manager/screens/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'; @@ -17,13 +15,21 @@ 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'; class DnsSettings extends StatefulWidget { - const DnsSettings({Key? key}) : super(key: key); + final bool splitView; + + const DnsSettings({ + super.key, + required this.splitView, + }); @override State createState() => _DnsSettingsState(); @@ -44,109 +50,22 @@ class _DnsSettingsState extends State { final width = MediaQuery.of(context).size.width; - void navigate(Widget widget) { - if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) { - SplitView.of(context).push(widget); + void navigate(Widget w) { + if (widget.splitView) { + SplitView.of(context).push(w); } else { - Navigator.push(context, MaterialPageRoute( - builder: (context) => widget - )); - } - } - - Widget generateBody() { - 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, - ), - 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(); + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => w + ) + ); } } void clearCache() async { final result = await clearDnsCache(context, serversProvider.selectedServer!); - if (result == true) { + if (result.successful == true) { showSnacbkar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.dnsCacheCleared, @@ -165,7 +84,16 @@ class _DnsSettingsState 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( @@ -200,7 +128,104 @@ class _DnsSettingsState 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 9169408..b13f593 100644 --- a/lib/screens/settings/dns/dns_server_settings.dart +++ b/lib/screens/settings/dns/dns_server_settings.dart @@ -1,5 +1,6 @@ // 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'; @@ -8,13 +9,14 @@ 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/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 DnsServerSettingsScreen extends StatefulWidget { - const DnsServerSettingsScreen({Key? key}) : super(key: key); + const DnsServerSettingsScreen({super.key}); @override State createState() => _DnsServerSettingsScreenState(); @@ -23,7 +25,12 @@ class DnsServerSettingsScreen extends StatefulWidget { class _DnsServerSettingsScreenState extends State { final TextEditingController limitRequestsController = TextEditingController(); 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; @@ -34,6 +41,9 @@ class _DnsServerSettingsScreenState extends State { final TextEditingController ipv6controller = TextEditingController(); String? ipv6error; + final _ttlController = TextEditingController(); + String? _ttlError; + bool isDataValid = false; void validateIpv4(String value) { @@ -47,6 +57,17 @@ class _DnsServerSettingsScreenState extends State { validateData(); } + 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) { 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) { @@ -71,7 +92,9 @@ class _DnsServerSettingsScreenState extends State { ipv6controller.text != '' && ipv6error == null ) - ) == true + ) == true && + ednsIpError == null && + _ttlError == null ) { setState(() => isDataValid = true); } @@ -80,18 +103,36 @@ class _DnsServerSettingsScreenState extends State { } } + void validateNumber(String value) { + final regex = RegExp(r'^(\d)+$'); + if (regex.hasMatch(value) == true) { + setState(() => _ttlError = null); + } + else { + setState(() => _ttlError = AppLocalizations.of(context)!.invalidValue); + } + validateData(); + } + @override void initState() { 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; isDataValid = true; + _ttlController.text = dnsProvider.dnsInfo!.blockedResponseTtl != null + ? dnsProvider.dnsInfo!.blockedResponseTtl.toString() + : ""; super.initState(); } @@ -99,31 +140,35 @@ class _DnsServerSettingsScreenState extends State { 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(context: context); + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.savingConfig); final result = await dnsProvider.saveDnsServerConfig({ "ratelimit": int.parse(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 + "blocking_ipv6": ipv6controller.text, + "blocked_response_ttl": int.tryParse(_ttlController.text) }); processModal.close(); - if (result['success'] == true) { + if (result.successful == true) { showSnacbkar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.dnsConfigSaved, color: Colors.green ); } - else if (result['success'] == false && result['error'] == 400) { + else if (result.successful == false && result.statusCode == 400) { showSnacbkar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.someValueNotValid, @@ -153,6 +198,7 @@ class _DnsServerSettingsScreenState extends State { return Scaffold( appBar: AppBar( title: Text(AppLocalizations.of(context)!.dnsServerSettings), + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, actions: [ IconButton( onPressed: isDataValid == true @@ -164,141 +210,231 @@ 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: 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); + } + 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, + ), + ), + const SizedBox(height: 10), + 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: validateNumber, 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..dcae0a2 --- /dev/null +++ b/lib/screens/settings/dns/fallback_dns.dart @@ -0,0 +1,220 @@ +// 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/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) { + 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) { + setState(() => field['error'] = null); + } + else { + setState(() => field['error'] = AppLocalizations.of(context)!.invalidIp); + } + 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!) { + final controller = TextEditingController(); + controller.text = item; + fallbackControllers.add({ + 'controller': controller, + 'error': null + }); + } + 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'].text).toList(), + }); + + processModal.close(); + + if (result.successful == true) { + showSnacbkar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.dnsConfigSaved, + color: Colors.green + ); + } + else if (result.successful == false && result.statusCode == 400) { + showSnacbkar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.someValueNotValid, + color: Colors.red + ); + } + else { + showSnacbkar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.dnsConfigNotSaved, + color: Colors.red + ); + } + } + + 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: [ + 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(() => fallbackControllers = fallbackControllers.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(() => fallbackControllers.add({ + 'controller': TextEditingController(), + 'error': null + })); + checkValidValues(); + }, + icon: const Icon(Icons.add), + label: Text(AppLocalizations.of(context)!.addItem) + ), + ], + ), + 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 bf6f63b..a9419e9 100644 --- a/lib/screens/settings/dns/private_reverse_servers.dart +++ b/lib/screens/settings/dns/private_reverse_servers.dart @@ -6,13 +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/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/providers/app_config_provider.dart'; class PrivateReverseDnsServersScreen extends StatefulWidget { - const PrivateReverseDnsServersScreen({Key? key}) : super(key: key); + const PrivateReverseDnsServersScreen({super.key}); @override State createState() => _PrivateReverseDnsServersScreenState(); @@ -21,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; @@ -67,19 +63,25 @@ class _PrivateReverseDnsServersScreenState extends State(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 dnsProvider.savePrivateReverseServersConfig( @@ -107,14 +111,14 @@ 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: [ - 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( + 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, @@ -252,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/test_upstream_dns_modal.dart b/lib/screens/settings/dns/test_upstream_dns_modal.dart new file mode 100644 index 0000000..8410334 --- /dev/null +++ b/lib/screens/settings/dns/test_upstream_dns_modal.dart @@ -0,0 +1,168 @@ +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); + 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 a46dab7..e540053 100644 --- a/lib/screens/settings/dns/upstream_dns.dart +++ b/lib/screens/settings/dns/upstream_dns.dart @@ -11,12 +11,13 @@ 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/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'; class UpstreamDnsScreen extends StatefulWidget { - const UpstreamDnsScreen({Key? key}) : super(key: key); + const UpstreamDnsScreen({super.key}); @override State createState() => _UpstreamDnsScreenState(); @@ -90,6 +91,7 @@ class _UpstreamDnsScreenState extends State { else { showModalBottomSheet( context: context, + useRootNavigator: true, builder: (context) => CommentModal( onConfirm: (value) { setState(() { @@ -123,6 +125,7 @@ class _UpstreamDnsScreenState extends State { else { showModalBottomSheet( context: context, + useRootNavigator: true, builder: (context) => CommentModal( comment: item['comment'], onConfirm: (value) { @@ -138,7 +141,7 @@ class _UpstreamDnsScreenState extends State { } void saveData() async { - ProcessModal processModal = ProcessModal(context: context); + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.savingConfig); final result = await dnsProvider.saveUpstreamDnsConfig({ @@ -148,14 +151,14 @@ class _UpstreamDnsScreenState extends State { processModal.close(); - if (result['success'] == true) { + if (result.successful == true) { showSnacbkar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.dnsConfigSaved, color: Colors.green ); } - else if (result['success'] == false && result['error'] == 400) { + else if (result.successful == false && result.statusCode == 400) { showSnacbkar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.someValueNotValid, @@ -174,6 +177,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 @@ -182,132 +186,134 @@ 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: 24 - ), - 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: 20), ], ), - )).toList(), - 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) + ...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), - ), - ], + )).toList(), + 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), + ), + ], + ), ), ); } diff --git a/lib/screens/settings/dns_rewrites/dns_rewrite_modal.dart b/lib/screens/settings/dns_rewrites/dns_rewrite_modal.dart index b43c168..439455c 100644 --- a/lib/screens/settings/dns_rewrites/dns_rewrite_modal.dart +++ b/lib/screens/settings/dns_rewrites/dns_rewrite_modal.dart @@ -1,34 +1,80 @@ 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/dns_rewrites/server_version_needed.dart'; - -import 'package:adguard_home_manager/providers/status_provider.dart'; -import 'package:adguard_home_manager/functions/compare_versions.dart'; import 'package:adguard_home_manager/models/rewrite_rules.dart'; -class DnsRewriteModal extends StatefulWidget { +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({ - Key? key, + super.key, required this.onConfirm, required this.dialog, this.rule, required this.onDelete - }) : super(key: key); + }); @override - State createState() => _AddDnsRewriteModalState(); + 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 _AddDnsRewriteModalState extends State { +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(); @@ -71,181 +117,139 @@ class _AddDnsRewriteModalState extends State { @override Widget build(BuildContext context) { - final statusProvider = Provider.of(context); - - Widget content() { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: SingleChildScrollView( - child: Wrap( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( - children: [ - Padding( - padding: const EdgeInsets.only(top: 24), - child: Icon( - widget.rule != null - ? Icons.edit - : Icons.add, - size: 24, - color: Theme.of(context).listTileTheme.iconColor - ), - ), - const SizedBox(height: 16), - Text( + 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 - ? AppLocalizations.of(context)!.editRewriteRule - : AppLocalizations.of(context)!.addDnsRewrite, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSurface - ), + ? Icons.edit + : Icons.add, + size: 24, + color: Theme.of(context).listTileTheme.iconColor ), - const SizedBox(height: 16), - ], - ), - ], - ), - 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, + 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 + ), + 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, ), - 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) ), ), ), ], - ), - ), + ) + ], ), - 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 - ? () { - if (serverVersionIsAhead( - currentVersion: statusProvider.serverStatus!.serverVersion, - referenceVersion: '0.107.33', - referenceVersionBeta: '0.108.0-b.39' - )) { - Navigator.pop(context); - widget.onConfirm( - RewriteRules( - domain: domainController.text, - answer: answerController.text - ), - widget.rule - ); - } - else { - showDialog( - context: context, - builder: (context) => const ServerVersionNeeded(version: 'v0.107.33') - ); - } - } - : 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) - ], - ); - } - - if (widget.dialog == true) { - return Dialog( - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 400 - ), - child: content() ), - ); - } - else { - return Padding( - padding: MediaQuery.of(context).viewInsets, - child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28) - ), - color: Theme.of(context).dialogBackgroundColor, - ), - child: content() - ), - ); - } + 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 fda731e..301c3a1 100644 --- a/lib/screens/settings/dns_rewrites/dns_rewrites.dart +++ b/lib/screens/settings/dns_rewrites/dns_rewrites.dart @@ -11,6 +11,7 @@ import 'package:adguard_home_manager/screens/settings/dns_rewrites/delete_dns_re import 'package:adguard_home_manager/screens/settings/dns_rewrites/dns_rewrite_modal.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'; @@ -18,7 +19,7 @@ import 'package:adguard_home_manager/models/rewrite_rules.dart'; import 'package:adguard_home_manager/classes/process_modal.dart'; class DnsRewritesScreen extends StatefulWidget { - const DnsRewritesScreen({Key? key}) : super(key: key); + const DnsRewritesScreen({super.key}); @override State createState() => _DnsRewritesScreenState(); @@ -58,7 +59,7 @@ class _DnsRewritesScreenState extends State { final width = MediaQuery.of(context).size.width; void deleteDnsRewrite(RewriteRules rule) async { - ProcessModal processModal = ProcessModal(context: context); + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.deleting); final result = await rewriteRulesProvider.deleteDnsRewrite(rule); @@ -82,7 +83,7 @@ class _DnsRewritesScreenState extends State { } void addDnsRewrite(RewriteRules rule, _) async { - ProcessModal processModal = ProcessModal(context: context); + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.addingRewrite); final result = await rewriteRulesProvider.addDnsRewrite(rule); @@ -106,7 +107,7 @@ class _DnsRewritesScreenState extends State { } void updateRewriteRule(RewriteRules newRule, RewriteRules? previousRule) async { - ProcessModal processModal = ProcessModal(context: context); + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.updatingRule); final result = await rewriteRulesProvider.editDnsRewrite(newRule, previousRule!); @@ -129,240 +130,247 @@ class _DnsRewritesScreenState extends State { } } - Widget generateBody() { - 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, - 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) { - showSnacbkar( - 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, - 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, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ) - ], - ), - ); - - default: - return const SizedBox(); - } - } - return Scaffold( appBar: AppBar( title: Text(AppLocalizations.of(context)!.dnsRewrites), + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, centerTitle: false, ), - body: Stack( - children: [ - generateBody(), - 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) + 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) { + showSnacbkar( + 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 { - showModalBottomSheet( - context: context, - builder: (context) => DnsRewriteModal( - onConfirm: addDnsRewrite, - dialog: false, - onDelete: (rule) => showDialog( - context: context, - builder: (context) => DeleteDnsRewrite( - onConfirm: () => deleteDnsRewrite(rule) - ) + ); + } + 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, + ), + ) + ], ), - backgroundColor: Colors.transparent, - isScrollControlled: true - ) - } - }, - child: const Icon(Icons.add), - ), - ) - ], + ); + + default: + return const SizedBox(); + } + }, + ), + 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/rule_modal.dart b/lib/screens/settings/dns_rewrites/rule_modal.dart deleted file mode 100644 index e69de29..0000000 diff --git a/lib/screens/settings/dns_rewrites/server_version_needed.dart b/lib/screens/settings/dns_rewrites/server_version_needed.dart index 88888c6..68ea426 100644 --- a/lib/screens/settings/dns_rewrites/server_version_needed.dart +++ b/lib/screens/settings/dns_rewrites/server_version_needed.dart @@ -4,6 +4,7 @@ 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 diff --git a/lib/screens/settings/encryption/encryption.dart b/lib/screens/settings/encryption/encryption.dart index de75918..1efdad4 100644 --- a/lib/screens/settings/encryption/encryption.dart +++ b/lib/screens/settings/encryption/encryption.dart @@ -6,50 +6,32 @@ import 'package:flutter_gen/gen_l10n/app_localizations.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/desktop_mode.dart'; import 'package:adguard_home_manager/functions/base64.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'; -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; @@ -90,59 +72,58 @@ class _EncryptionSettingsWidgetState extends State { String? validDataError; int certKeyValidApi = 0; - Map? certKeyValid; + EncryptionValidation? certKeyValid; + String? encryptionResultMessage; bool formEdited = false; void fetchData({bool? showRefreshIndicator}) async { - setState(() => loadStatus = 0); + setState(() => loadStatus = LoadStatus.loading); - final result = await Provider.of(context, listen: false).apiClient!.getEncryptionSettings(); + 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()); - - if (mounted) { - 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 = decodeBase64(result['data'].certificateChain); - } - 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; - }); + 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 { - widget.appConfigProvider.addLog(result['log']); - setState(() => loadStatus = 2); - } + 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; + loadStatus = LoadStatus.loaded; + }); + } + else { + setState(() => loadStatus = LoadStatus.error); } } Future checkValidDataApi({Map? data}) async { setState(() => certKeyValidApi = 0); - final result = await Provider.of(context, listen: false).apiClient!.checkEncryptionSettings( + final result = await Provider.of(context, listen: false).apiClient2!.checkEncryptionSettings( data: data ?? { "enabled": enabled, "server_name": domainNameController.text, @@ -158,25 +139,28 @@ class _EncryptionSettingsWidgetState extends State { } ); - if (mounted) { - if (result['result'] == 'success') { + if (!mounted) return; + if (result.successful == true) { + final data = result.content as EncryptionValidationResult; + if (data.isObject == true) { + final object = data.encryptionValidation!; setState(() { - if (result['data']['warning_validation'] != null && result['data']['warning_validation'] != '') { + if (object.warningValidation != null && object.warningValidation != '') { certKeyValidApi = 2; - validDataError = result['data']['warning_validation']; + validDataError = object.warningValidation; } else { certKeyValidApi = 1; validDataError = null; } - certKeyValid = result['data']; + certKeyValid = object; }); } else { - if (result['log'].resBody != null) { - setState(() => validDataError = result['log'].resBody); - } - setState(() => certKeyValidApi = 2); + setState(() { + encryptionResultMessage = data.message; + certKeyValidApi = 2; + }); } } } @@ -224,10 +208,10 @@ class _EncryptionSettingsWidgetState extends State { 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 serversProvider.apiClient!.saveEncryptionSettings( + final result = await serversProvider.apiClient2!.saveEncryptionSettings( data: { "enabled": enabled, "server_name": domainNameController.text, @@ -245,7 +229,7 @@ class _EncryptionSettingsWidgetState extends State { processModal.close(); - if (result['result'] == 'success') { + if (result.successful == true) { showSnacbkar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.encryptionConfigSaved, @@ -253,410 +237,75 @@ class _EncryptionSettingsWidgetState extends State { ); } else { - appConfigProvider.addLog(result['log']); - showSnacbkar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.encryptionConfigNotSaved, color: Colors.red ); - if (result['log'].resBody != null) { + if (result.content != null) { showDialog( context: context, builder: (context) => ErrorMessageEncryption( - errorMessage: result['log'].resBody + 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), - 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!['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": "", + } + ); + 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) { + showSnacbkar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.configurationResetSuccessfully, + color: Colors.green + ); + } + else { + showSnacbkar( + 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 ? @@ -668,7 +317,399 @@ 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 (certKeyValidApi == 2 && (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(); + } + ), + 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/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/general_settings/general_settings.dart b/lib/screens/settings/general_settings/general_settings.dart index c5ba7cb..74cc752 100644 --- a/lib/screens/settings/general_settings/general_settings.dart +++ b/lib/screens/settings/general_settings/general_settings.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter_split_view/flutter_split_view.dart'; import 'package:provider/provider.dart'; import 'package:store_checker/store_checker.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -13,13 +14,19 @@ 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/check_app_updates.dart'; +import 'package:adguard_home_manager/functions/desktop_mode.dart'; import 'package:adguard_home_manager/functions/snackbar.dart'; import 'package:adguard_home_manager/functions/open_url.dart'; import 'package:adguard_home_manager/functions/app_update_download_link.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; class GeneralSettings extends StatefulWidget { - const GeneralSettings({Key? key}) : super(key: key); + final bool splitView; + + const GeneralSettings({ + super.key, + required this.splitView, + }); @override State createState() => _GeneralSettingsState(); @@ -34,6 +41,8 @@ class _GeneralSettingsState extends State { 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 @@ -113,148 +122,177 @@ class _GeneralSettingsState extends State { return Scaffold( appBar: AppBar( title: Text(AppLocalizations.of(context)!.generalSettings), + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, ), - body: 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, + 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 + ) ), - 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, + 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 + ) ), - 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, + 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 + ) ), - onTap: () => updateSettings( - newStatus: !appConfigProvider.hideServerAddress, - function: appConfigProvider.setHideServerAddress + CustomListTile( + icon: Icons.reorder_rounded, + title: AppLocalizations.of(context)!.topItemsOrder, + subtitle: AppLocalizations.of(context)!.topItemsOrderDescription, + onTap: () => widget.splitView == true + ? SplitView.of(context).push(const ReorderableTopItemsHome()) + : Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const ReorderableTopItemsHome() + ) + ) ), - 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: () => Navigator.push(context, MaterialPageRoute( - builder: (context) => const ReorderableTopItemsHome() - )), - ), - 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, + CustomListTile( + icon: Icons.donut_large_rounded, + title: AppLocalizations.of(context)!.showTopItemsChart, + subtitle: AppLocalizations.of(context)!.showTopItemsChartDescription, + trailing: Switch( + value: appConfigProvider.showTopItemsChart, + onChanged: (value) => updateSettings( + newStatus: value, + function: appConfigProvider.setShowTopItemsChart + ), + ), + onTap: () => updateSettings( + newStatus: !appConfigProvider.showTopItemsChart, + function: appConfigProvider.setShowTopItemsChart + ), + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + left: 16, + right: 10 + ) + ), + SectionLabel(label: AppLocalizations.of(context)!.logs), + CustomListTile( + icon: Icons.timer_rounded, + 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 + ) ), - 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, + 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 + ) ), - 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 == Source.IS_INSTALLED_FROM_LOCAL_SOURCE || - appConfigProvider.installationSource == Source.IS_INSTALLED_FROM_PLAY_PACKAGE_INSTALLER || - appConfigProvider.installationSource == Source.UNKNOWN - )) - ) ...[ - 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() - ) - ] - ], + if ( + !(Platform.isAndroid || Platform.isIOS) || + (Platform.isAndroid && ( + appConfigProvider.installationSource == Source.IS_INSTALLED_FROM_LOCAL_SOURCE || + appConfigProvider.installationSource == Source.IS_INSTALLED_FROM_PLAY_PACKAGE_INSTALLER || + appConfigProvider.installationSource == Source.UNKNOWN + )) + ) ...[ + 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() + ) + ] + ], + ), ) ); } diff --git a/lib/screens/settings/general_settings/reorderable_top_items_home.dart b/lib/screens/settings/general_settings/reorderable_top_items_home.dart index 75ad9cc..46e3a58 100644 --- a/lib/screens/settings/general_settings/reorderable_top_items_home.dart +++ b/lib/screens/settings/general_settings/reorderable_top_items_home.dart @@ -1,22 +1,23 @@ -// ignore_for_file: use_build_context_synchronously +import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:flutter_reorderable_list/flutter_reorderable_list.dart' as reorderable_list_library; +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/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/app_config_provider.dart'; -class ItemData { +class _ItemData { final HomeTopItems title; final Key key; - const ItemData({ + const _ItemData({ required this.title, required this.key }); @@ -28,7 +29,7 @@ enum DraggingMode { } class ReorderableTopItemsHome extends StatefulWidget { - const ReorderableTopItemsHome({Key? key}) : super(key: key); + const ReorderableTopItemsHome({super.key}); @override State createState() => _ReorderableTopItemsHomeState(); @@ -37,10 +38,10 @@ class ReorderableTopItemsHome extends StatefulWidget { class _ReorderableTopItemsHomeState extends State { List homeTopItemsList = []; List persistHomeTopItemsList = []; - List renderItems = []; + List<_ItemData> renderItems = []; int _indexOfKey(Key key) { - return renderItems.indexWhere((ItemData d) => d.key == key); + return renderItems.indexWhere((_ItemData d) => d.key == key); } bool _reorderCallback(Key item, Key newPosition) { @@ -78,7 +79,7 @@ class _ReorderableTopItemsHomeState extends State { homeTopItemsList = appConfigProvider.homeTopItemsOrder; persistHomeTopItemsList = appConfigProvider.homeTopItemsOrder; renderItems = appConfigProvider.homeTopItemsOrder.asMap().entries.map( - (e) => ItemData( + (e) => _ItemData( key: ValueKey(e.key), title: e.value, ) @@ -91,6 +92,8 @@ class _ReorderableTopItemsHomeState extends State { Widget build(BuildContext context) { final appConfigProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + Widget tile(HomeTopItems title) { switch (title) { case HomeTopItems.queriedDomains: @@ -114,48 +117,28 @@ class _ReorderableTopItemsHomeState extends State { 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(); } } - Future onWillPopScope() async { - if (!listEquals(appConfigProvider.homeTopItemsOrder, persistHomeTopItemsList)) { - showDialog( - context: context, - builder: (dialogContext) => AlertDialog( - title: Text(AppLocalizations.of(context)!.discardChanges), - content: Text(AppLocalizations.of(context)!.discardChangesDescription), - actions: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () { - Navigator.pop(dialogContext); - Navigator.pop(context); - }, - child: Text(AppLocalizations.of(context)!.confirm) - ), - const SizedBox(width: 8), - TextButton( - onPressed: () => Navigator.pop(dialogContext), - child: Text(AppLocalizations.of(context)!.cancel) - ), - ], - ) - ], - ) - ); - return false; - } - else { - return true; - } - } - void saveSettings() async { final result = await appConfigProvider.setHomeTopItemsOrder(homeTopItemsList); + if (!mounted) return; if (result == true) { showSnacbkar( appConfigProvider: appConfigProvider, @@ -171,131 +154,159 @@ class _ReorderableTopItemsHomeState extends State { ); } } + + final draggingMode = Platform.isAndroid + ? DraggingMode.android + : DraggingMode.iOS; - return WillPopScope( - onWillPop: onWillPopScope, - child: Scaffold( - appBar: AppBar( - 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) - ], - ), - body: Column( - 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) - ) - ], - ), - ), - ), - Expanded( - child: reorderable_list_library.ReorderableList( - onReorder: _reorderCallback, - onReorderDone: _reorderDone, - child: ListView.builder( - itemBuilder: (context, index) => reorderable_list_library.ReorderableItem( - key: renderItems[index].key, - childBuilder: (context, state) => Item( - tileWidget: tile(renderItems[index].title), - isFirst: index == 0, - isLast: index == renderItems.length - 1, - state: state - ), + return Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context)!.topItemsOrder), + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, + actions: [ + IconButton( + onPressed: !listEquals(appConfigProvider.homeTopItemsOrder, persistHomeTopItemsList) + ? () => saveSettings() + : null, + icon: const Icon(Icons.save_rounded), + tooltip: AppLocalizations.of(context)!.save, + ), + const SizedBox(width: 8) + ], + ), + body: Column( + 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, ), - itemCount: renderItems.length, - ) + const SizedBox(width: 16), + Flexible( + child: Text(AppLocalizations.of(context)!.topItemsReorderInfo) + ) + ], ), ), - ], - ), + ), + Expanded( + child: reorderable_list.ReorderableList( + onReorder: _reorderCallback, + onReorderDone: _reorderDone, + child: ListView.builder( + itemBuilder: (context, index) => reorderable_list.ReorderableItem( + key: renderItems[index].key, + childBuilder: (context, state) { + if (draggingMode == DraggingMode.android) { + return reorderable_list.DelayedReorderableListener( + child: _Tile( + draggingMode: draggingMode, + isFirst: index == 0, + isLast: index == renderItems.length - 1, + state: state, + tileWidget: tile(renderItems[index].title), + ), + ); + } + else { + return _Tile( + draggingMode: draggingMode, + isFirst: index == 0, + isLast: index == renderItems.length - 1, + state: state, + tileWidget: tile(renderItems[index].title), + ); + } + }, + ), + itemCount: renderItems.length, + ) + ), + ), + ], ), ); } } -class Item extends StatelessWidget { +class _Tile extends StatelessWidget { final Widget tileWidget; final bool isFirst; final bool isLast; - final reorderable_list_library.ReorderableItemState state; + final reorderable_list.ReorderableItemState state; + final DraggingMode draggingMode; - const Item({ - Key? key, + const _Tile({ required this.tileWidget, required this.isFirst, required this.isLast, required this.state, - }) : super(key: key); + required this.draggingMode + }); @override Widget build(BuildContext context) { - BoxDecoration decoration; - - if ( - state == reorderable_list_library.ReorderableItemState.dragProxy || - state == reorderable_list_library.ReorderableItemState.dragProxyFinished - ) { - decoration = BoxDecoration( - color: Theme.of(context).colorScheme.surface.withOpacity(0.7) - ); - } - else { - bool placeholder = state == reorderable_list_library.ReorderableItemState.placeholder; - decoration = 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 reorderable_list_library.DelayedReorderableListener( - child: Container( - decoration: decoration, - child: SafeArea( - top: false, - bottom: false, - child: Opacity( - opacity: state == reorderable_list_library.ReorderableItemState.placeholder ? 0.0 : 1.0, - child: IntrinsicHeight( - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: tileWidget - ), - ], - ), + 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/safe_search_settings.dart b/lib/screens/settings/safe_search_settings.dart index 461b6a6..793a1c2 100644 --- a/lib/screens/settings/safe_search_settings.dart +++ b/lib/screens/settings/safe_search_settings.dart @@ -1,5 +1,6 @@ // 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'; @@ -13,7 +14,7 @@ import 'package:adguard_home_manager/functions/snackbar.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; class SafeSearchSettingsScreen extends StatefulWidget { - const SafeSearchSettingsScreen({Key? key}) : super(key: key); + const SafeSearchSettingsScreen({super.key}); @override State createState() => _SafeSearchSettingsScreenState(); @@ -70,8 +71,10 @@ class _SafeSearchSettingsScreenState extends State { final statusProvider = Provider.of(context); final appConfigProvider = Provider.of(context); + final width = MediaQuery.of(context).size.width; + void saveConfig() async { - ProcessModal processModal = ProcessModal(context: context); + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.savingSettings); final result = await statusProvider.updateSafeSearchConfig({ @@ -104,167 +107,10 @@ class _SafeSearchSettingsScreenState extends State { } } - Widget body() { - 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: [ - 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(); - } - } - return Scaffold( appBar: AppBar( title: Text(AppLocalizations.of(context)!.safeSearchSettings), + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, centerTitle: false, actions: [ IconButton( @@ -277,7 +123,169 @@ class _SafeSearchSettingsScreenState extends State { const SizedBox(width: 8) ], ), - body: body(), + 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/server_info.dart b/lib/screens/settings/server_info/server_info.dart index a6a9fcc..b15d8e0 100644 --- a/lib/screens/settings/server_info/server_info.dart +++ b/lib/screens/settings/server_info/server_info.dart @@ -1,60 +1,37 @@ -import 'package:adguard_home_manager/constants/enums.dart'; -import 'package:animations/animations.dart'; 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/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/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 { +class _ServerInformationState extends State { ServerInfo serverInfo = ServerInfo(loadStatus: LoadStatus.loading); void fetchServerInfo() async { - final result = await Provider.of(context, listen: false).apiClient!.getServerInfo(); - if (mounted) { - if (result['result'] == 'success') { - setState(() { - serverInfo.data = result['data']; - serverInfo.loadStatus = LoadStatus.loaded; - }); - } - else { - setState(() => serverInfo.loadStatus = LoadStatus.error); - } + 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); } } @@ -66,121 +43,125 @@ class _ServerInformationWidgetState extends State { @override Widget build(BuildContext context) { - Widget generateBody() { - 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: () { - 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 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(); - } - } - + 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: () { + 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 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 709a245..6efdd9b 100644 --- a/lib/screens/settings/settings.dart +++ b/lib/screens/settings/settings.dart @@ -23,48 +23,69 @@ 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/functions/desktop_mode.dart'; import 'package:adguard_home_manager/constants/strings.dart'; import 'package:adguard_home_manager/functions/open_url.dart'; -import 'package:adguard_home_manager/functions/compare_versions.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'; class Settings extends StatelessWidget { - const Settings({Key? key}) : super(key: key); + const Settings({super.key}); @override Widget build(BuildContext context) { - final width = MediaQuery.of(context).size.width; - - if (width > 900) { - return SplitView.material( - hideDivider: true, - flexWidth: const FlexWidth(mainViewFlexWidth: 1, secondaryViewFlexWidth: 2), - placeholder: Center( - child: Padding( - padding: const EdgeInsets.all(24), - child: Text( - AppLocalizations.of(context)!.selectOptionLeftColumn, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSurfaceVariant + return LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth > 900) { + return SplitView.material( + hideDivider: true, + flexWidth: const FlexWidth(mainViewFlexWidth: 1, secondaryViewFlexWidth: 2), + placeholder: Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Text( + AppLocalizations.of(context)!.selectOptionLeftColumn, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), + ), ), ), - ), - ), - child: const SettingsWidget(), - ); - } - else { - return const SettingsWidget(); - } + child: const _SettingsWidget( + twoColumns: true, + ), + ); + } + else { + return const _SettingsWidget( + twoColumns: false, + ); + } + }, + ); } } -class SettingsWidget extends StatelessWidget { - const SettingsWidget({Key? key}) : super(key: key); +class _SettingsWidget extends StatefulWidget { + final bool twoColumns; + + const _SettingsWidget({ + required this.twoColumns, + }); + + @override + State<_SettingsWidget> createState() => _SettingsWidgetState(); +} + +class _SettingsWidgetState extends State<_SettingsWidget> { + @override + void initState() { + Provider.of(context, listen: false).setSelectedSettingsScreen(screen: null); + super.initState(); + } @override Widget build(BuildContext context) { @@ -74,47 +95,10 @@ class SettingsWidget extends StatelessWidget { final width = MediaQuery.of(context).size.width; - if (width <= 900 && appConfigProvider.selectedSettingsScreen != null) { + if (!widget.twoColumns && appConfigProvider.selectedSettingsScreen != null) { appConfigProvider.setSelectedSettingsScreen(screen: null); } - Widget settingsTile({ - required String title, - required String subtitle, - required IconData icon, - Widget? trailing, - required Widget screenToNavigate, - required int thisItem - }) { - if (width > 900) { - return CustomSettingsTile( - title: title, - subtitle: subtitle, - icon: icon, - trailing: trailing, - thisItem: thisItem, - selectedItem: appConfigProvider.selectedSettingsScreen, - onTap: () { - appConfigProvider.setSelectedSettingsScreen(screen: thisItem, notify: true); - SplitView.of(context).setSecondary(screenToNavigate); - }, - ); - } - else { - return CustomListTile( - title: title, - subtitle: subtitle, - icon: icon, - trailing: trailing, - onTap: () { - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => screenToNavigate) - ); - }, - ); - } - } - return Scaffold( body: NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) => [ @@ -125,164 +109,241 @@ class SettingsWidget extends StatelessWidget { floating: true, centerTitle: false, forceElevated: innerBoxIsScrolled, + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, title: Text(AppLocalizations.of(context)!.settings), ) ) ], - body: ListView( - children: [ - if ( - serversProvider.selectedServer != null && - statusProvider.serverStatus != null && - serversProvider.apiClient != null - ) ...[ - SectionLabel(label: AppLocalizations.of(context)!.serverSettings), - if (serverVersionIsAhead( - currentVersion: statusProvider.serverStatus!.serverVersion, - referenceVersion: 'v0.107.28', - referenceVersionBeta: 'v0.108.0-b.33' - ) == true) settingsTile( - icon: Icons.search_rounded, - title: AppLocalizations.of(context)!.safeSearch, - subtitle: AppLocalizations.of(context)!.safeSearchSettings, - thisItem: 0, - screenToNavigate: const SafeSearchSettingsScreen(), - ), - settingsTile( - icon: Icons.lock_rounded, - title: AppLocalizations.of(context)!.accessSettings, - subtitle: AppLocalizations.of(context)!.accessSettingsDescription, - thisItem: 1, - screenToNavigate: const AccessSettings(), - ), - settingsTile( - icon: Icons.install_desktop_rounded, - title: AppLocalizations.of(context)!.dhcpSettings, - subtitle: AppLocalizations.of(context)!.dhcpSettingsDescription, - thisItem: 2, - screenToNavigate: const DhcpScreen(), - ), - settingsTile( - icon: Icons.dns_rounded, - title: AppLocalizations.of(context)!.dnsSettings, - subtitle: AppLocalizations.of(context)!.dnsSettingsDescription, - thisItem: 3, - screenToNavigate: const DnsSettings(), - ), - settingsTile( - icon: Icons.security_rounded, - title: AppLocalizations.of(context)!.encryptionSettings, - subtitle: AppLocalizations.of(context)!.encryptionSettingsDescription, - thisItem: 4, - screenToNavigate: const EncryptionSettings(), - ), - settingsTile( - icon: Icons.route_rounded, - title: AppLocalizations.of(context)!.dnsRewrites, - subtitle: AppLocalizations.of(context)!.dnsRewritesDescription, - thisItem: 5, - screenToNavigate: const DnsRewritesScreen(), - ), - 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 + body: SafeArea( + top: false, + bottom: false, + child: Builder( + builder: (context) => CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + ), + SliverList.list( + children: [ + if ( + serversProvider.selectedServer != null && + statusProvider.serverStatus != null && + serversProvider.apiClient2 != null + ) ...[ + SectionLabel(label: AppLocalizations.of(context)!.serverSettings), + _SettingsTile( + icon: Icons.search_rounded, + title: AppLocalizations.of(context)!.safeSearch, + subtitle: AppLocalizations.of(context)!.safeSearchSettings, + thisItem: 0, + screenToNavigate: const SafeSearchSettingsScreen(), + twoColumns: widget.twoColumns, + ), + _SettingsTile( + icon: Icons.lock_rounded, + title: AppLocalizations.of(context)!.accessSettings, + subtitle: AppLocalizations.of(context)!.accessSettingsDescription, + thisItem: 1, + screenToNavigate: const AccessSettings(), + twoColumns: widget.twoColumns, + ), + _SettingsTile( + icon: Icons.install_desktop_rounded, + title: AppLocalizations.of(context)!.dhcpSettings, + subtitle: AppLocalizations.of(context)!.dhcpSettingsDescription, + thisItem: 2, + screenToNavigate: const DhcpScreen(), + twoColumns: widget.twoColumns, + ), + _SettingsTile( + icon: Icons.dns_rounded, + title: AppLocalizations.of(context)!.dnsSettings, + subtitle: AppLocalizations.of(context)!.dnsSettingsDescription, + thisItem: 3, + screenToNavigate: DnsSettings( + splitView: widget.twoColumns, ), - ) - : null, - thisItem: 6, - screenToNavigate: const UpdateScreen(), - ), - settingsTile( - icon: Icons.info_rounded, - title: AppLocalizations.of(context)!.serverInformation, - subtitle: AppLocalizations.of(context)!.serverInformationDescription, - thisItem: 7, - screenToNavigate: const ServerInformation(), - ), - ], - SectionLabel(label: AppLocalizations.of(context)!.appSettings), - settingsTile( - icon: Icons.palette_rounded, - title: AppLocalizations.of(context)!.customization, - subtitle: AppLocalizations.of(context)!.customizationDescription, - thisItem: 8, - screenToNavigate: const Customization(), - ), - settingsTile( - icon: Icons.storage_rounded, - title: AppLocalizations.of(context)!.servers, - subtitle: serversProvider.selectedServer != null - ? statusProvider.serverStatus != null - ? "${AppLocalizations.of(context)!.connectedTo} ${serversProvider.selectedServer!.name}" - : "${AppLocalizations.of(context)!.selectedServer} ${serversProvider.selectedServer!.name}" - : AppLocalizations.of(context)!.noServerSelected, - thisItem: 9, - screenToNavigate: const Servers(), - ), - settingsTile( - icon: Icons.settings, - title: AppLocalizations.of(context)!.generalSettings, - subtitle: AppLocalizations.of(context)!.generalSettingsDescription, - thisItem: 10, - screenToNavigate: const GeneralSettings(), - ), - settingsTile( - icon: Icons.build_outlined, - title: AppLocalizations.of(context)!.advancedSettings, - subtitle: AppLocalizations.of(context)!.advancedSetupDescription, - thisItem: 11, - screenToNavigate: 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: [ - if (Platform.isAndroid) IconButton( - onPressed: () => openUrl(Urls.playStore), - icon: SvgPicture.asset( - 'assets/resources/google-play.svg', - color: Theme.of(context).colorScheme.onSurfaceVariant, - width: 30, - height: 30, + twoColumns: widget.twoColumns, + ), + _SettingsTile( + icon: Icons.security_rounded, + title: AppLocalizations.of(context)!.encryptionSettings, + subtitle: AppLocalizations.of(context)!.encryptionSettingsDescription, + thisItem: 4, + screenToNavigate: const EncryptionSettings(), + twoColumns: widget.twoColumns, + ), + _SettingsTile( + icon: Icons.route_rounded, + title: AppLocalizations.of(context)!.dnsRewrites, + subtitle: AppLocalizations.of(context)!.dnsRewritesDescription, + thisItem: 5, + screenToNavigate: const DnsRewritesScreen(), + twoColumns: widget.twoColumns, + ), + if (serversProvider.updateAvailable.data != null) _SettingsTile( + icon: Icons.system_update_rounded, + title: AppLocalizations.of(context)!.updates, + subtitle: AppLocalizations.of(context)!.updatesDescription, + trailing: serversProvider.updateAvailable.data != null && + serversProvider.updateAvailable.data!.canAutoupdate == true + ? Container( + width: 10, + height: 10, + margin: const EdgeInsets.only(right: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Colors.red + ), + ) + : null, + thisItem: 6, + screenToNavigate: const UpdateScreen(), + twoColumns: widget.twoColumns, + ), + _SettingsTile( + icon: Icons.info_rounded, + title: AppLocalizations.of(context)!.serverInformation, + subtitle: AppLocalizations.of(context)!.serverInformationDescription, + thisItem: 7, + 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: 8, + screenToNavigate: const Customization(), + twoColumns: widget.twoColumns, ), - tooltip: AppLocalizations.of(context)!.visitGooglePlay, - ), - IconButton( - onPressed: () => openUrl(Urls.gitHub), - icon: SvgPicture.asset( - 'assets/resources/github.svg', - color: Theme.of(context).colorScheme.onSurfaceVariant, - width: 30, - height: 30, + _SettingsTile( + icon: Icons.storage_rounded, + title: AppLocalizations.of(context)!.servers, + subtitle: serversProvider.selectedServer != null + ? statusProvider.serverStatus != null + ? "${AppLocalizations.of(context)!.connectedTo} ${serversProvider.selectedServer!.name}" + : "${AppLocalizations.of(context)!.selectedServer} ${serversProvider.selectedServer!.name}" + : AppLocalizations.of(context)!.noServerSelected, + thisItem: 9, + screenToNavigate: const Servers(), + twoColumns: widget.twoColumns, ), - tooltip: AppLocalizations.of(context)!.gitHub, - ), - ], - ), + _SettingsTile( + icon: Icons.settings, + title: AppLocalizations.of(context)!.generalSettings, + subtitle: AppLocalizations.of(context)!.generalSettingsDescription, + thisItem: 10, + screenToNavigate: GeneralSettings(splitView: widget.twoColumns), + twoColumns: widget.twoColumns, + ), + _SettingsTile( + icon: Icons.build_outlined, + title: AppLocalizations.of(context)!.advancedSettings, + subtitle: AppLocalizations.of(context)!.advancedSetupDescription, + thisItem: 11, + screenToNavigate: const AdvancedSettings(), + twoColumns: widget.twoColumns, + ), + SectionLabel(label: AppLocalizations.of(context)!.aboutApp), + CustomListTile( + title: AppLocalizations.of(context)!.appVersion, + subtitle: appConfigProvider.getAppInfo!.version, + ), + CustomListTile( + title: AppLocalizations.of(context)!.createdBy, + subtitle: Strings.createdBy, + ), + Padding( + padding: const EdgeInsets.all(15), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + if (Platform.isAndroid) IconButton( + onPressed: () => openUrl(Urls.playStore), + icon: SvgPicture.asset( + 'assets/resources/google-play.svg', + color: Theme.of(context).colorScheme.onSurfaceVariant, + width: 30, + height: 30, + ), + tooltip: AppLocalizations.of(context)!.visitGooglePlay, + ), + IconButton( + onPressed: () => openUrl(Urls.gitHub), + icon: SvgPicture.asset( + 'assets/resources/github.svg', + color: Theme.of(context).colorScheme.onSurfaceVariant, + width: 30, + height: 30, + ), + tooltip: AppLocalizations.of(context)!.gitHub, + ), + ], + ), + ) + ], + ) + ], ) - ], + ), ), ) ); } +} + +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); + + if (twoColumns) { + return CustomSettingsTile( + title: title, + subtitle: subtitle, + icon: icon, + trailing: trailing, + thisItem: thisItem, + selectedItem: appConfigProvider.selectedSettingsScreen, + onTap: () { + appConfigProvider.setSelectedSettingsScreen(screen: thisItem, notify: true); + SplitView.of(context).setSecondary(screenToNavigate); + }, + ); + } + else { + return CustomListTile( + title: title, + subtitle: subtitle, + icon: icon, + trailing: trailing, + onTap: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => screenToNavigate) + ); + }, + ); + } + } } \ 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/update.dart b/lib/screens/settings/update_server/update.dart index 3467994..d1f8d95 100644 --- a/lib/screens/settings/update_server/update.dart +++ b/lib/screens/settings/update_server/update.dart @@ -17,7 +17,7 @@ import 'package:adguard_home_manager/constants/enums.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart'; class UpdateScreen extends StatelessWidget { - const UpdateScreen({Key? key}) : super(key: key); + const UpdateScreen({super.key}); @override Widget build(BuildContext context) { @@ -32,14 +32,14 @@ class UpdateScreen extends StatelessWidget { } void update() async { - ProcessModal processModal = ProcessModal(context: context); + ProcessModal processModal = ProcessModal(); processModal.open(AppLocalizations.of(context)!.requestingUpdate); - final result = await serversProvider.apiClient!.requestUpdateServer(); + final result = await serversProvider.apiClient2!.requestUpdateServer(); processModal.close(); - if (result['result'] == 'success') { + if (result.successful == true) { serversProvider.recheckPeriodServerUpdated(); showSnacbkar( appConfigProvider: appConfigProvider, @@ -166,33 +166,38 @@ class UpdateScreen extends StatelessWidget { ); } - final changelog = serversProvider.updateAvailable.loadStatus == LoadStatus.loaded && serversProvider.updateAvailable.data!.changelog != null - ? 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 + 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, + 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, + ) ) - ) - ], - ) - : null; + ], + ), + ); + } else { + changelog = null; + } return Scaffold( body: Column( diff --git a/lib/screens/top_items/top_items.dart b/lib/screens/top_items/top_items.dart deleted file mode 100644 index 6e9af74..0000000 --- a/lib/screens/top_items/top_items.dart +++ /dev/null @@ -1,256 +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/widgets/domain_options.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/providers/logs_provider.dart'; -import 'package:adguard_home_manager/providers/status_provider.dart'; -import 'package:adguard_home_manager/functions/snackbar.dart'; -import 'package:adguard_home_manager/functions/number_format.dart'; -import 'package:adguard_home_manager/providers/app_config_provider.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 statusProvider = 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()); - } - - 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 statusProvider.getServerStatus(); - if (result == false) { - showSnacbkar( - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.serverStatusNotRefreshed, - color: Colors.red - ); - } - }, - child: screenData.isNotEmpty - ? ListView.builder( - itemCount: screenData.length, - itemBuilder: (context, index) { - String? name; - if (widget.isClient != null && widget.isClient == true) { - try { - name = statusProvider.serverStatus!.clients.firstWhere((c) => c.ids.contains(screenData[index].keys.toList()[0])).name; - } catch (e) { - // ---- // - } - } - - return DomainOptions( - item: screenData[index].keys.toList()[0], - isBlocked: widget.type == 'topBlockedDomains', - isClient: widget.type == 'topClients', - onTap: () { - if (widget.type == 'topQueriedDomains' || widget.type == 'topBlockedDomains') { - logsProvider.setSearchText(screenData[index].keys.toList()[0]); - logsProvider.setSelectedClients(null); - logsProvider.setAppliedFilters( - AppliedFiters( - selectedResultStatus: 'all', - searchText: screenData[index].keys.toList()[0], - clients: null - ) - ); - appConfigProvider.setSelectedScreen(2); - Navigator.pop(context); - } - else if (widget.type == 'topClients') { - logsProvider.setSearchText(null); - logsProvider.setSelectedClients([screenData[index].keys.toList()[0]]); - logsProvider.setAppliedFilters( - AppliedFiters( - selectedResultStatus: 'all', - searchText: null, - clients: [screenData[index].keys.toList()[0]] - ) - ); - appConfigProvider.setSelectedScreen(2); - Navigator.pop(context); - } - }, - child: CustomListTile( - 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/screens/top_items/top_items_modal.dart b/lib/screens/top_items/top_items_modal.dart deleted file mode 100644 index 845459a..0000000 --- a/lib/screens/top_items/top_items_modal.dart +++ /dev/null @@ -1,236 +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/widgets/domain_options.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/providers/logs_provider.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 TopItemsModal extends StatefulWidget { - final String type; - final String title; - final bool? isClient; - final List> data; - - const TopItemsModal({ - Key? key, - required this.type, - required this.title, - this.isClient, - required this.data, - }) : super(key: key); - - @override - State createState() => _TopItemsModalState(); -} - -class _TopItemsModalState 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 statusProvider = 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()); - } - - return Dialog( - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 500 - ), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - flex: 1, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - IconButton( - onPressed: () => Navigator.pop(context), - icon: const Icon(Icons.clear_rounded), - tooltip: AppLocalizations.of(context)!.close, - ), - ], - ), - ), - Expanded( - flex: 1, - child: TextField( - controller: searchController, - onChanged: search, - decoration: InputDecoration( - filled: true, - fillColor: Theme.of(context).colorScheme.primary.withOpacity(0.1), - hintText: AppLocalizations.of(context)!.search, - prefixIcon: const Icon(Icons.search_rounded), - contentPadding: const EdgeInsets.only(left: 14, bottom: 9, top: 11), - focusedBorder: OutlineInputBorder( - borderSide: const BorderSide(color: Colors.transparent), - borderRadius: BorderRadius.circular(25.7), - ), - enabledBorder: UnderlineInputBorder( - borderSide: const BorderSide(color: Colors.transparent), - borderRadius: BorderRadius.circular(25.7), - ), - ), - ), - ) - ], - ), - ), - if (screenData.isNotEmpty) Flexible( - child: ListView.builder( - padding: const EdgeInsets.only(top: 0), - itemCount: screenData.length, - itemBuilder: (context, index) { - String? name; - if (widget.isClient != null && widget.isClient == true) { - try { - name = statusProvider.serverStatus!.clients.firstWhere((c) => c.ids.contains(screenData[index].keys.toList()[0])).name; - } catch (e) { - // ---- // - } - } - - return DomainOptions( - isBlocked: widget.type == 'topBlockedDomains', - isClient: widget.type == 'topClients', - item: screenData[index].keys.toList()[0], - onTap: () { - if (widget.type == 'topQueriedDomains' || widget.type == 'topBlockedDomains') { - logsProvider.setSearchText(screenData[index].keys.toList()[0]); - logsProvider.setSelectedClients(null); - logsProvider.setAppliedFilters( - AppliedFiters( - selectedResultStatus: 'all', - searchText: screenData[index].keys.toList()[0], - clients: null - ) - ); - appConfigProvider.setSelectedScreen(2); - Navigator.pop(context); - } - else if (widget.type == 'topClients') { - logsProvider.setSearchText(null); - logsProvider.setSelectedClients([screenData[index].keys.toList()[0]]); - logsProvider.setAppliedFilters( - AppliedFiters( - selectedResultStatus: 'all', - searchText: null, - clients: [screenData[index].keys.toList()[0]] - ) - ); - appConfigProvider.setSelectedScreen(2); - Navigator.pop(context); - } - }, - child: CustomListTile( - title: screenData[index].keys.toList()[0], - trailing: Text( - screenData[index].values.toList()[0].toString(), - style: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant - ), - ), - subtitleWidget: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (name != null) ...[ - Text( - name, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.onSurface - ), - ), - const SizedBox(height: 5), - ], - Row( - children: [ - SizedBox( - width: 50, - child: Text( - "${doubleFormat((screenData[index].values.toList()[0]/total*100), Platform.localeName)}%", - style: TextStyle( - color: Theme.of(context).listTileTheme.textColor - ), - ), - ), - const SizedBox(width: 10), - Flexible( - child: LinearPercentIndicator( - animation: true, - lineHeight: 4, - animationDuration: 500, - curve: Curves.easeOut, - percent: screenData[index].values.toList()[0]/total, - barRadius: const Radius.circular(5), - progressColor: Theme.of(context).colorScheme.primary, - backgroundColor: Theme.of(context).colorScheme.surfaceVariant, - ), - ), - const SizedBox(width: 10), - ], - ), - ], - ) - ), - ); - } - ), - ), - if (screenData.isEmpty) Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Text( - AppLocalizations.of(context)!.noItemsSearch, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ), - ) - ], - ), - ), - ); - } -} \ 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..b45622a --- /dev/null +++ b/lib/services/api_client.dart @@ -0,0 +1,877 @@ +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/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({ + required int count, + int? offset, + DateTime? olderThan, + String? responseStatus, + String? search + }) async { + final result = await HttpRequestClient.get( + urlPath: '/querylog?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( + dhcpAvailable: jsonDecode(results[1].body!)['message'] != null + ? false + : true, + 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) { + return ApiResponse( + successful: true, + content: jsonDecode(result.body!) + ); + } + 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.put( + 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 + ); + } +} \ No newline at end of file diff --git a/lib/services/auth.dart b/lib/services/auth.dart new file mode 100644 index 0000000..a450348 --- /dev/null +++ b/lib/services/auth.dart @@ -0,0 +1,82 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +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) { + 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) { + return AuthStatus.unknown; + } + } +} \ No newline at end of file diff --git a/lib/services/db/database.dart b/lib/services/db/database.dart index 66bd269..7d8cf13 100644 --- a/lib/services/db/database.dart +++ b/lib/services/db/database.dart @@ -116,13 +116,94 @@ Future> loadDb(bool acceptsDynamicTheme) async { }); } + Future upgradeDbToV10(Database db) async { + await db.execute("ALTER TABLE appConfig ADD COLUMN showTopItemsChart NUMERIC"); + await db.execute("UPDATE appConfig SET showTopItemsChart = 1"); + + await db.transaction((txn) async{ + await txn.rawQuery( + 'SELECT * FROM appConfig', + ); + }); + } + Database db = await openDatabase( 'adguard_home_manager.db', - version: 9, + version: 10, 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, showTimeLogs NUMERIC, showIpLogs NUMERIC, combinedChart NUMERIC, doNotRememberVersion TEXT, hideServerAddress NUMERIC, homeTopItemsOrder TEXT)"); - await db.execute("INSERT INTO appConfig (theme, overrideSslCheck, hideZeroValues, useDynamicColor, staticColor, useThemeColorForStatus, showTimeLogs, showIpLogs, combinedChart, hideServerAddress, homeTopItemsOrder) VALUES (0, 0, 0, ${acceptsDynamicTheme == true ? 1 : 0}, 0, 0, 0, 0, 0, 0, $homeTopItemsDefaultOrderString)"); + 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, + showTimeLogs NUMERIC, + showIpLogs NUMERIC, + combinedChart NUMERIC, + doNotRememberVersion TEXT, + hideServerAddress NUMERIC, + homeTopItemsOrder TEXT, + showTopItemsChart NUMERIC + ) + """ + ); + + await db.execute( + """ + INSERT INTO + appConfig ( + theme, + overrideSslCheck, + hideZeroValues, + useDynamicColor, + staticColor, + useThemeColorForStatus, + showTimeLogs, + showIpLogs, + combinedChart, + hideServerAddress, + homeTopItemsOrder, + showTopItemsChart + ) + VALUES ( + 0, + 0, + 0, + ${acceptsDynamicTheme == true ? 1 : 0}, + 0, + 0, + 0, + 0, + 0, + 0, + '$homeTopItemsDefaultOrderString', + 1 + ) + """ + ); }, onUpgrade: (Database db, int oldVersion, int newVersion) async { if (oldVersion == 1) { @@ -134,6 +215,7 @@ Future> loadDb(bool acceptsDynamicTheme) async { await upgradeDbToV7(db); await upgradeDbToV8(db); await upgradeDbToV9(db); + await upgradeDbToV10(db); } if (oldVersion == 2) { await upgradeDbToV3(db); @@ -143,6 +225,7 @@ Future> loadDb(bool acceptsDynamicTheme) async { await upgradeDbToV7(db); await upgradeDbToV8(db); await upgradeDbToV9(db); + await upgradeDbToV10(db); } if (oldVersion == 3) { await upgradeDbToV4(db); @@ -151,6 +234,7 @@ Future> loadDb(bool acceptsDynamicTheme) async { await upgradeDbToV7(db); await upgradeDbToV8(db); await upgradeDbToV9(db); + await upgradeDbToV10(db); } if (oldVersion == 4) { await upgradeDbToV5(db); @@ -158,24 +242,32 @@ Future> loadDb(bool acceptsDynamicTheme) async { await upgradeDbToV7(db); await upgradeDbToV8(db); await upgradeDbToV9(db); + await upgradeDbToV10(db); } if (oldVersion == 5) { await upgradeDbToV6(db); await upgradeDbToV7(db); await upgradeDbToV8(db); await upgradeDbToV9(db); + await upgradeDbToV10(db); } if (oldVersion == 6) { await upgradeDbToV7(db); await upgradeDbToV8(db); await upgradeDbToV9(db); + await upgradeDbToV10(db); } if (oldVersion == 7) { await upgradeDbToV8(db); await upgradeDbToV9(db); + await upgradeDbToV10(db); } if (oldVersion == 8) { await upgradeDbToV9(db); + await upgradeDbToV10(db); + } + if (oldVersion == 9) { + await upgradeDbToV10(db); } }, onOpen: (Database db) async { 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 85de805..37a2cc8 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,7 +20,6 @@ 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({ @@ -270,80 +270,107 @@ class ApiClient { }); Future getServerVersion() async { - final result = await apiRequest( - server: server, - method: 'get', - urlPath: '/status', - type: 'get_server_version' - ); + try { + final result = await apiRequest( + server: server, + method: 'get', + urlPath: '/status', + type: 'get_server_version' + ); - if (result['hasResponse'] == true) { - if (result['statusCode'] == 200 && result['body'] != null) { - return { - 'result': 'success', - 'data': jsonDecode(result['body'])['version'] - }; + 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': 'error', - 'log': AppLog( - type: 'get_server_version', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'] - ) - }; + return result; } - } - else { - return result; + } catch (e) { + Sentry.captureException(e); + return { + 'result': 'error', + 'log': AppLog( + type: 'get_server_version', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + resBody: e.toString() + ) + }; } } Future getServerStatus() 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'), - ]); + 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[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 + 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 ) { - 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) - }; + 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 { @@ -351,24 +378,23 @@ class ApiClient { '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() + message: 'no_response', + statusCode: result.map((res) => res['statusCode'] ?? 'null').toString(), + resBody: result.map((res) => res['body'] ?? 'null').toString() ) }; } - } - else { + } catch (e) { + Sentry.captureException(e); 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() - ) - }; + 'result': 'error', + 'log': AppLog( + type: 'get_server_status', + dateTime: DateTime.now(), + message: 'no_response', + resBody: e.toString() + ) + }; } } @@ -589,19 +615,33 @@ class ApiClient { } Future getClients() 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'), - ]); + 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 - }; + 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 { @@ -609,22 +649,21 @@ class ApiClient { 'log': AppLog( type: 'get_clients', dateTime: DateTime.now(), - message: 'error_code_not_expected', + message: 'no_response', statusCode: result.map((res) => res['statusCode'] ?? 'null').toString(), resBody: result.map((res) => res['body'] ?? 'null').toString(), ) }; } - } - else { + } catch (e) { + Sentry.captureException(e); 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(), + resBody: e.toString() ) }; } @@ -674,68 +713,94 @@ class ApiClient { 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'])) - }; + 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': 'error', - 'log': AppLog( - type: 'get_logs', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'] - ) - }; + return result; } - } - else { - return result; + } catch (e) { + Sentry.captureException(e); + return { + 'result': 'error', + 'log': AppLog( + type: 'get_logs', + dateTime: DateTime.now(), + message: 'error_code_not_expected', + resBody: e.toString() + ) + }; } } Future getFilteringRules() 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'])) - }; + 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': 'error', - 'log': AppLog( - type: 'get_filtering_rules', - dateTime: DateTime.now(), - message: 'error_code_not_expected', - statusCode: result['statusCode'].toString(), - resBody: result['body'] - ) - }; + return result; } - } - 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', + resBody: e.toString() + ) + }; } } @@ -872,32 +937,46 @@ class ApiClient { } Future getFiltering() async { - 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' - ), - ]); + 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']) - : [] - }) - }; + 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 { @@ -905,22 +984,21 @@ class ApiClient { 'log': AppLog( type: 'get_filtering_status', dateTime: DateTime.now(), - message: 'error_code_not_expected', + message: 'no_response', statusCode: result.map((res) => res['statusCode'] ?? 'null').toString(), resBody: result.map((res) => res['body'] ?? 'null').toString(), ) }; } - } - else { + } catch (e) { + Sentry.captureException(e); 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(), + resBody: e.toString(), ) }; } @@ -1251,55 +1329,69 @@ class ApiClient { } Future getDhcpData() async { - 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' - ), - ]); + 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))); + 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'])) - ) - }; + return { + 'result': 'success', + 'data': DhcpModel( + dhcpAvailable: true, + 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_dhcp_data', + type: 'get_dhpc_data', dateTime: DateTime.now(), - message: 'error_code_not_expected', + 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(), ) }; } - } - else { + } catch (e) { + Sentry.captureException(e); 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(), + message: 'error_code_not_expected', + resBody: e.toString(), ) }; } @@ -2004,34 +2096,48 @@ class ApiClient { } Future checkServerUpdates() async { - 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', - ), - ]); + 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 - }; + 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 { @@ -2039,100 +2145,26 @@ class ApiClient { 'log': AppLog( type: 'get_filtering_status', dateTime: DateTime.now(), - message: 'error_code_not_expected', + message: 'no_response', statusCode: result.map((res) => res['statusCode'] ?? 'null').toString(), resBody: result.map((res) => res['body'] ?? 'null').toString(), ) }; } - } - else { + } catch (e) { + Sentry.captureException(e); 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(), + resBody: e.toString(), ) }; } } - Future getUpdateChangelog({ - required String releaseTag - }) async { - try { - HttpClient httpClient = HttpClient(); - HttpClientRequest request = await httpClient.getUrl(Uri.parse("${Urls.adGuardHomeReleasesTags}/$releaseTag")); - 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': jsonDecode(reply)['body'] - }; - } - 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() - ) - }; - } - } - Future requestUpdateServer() async { final result = await apiRequest( urlPath: '/update', @@ -2228,75 +2260,4 @@ class ApiClient { return result; } } -} - -Future checkAppUpdatesGitHub() async { - try { - HttpClient httpClient = HttpClient(); - HttpClientRequest request = await httpClient.getUrl(Uri.parse(Urls.getReleasesGitHub)); - 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': List.from(jsonDecode(reply).map((entry) => GitHubRelease.fromJson(entry))) - }; - } - 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..6dd9c38 --- /dev/null +++ b/lib/widgets/add_server/add_server_functions.dart @@ -0,0 +1,120 @@ +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 != '') { + RegExp subrouteRegexp = RegExp(r'^\/\b([A-Za-z0-9_\-~/]*)[^\/|\.|\:]$'); + if (subrouteRegexp.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 != '') { + 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) { + 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..ac7d4a1 --- /dev/null +++ b/lib/widgets/add_server/add_server_modal.dart @@ -0,0 +1,626 @@ +// 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, + 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) { + showSnacbkar( + 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 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 something goes wrong when saving the connection on the db + if (serverCreated != null) { + if (mounted) setState(() => isConnecting = false); + if (mounted) { + showSnacbkar( + 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) { + showSnacbkar( + 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 something goes wrong when saving the connection on the db + if (serverSaved != null) { + if (mounted) setState(() => isConnecting = false); + appConfigProvider.addLog( + AppLog( + type: 'save_connection_db', + dateTime: DateTime.now(), + message: serverSaved.toString() + ) + ); + if (mounted) { + showSnacbkar( + 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, + foregroundSelectedColor: Theme.of(context).colorScheme.onPrimary, + foregroundUnselectedColor: Theme.of(context).colorScheme.onSurface, + hoverColor: Theme.of(context).colorScheme.onSurfaceVariant, + ), + textOverflow: TextOverflow.ellipsis, + fontSize: 14, + height: 40, + margin: const EdgeInsets.symmetric( + horizontal: 24, + ), + ), + 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..0fb6f94 --- /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({ + Key? 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 + }) : super(key: key); + + @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 362eee9..0000000 --- a/lib/widgets/add_server_modal.dart +++ /dev/null @@ -1,760 +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/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/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; - final bool window; - final void Function(String version) onUnsupportedVersion; - - const AddServerModal({ - Key? key, - this.server, - required this.window, - required this.onUnsupportedVersion - }) : 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(); - - final TextEditingController passwordController = TextEditingController(); - - 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 - ) { - 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]+)|((\w|-)+)$'); - 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(); - } - - @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 statusProvider = Provider.of(context, listen: false); - final appConfigProvider = Provider.of(context, listen: false); - - final mediaQuery = MediaQuery.of(context); - - void cancelConnecting() { - if (mounted) { - setState(() => isConnecting = false); - } - else { - isConnecting = false; - } - } - - 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 != "" ? userController.text : null, - password: passwordController.text != "" ? passwordController.text : null, - defaultServer: defaultServer, - authToken: homeAssistant == true - ? encodeBase64UserPass(userController.text, passwordController.text) - : null, - runningOnHa: homeAssistant - ); - setState(() => isConnecting = true); - - final result = homeAssistant == true - ? await loginHA(serverObj) - : await login(serverObj); - - if (result['result'] == 'success') { - if (serverObj.user != null && serverObj.password != null) { - serverObj.authToken = encodeBase64UserPass(serverObj.user!, serverObj.password!); - } - final serverCreated = await serversProvider.createServer(serverObj); - if (serverCreated == null) { - statusProvider.setServerStatusLoad(LoadStatus.loading); - - final ApiClient apiClient = ApiClient(server: serverObj); - - final serverStatus = await apiClient.getServerStatus(); - - if (serverStatus['result'] == 'success') { - statusProvider.setServerStatusData( - data: serverStatus['data'] - ); - serversProvider.setApiClient(apiClient); - statusProvider.setServerStatusLoad(LoadStatus.loaded); - if (serverStatus['data'].serverVersion.contains('a') || serverStatus['data'].serverVersion.contains('b')) { - Navigator.pop(context); - widget.onUnsupportedVersion(serverStatus['data'].serverVersion); - } - else { - Navigator.pop(context); - } - } - else { - appConfigProvider.addLog(serverStatus['log']); - statusProvider.setServerStatusLoad(LoadStatus.error); - Navigator.pop(context); - } - } - else { - setState(() => isConnecting = false); - appConfigProvider.addLog( - AppLog( - type: 'save_connection_db', - dateTime: DateTime.now(), - message: serverCreated.toString() - ) - ); - showSnacbkar( - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.connectionNotCreated, - color: Colors.red - ); - } - } - else if (result['result'] == 'invalid_username_password') { - cancelConnecting(); - appConfigProvider.addLog(result['log']); - showSnacbkar( - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.invalidUsernamePassword, - color: Colors.red - ); - } - else if (result['result'] == 'many_attempts') { - cancelConnecting(); - appConfigProvider.addLog(result['log']); - showSnacbkar( - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.tooManyAttempts, - color: Colors.red - ); - } - else if (result['result'] == 'no_connection') { - cancelConnecting(); - appConfigProvider.addLog(result['log']); - showSnacbkar( - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.cantReachServer, - color: Colors.red - ); - } - else if (result['result'] == 'ssl_error') { - cancelConnecting(); - appConfigProvider.addLog(result['log']); - showSnacbkar( - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.sslError, - color: Colors.red - ); - } - else if (result['result'] == 'server_error') { - cancelConnecting(); - appConfigProvider.addLog(result['log']); - showSnacbkar( - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.serverError, - color: Colors.red - ); - } - else { - cancelConnecting(); - appConfigProvider.addLog(result['log']); - showSnacbkar( - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.unknownError, - color: 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 != "" ? 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 loginHA(serverObj) - : await login(serverObj); - - if (result['result'] == 'success') { - if (serverObj.user != null && serverObj.password != null) { - serverObj.authToken = encodeBase64UserPass(serverObj.user!, serverObj.password!); - } - final serverSaved = await serversProvider.editServer(serverObj); - - if (serverSaved == null) { - final ApiClient apiClient = ApiClient(server: serverObj); - final version = await apiClient.getServerVersion(); - if ( - version['result'] == 'success' && - (version['data'].contains('a') || version['data'].contains('b')) // alpha or beta - ) { - Navigator.pop(context); - widget.onUnsupportedVersion(version['data']); - } - else { - Navigator.pop(context); - } - } - else { - appConfigProvider.addLog( - AppLog( - type: 'edit_connection_db', - dateTime: DateTime.now(), - message: serverSaved.toString() - ) - ); - showSnacbkar( - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.connectionNotCreated, - color: Colors.red - ); - } - } - else if (result['result'] == 'invalid_username_password') { - appConfigProvider.addLog(result['log']); - showSnacbkar( - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.invalidUsernamePassword, - color: Colors.red - ); - } - else if (result['result'] == 'many_attempts') { - appConfigProvider.addLog(result['log']); - showSnacbkar( - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.tooManyAttempts, - color: Colors.red - ); - } - else if (result['result'] == 'no_connection') { - appConfigProvider.addLog(result['log']); - showSnacbkar( - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.cantReachServer, - color: Colors.red - ); - } - else if (result['result'] == 'ssl_error') { - appConfigProvider.addLog(result['log']); - showSnacbkar( - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.sslError, - color: Colors.red - ); - } - else if (result['result'] == 'server_error') { - appConfigProvider.addLog(result['log']); - showSnacbkar( - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.serverError, - color: Colors.red - ); - } - else { - appConfigProvider.addLog(result['log']); - showSnacbkar( - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.unknownError, - color: Colors.red - ); - } - } - - 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(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, - ), - const SizedBox(height: 20), - textField( - label: AppLocalizations.of(context)!.password, - controller: passwordController, - icon: Icons.lock_rounded, - keyboardType: TextInputType.visiblePassword, - 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, - ) - ], - ), - ), - ), - ), - 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), - ) - ], - ), - ), - ), - ), - const SizedBox(height: 20), - ]; - } - - if (widget.window == true) { - return Dialog( - child: SizedBox( - width: 400, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - IconButton( - onPressed: () => Navigator.pop(context), - icon: const Icon(Icons.clear_rounded) - ), - const SizedBox(width: 8), - Text( - AppLocalizations.of(context)!.createConnection, - style: const TextStyle( - fontSize: 20 - ), - ), - ], - ), - 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 - ? widget.server == null - ? () => connect() - : () => edit() - : null, - icon: Icon( - widget.server == null - ? Icons.login_rounded - : Icons.save_rounded - ) - ), - ], - ), - ], - ), - ), - Expanded( - child: ListView( - children: form() - ), - ) - ], - ), - ), - ); - } - else { - return Stack( - children: [ - Scaffold( - appBar: AppBar( - title: Text(AppLocalizations.of(context)!.createConnection), - actions: [ - IconButton( - 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 - ? widget.server == null - ? () => connect() - : () => edit() - : null, - icon: Icon( - widget.server == null - ? Icons.login_rounded - : Icons.save_rounded - ) - ), - const SizedBox(width: 10) - ], - toolbarHeight: 70, - ), - body: ListView( - children: form(), - ) - ), - AnimatedOpacity( - opacity: isConnecting == true ? 1 : 0, - 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 366fe2c..8d06a8d 100644 --- a/lib/widgets/bottom_nav_bar.dart +++ b/lib/widgets/bottom_nav_bar.dart @@ -3,21 +3,19 @@ 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 && serversProvider.apiClient != null + List screens = serversProvider.selectedServer != null && serversProvider.apiClient2 != null ? screensServerConnected : screensSelectServer; @@ -46,12 +44,12 @@ class BottomNavBar extends StatelessWidget { } } - if ((serversProvider.selectedServer == null || serversProvider.apiClient == null) && appConfigProvider.selectedScreen > 1) { + if ((serversProvider.selectedServer == null || serversProvider.apiClient2 == null) && appConfigProvider.selectedScreen > 1) { appConfigProvider.setSelectedScreen(0); } return NavigationBar( - selectedIndex: (serversProvider.selectedServer == null || serversProvider.apiClient == null) && appConfigProvider.selectedScreen > 1 + selectedIndex: (serversProvider.selectedServer == null || serversProvider.apiClient2 == null) && appConfigProvider.selectedScreen > 1 ? 0 : appConfigProvider.selectedScreen, destinations: screens.map((screen) => NavigationDestination( @@ -84,21 +82,6 @@ class BottomNavBar extends StatelessWidget { ), 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(); - } - // Reset settings selected screen - if (value != screens.length-1) { - appConfigProvider.setSelectedSettingsScreen(screen: null); - } - appConfigProvider.setSelectedScreen(value); - }, ); } } \ No newline at end of file diff --git a/lib/widgets/custom_list_tile.dart b/lib/widgets/custom_list_tile.dart index 6f27129..0523468 100644 --- a/lib/widgets/custom_list_tile.dart +++ b/lib/widgets/custom_list_tile.dart @@ -11,6 +11,7 @@ class CustomListTile extends StatelessWidget { final void Function()? onLongPress; final bool? disabled; final void Function(bool)? onHover; + final Color? color; const CustomListTile({ Key? key, @@ -24,12 +25,13 @@ class CustomListTile extends StatelessWidget { this.onLongPress, this.disabled, this.onHover, + this.color, }) : super(key: key); @override Widget build(BuildContext context) { return Material( - color: Colors.transparent, + color: color ?? Colors.transparent, child: InkWell( onTap: onTap, onHover: onHover, 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..0c8f9f6 --- /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({ + Key? key, + required this.data, + required this.colors, + this.animationDuration = const Duration(milliseconds: 800), + }) : super(key: key); + + @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/domain_options.dart b/lib/widgets/domain_options.dart deleted file mode 100644 index 58409d9..0000000 --- a/lib/widgets/domain_options.dart +++ /dev/null @@ -1,136 +0,0 @@ -// ignore_for_file: use_build_context_synchronously - -import 'package:flutter/material.dart'; -import 'package:contextmenu/contextmenu.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/widgets/options_modal.dart'; -import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; - -import 'package:adguard_home_manager/functions/snackbar.dart'; -import 'package:adguard_home_manager/classes/process_modal.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/models/menu_option.dart'; - -class DomainOptions extends StatelessWidget { - final bool isBlocked; - final bool? isClient; - final String? item; - final Widget child; - final void Function() onTap; - final BorderRadius? borderRadius; - - const DomainOptions({ - Key? key, - required this.isBlocked, - this.isClient, - required this.item, - required this.child, - required this.onTap, - this.borderRadius - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final statusProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - - void blockUnblock(String domain, String newStatus) async { - final ProcessModal processModal = ProcessModal(context: context); - processModal.open(AppLocalizations.of(context)!.savingUserFilters); - - final rules = await statusProvider.blockUnblockDomain( - domain: domain, - newStatus: newStatus - ); - - processModal.close(); - - if (rules == true) { - showSnacbkar( - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.userFilteringRulesUpdated, - color: Colors.green - ); - } - else { - showSnacbkar( - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.userFilteringRulesNotUpdated, - color: 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, - ) - ); - } - - List generateOptions() { - return [ - if (isClient != true && isBlocked == true) MenuOption( - title: AppLocalizations.of(context)!.unblock, - icon: Icons.check, - action: () => blockUnblock(item!, 'unblock') - ), - if (isClient != true && isBlocked == false) MenuOption( - title: AppLocalizations.of(context)!.block, - icon: Icons.block, - action: () => blockUnblock(item!, 'block') - ), - MenuOption( - title: AppLocalizations.of(context)!.copyClipboard, - icon: Icons.copy, - action: () => copyDomainClipboard(item!) - ), - ]; - } - - void openOptionsModal() { - showDialog( - context: context, - builder: (context) => OptionsModal( - options: generateOptions(), - ) - ); - } - - if (item != null) { - return Material( - color: Colors.transparent, - borderRadius: borderRadius, - child: ContextMenuArea( - builder: (context) => generateOptions().map((opt) => CustomListTile( - title: opt.title, - icon: opt.icon, - onTap: () { - opt.action(); - Navigator.pop(context); - }, - )).toList(), - child: InkWell( - onTap: onTap, - onLongPress: openOptionsModal, - borderRadius: borderRadius, - child: child, - ), - ), - ); - } - else { - return child; - } - } -} \ No newline at end of file diff --git a/lib/widgets/layout.dart b/lib/widgets/layout.dart new file mode 100644 index 0000000..9a42415 --- /dev/null +++ b/lib/widgets/layout.dart @@ -0,0 +1,297 @@ +import 'package:flutter/foundation.dart'; +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/update_modal.dart'; +import 'package:adguard_home_manager/widgets/system_ui_overlay_style.dart'; + +import 'package:adguard_home_manager/functions/check_app_updates.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/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/navigation_rail.dart b/lib/widgets/navigation_rail.dart index 063993c..2d2e1ef 100644 --- a/lib/widgets/navigation_rail.dart +++ b/lib/widgets/navigation_rail.dart @@ -9,7 +9,7 @@ import 'package:adguard_home_manager/providers/app_config_provider.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart'; class SideNavigationRail extends StatelessWidget { - const SideNavigationRail({Key? key}) : super(key: key); + const SideNavigationRail({super.key}); @override Widget build(BuildContext context) { @@ -46,12 +46,12 @@ class SideNavigationRail extends StatelessWidget { } } - if ((serversProvider.selectedServer == null || serversProvider.apiClient == null) && appConfigProvider.selectedScreen > 1) { + if ((serversProvider.selectedServer == null || serversProvider.apiClient2 == null) && appConfigProvider.selectedScreen > 1) { appConfigProvider.setSelectedScreen(0); } return NavigationRail( - selectedIndex: (serversProvider.selectedServer == null || serversProvider.apiClient == null) && appConfigProvider.selectedScreen > 1 + selectedIndex: (serversProvider.selectedServer == null || serversProvider.apiClient2 == null) && appConfigProvider.selectedScreen > 1 ? 0 : appConfigProvider.selectedScreen, destinations: screens.map((screen) => NavigationRailDestination( diff --git a/lib/widgets/option_box.dart b/lib/widgets/option_box.dart index 7c05a67..f19a88f 100644 --- a/lib/widgets/option_box.dart +++ b/lib/widgets/option_box.dart @@ -1,17 +1,17 @@ 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, required this.optionsValue, required this.itemValue, required this.onTap, + required this.label, }) : super(key: key); @override @@ -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..ad91b33 --- /dev/null +++ b/lib/widgets/options_menu.dart @@ -0,0 +1,125 @@ +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), + 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: 400 + ), + child: SingleChildScrollView( + child: Wrap( + 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/options_modal.dart b/lib/widgets/options_modal.dart deleted file mode 100644 index 4128fb2..0000000 --- a/lib/widgets/options_modal.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:adguard_home_manager/models/menu_option.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'; - -class OptionsModal extends StatelessWidget { - final List options; - - const OptionsModal({ - Key? key, - required this.options, - }) : 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: options.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 e821696..0c91442 100644 --- a/lib/widgets/process_dialog.dart +++ b/lib/widgets/process_dialog.dart @@ -24,7 +24,7 @@ class ProcessDialog extends StatelessWidget { children: [ const CircularProgressIndicator(), const SizedBox(width: 40), - Expanded( + Flexible( child: Text( message, style: TextStyle( 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..ec3c92e --- /dev/null +++ b/lib/widgets/servers_list/server_tile_functions.dart @@ -0,0 +1,166 @@ +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), () => { + 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); + showSnacbkar( + 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) { + showSnacbkar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.connectionDefaultSuccessfully, + color: Colors.green + ); + } + else { + showSnacbkar( + 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_item.dart b/lib/widgets/servers_list/servers_list_item.dart index 724be22..47ca6fe 100644 --- a/lib/widgets/servers_list/servers_list_item.dart +++ b/lib/widgets/servers_list/servers_list_item.dart @@ -5,17 +5,9 @@ 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/version_warning_modal.dart'; -import 'package:adguard_home_manager/widgets/add_server_modal.dart'; -import 'package:adguard_home_manager/widgets/servers_list/delete_modal.dart'; +import 'package:adguard_home_manager/widgets/servers_list/server_tile_functions.dart'; -import 'package:adguard_home_manager/classes/process_modal.dart'; -import 'package:adguard_home_manager/functions/snackbar.dart'; -import 'package:adguard_home_manager/constants/enums.dart'; import 'package:adguard_home_manager/providers/status_provider.dart'; -import 'package:adguard_home_manager/models/app_log.dart'; -import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; import 'package:adguard_home_manager/models/server.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart'; @@ -26,12 +18,12 @@ class ServersListItem extends StatefulWidget { final void Function(int) onChange; const ServersListItem({ - Key? key, + super.key, required this.expandableController, required this.server, required this.index, required this.onChange - }) : super(key: key); + }); @override State createState() => _ServersListItemState(); @@ -70,329 +62,8 @@ class _ServersListItemState extends State with SingleTickerProv @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; - void showDeleteModal(Server server) async { - await Future.delayed(const Duration(seconds: 0), () => { - showDialog( - context: context, - builder: (context) => DeleteModal( - serverToDelete: server, - ), - barrierDismissible: false - ) - }); - } - - void openAddServerBottomSheet({Server? server}) async { - await Future.delayed(const Duration(seconds: 0), (() => { - if (width > 700) { - showDialog( - context: context, - barrierDismissible: false, - builder: (context) => AddServerModal( - server: server, - window: true, - onUnsupportedVersion: (version) => showDialog( - context: context, - builder: (ctx) => VersionWarningModal( - version: version - ), - barrierDismissible: false - ), - ), - ) - } - else { - Navigator.push(context, MaterialPageRoute( - fullscreenDialog: true, - builder: (BuildContext context) => AddServerModal( - server: server, - window: false, - onUnsupportedVersion: (version) => showDialog( - context: context, - builder: (ctx) => VersionWarningModal( - version: version - ), - barrierDismissible: false - ), - ) - )) - } - })); - } - - void connectToServer(Server server) async { - final ProcessModal process = ProcessModal(context: context); - process.open(AppLocalizations.of(context)!.connecting); - - final result = server.runningOnHa == true - ? await loginHA(server) - : await login(server); - - if (result['result'] == 'success') { - final ApiClient apiClient = ApiClient(server: server); - serversProvider.setApiClient(apiClient); - serversProvider.setSelectedServer(server); - - statusProvider.setServerStatusLoad(LoadStatus.loading); - final serverStatus = await apiClient.getServerStatus(); - if (serverStatus['result'] == 'success') { - statusProvider.setServerStatusData( - data: serverStatus['data'] - ); - serversProvider.checkServerUpdatesAvailable( - server: server, - ); - statusProvider.setServerStatusLoad(LoadStatus.loaded); - } - else { - appConfigProvider.addLog(serverStatus['log']); - statusProvider.setServerStatusLoad(LoadStatus.error); - } - - process.close(); - } - else { - process.close(); - appConfigProvider.addLog(result['log']); - showSnacbkar( - 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( - 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( - 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 - ? 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, - ); - } - } - - Widget topRow(Server server, int index) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Row( - children: [ - Container( - margin: const EdgeInsets.only(right: 16), - child: leadingIcon(server), - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "${server.connectionMethod}://${server.domain}${server.path ?? ""}${server.port != null ? ':${server.port}' : ""}", - textAlign: TextAlign.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), - ), - ], - ); - } - - 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 == 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), - ), - ), - ) - ], - ) - ], - ); - } - return Container( decoration: BoxDecoration( border: Border( @@ -413,7 +84,7 @@ class _ServersListItemState extends State with SingleTickerProv onTap: () => widget.onChange(widget.index), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: topRow(widget.server, widget.index), + child: _TopRow(server: widget.server, animation: animation) ), ), ), @@ -425,8 +96,14 @@ class _ServersListItemState extends State with SingleTickerProv padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Column( children: [ - topRow(widget.server, widget.index), - bottomRow(widget.server, widget.index) + _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), + ) ], ), ), @@ -438,4 +115,247 @@ class _ServersListItemState extends State with SingleTickerProv ), ); } +} + +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 index 1cbd622..bf28ade 100644 --- a/lib/widgets/servers_list/servers_tile_item.dart +++ b/lib/widgets/servers_list/servers_tile_item.dart @@ -4,17 +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/widgets/version_warning_modal.dart'; -import 'package:adguard_home_manager/widgets/add_server_modal.dart'; -import 'package:adguard_home_manager/widgets/servers_list/delete_modal.dart'; +import 'package:adguard_home_manager/widgets/servers_list/server_tile_functions.dart'; -import 'package:adguard_home_manager/classes/process_modal.dart'; -import 'package:adguard_home_manager/functions/snackbar.dart'; -import 'package:adguard_home_manager/constants/enums.dart'; import 'package:adguard_home_manager/providers/status_provider.dart'; -import 'package:adguard_home_manager/models/app_log.dart'; -import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/services/http_requests.dart'; import 'package:adguard_home_manager/models/server.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart'; @@ -25,12 +17,12 @@ class ServersTileItem extends StatefulWidget { final double breakingWidth; const ServersTileItem({ - Key? key, + super.key, required this.server, required this.index, required this.onChange, required this.breakingWidth - }) : super(key: key); + }); @override State createState() => _ServersTileItemState(); @@ -40,368 +32,275 @@ class _ServersTileItemState extends State with SingleTickerProv @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; - void showDeleteModal(Server server) async { - await Future.delayed(const Duration(seconds: 0), () => { - showDialog( - context: context, - builder: (context) => DeleteModal( - serverToDelete: server, - ), - barrierDismissible: false - ) - }); - } - - void openAddServerBottomSheet({Server? server}) async { - await Future.delayed(const Duration(seconds: 0), (() => { - if (width > 700) { - showDialog( - context: context, - barrierDismissible: false, - builder: (context) => AddServerModal( - server: server, - window: true, - onUnsupportedVersion: (version) => showDialog( - context: context, - builder: (ctx) => VersionWarningModal( - version: version - ), - barrierDismissible: false - ), - ), - ) - } - else { - Navigator.push(context, MaterialPageRoute( - fullscreenDialog: true, - builder: (BuildContext context) => AddServerModal( - server: server, - window: false, - onUnsupportedVersion: (version) => showDialog( - context: context, - builder: (ctx) => VersionWarningModal( - version: version - ), - barrierDismissible: false - ), - ) - )) - } - })); - } - - void connectToServer(Server server) async { - final ProcessModal process = ProcessModal(context: context); - process.open(AppLocalizations.of(context)!.connecting); - - final result = server.runningOnHa == true - ? await loginHA(server) - : await login(server); - - if (result['result'] == 'success') { - final ApiClient apiClient = ApiClient(server: server); - serversProvider.setApiClient(apiClient); - serversProvider.setSelectedServer(server); - - statusProvider.setServerStatusLoad(LoadStatus.loading); - final serverStatus = await apiClient.getServerStatus(); - if (serverStatus['result'] == 'success') { - statusProvider.setServerStatusData( - data: serverStatus['data'] - ); - serversProvider.checkServerUpdatesAvailable( - server: server, - ); - statusProvider.setServerStatusLoad(LoadStatus.loaded); - } - else { - appConfigProvider.addLog(serverStatus['log']); - statusProvider.setServerStatusLoad(LoadStatus.error); - } - - process.close(); - } - else { - process.close(); - appConfigProvider.addLog(result['log']); - showSnacbkar( - 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( - 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( - 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 - ? 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, - ); - } - } - - Widget topRow(Server server, int index) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Row( - children: [ - Container( - margin: const EdgeInsets.only(right: 16), - child: leadingIcon(server), - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "${server.connectionMethod}://${server.domain}${server.path ?? ""}${server.port != null ? ':${server.port}' : ""}", - textAlign: TextAlign.left, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurface - ), - ), - Column( - children: [ - const SizedBox(height: 3), - Text( - server.name, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.left, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurfaceVariant - ), - ) - ], - ) - ], - ), - ), - ], - ), - ), - ], - ); - } - - Widget bottomRow(Server server, int index) { - return Column( - children: [ - 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: (() => openAddServerBottomSheet(server: server)), - child: Row( - children: [ - const Icon(Icons.edit), - const SizedBox(width: 15), - Text(AppLocalizations.of(context)!.edit) - ], - ) - ), - PopupMenuItem( - onTap: (() => showDeleteModal(server)), - child: Row( - children: [ - const Icon(Icons.delete), - const SizedBox(width: 15), - Text(AppLocalizations.of(context)!.delete) - ], - ) - ), - ] - ), - SizedBox( - child: serversProvider.selectedServer != null && - serversProvider.selectedServer != null && serversProvider.selectedServer?.id == server.id && 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), - ), - ), - ) - ], - ) - ], - ); - } - - EdgeInsets generateMargins(int index) { - if (index == 0) { - return const EdgeInsets.only(top: 16, left: 16, right: 8, bottom: 8); - } - if (index == 1) { - return const EdgeInsets.only(top: 16, left: 8, right: 16, bottom: 8); - } - else if (index == serversProvider.serversList.length-1 && (index+1)%2 == 0) { - return const EdgeInsets.only(top: 8, left: 8, right: 16, bottom: 16); - } - else if (index == serversProvider.serversList.length-1 && (index+1)%2 == 1) { - return const EdgeInsets.only(top: 8, left: 16, right: 8, bottom: 16); - } - else { - if ((index+1)%2 == 0) { - return const EdgeInsets.only(top: 8, left: 8, right: 16, bottom: 8); - } - else { - return const EdgeInsets.only(top: 8, left: 16, right: 8, bottom: 8); - } - } - } - return FractionallySizedBox( widthFactor: width > widget.breakingWidth ? 0.5 : 1, child: Card( margin: width > widget.breakingWidth - ? generateMargins(widget.index) + ? 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(widget.server, widget.index), + child: _TopRow(server: widget.server, index: widget.index) ), Padding( padding: const EdgeInsets.only( left: 8, right: 8, bottom: 16 ), - child: bottomRow(widget.server, widget.index), + 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 index d1f2ad0..92b7913 100644 --- a/lib/widgets/tab_content_list.dart +++ b/lib/widgets/tab_content_list.dart @@ -19,7 +19,7 @@ class CustomTabContentList extends StatelessWidget { final EdgeInsets? listPadding; const CustomTabContentList({ - Key? key, + super.key, required this.loadingGenerator, required this.itemsCount, required this.contentWidget, @@ -32,7 +32,7 @@ class CustomTabContentList extends StatelessWidget { this.fabVisible, this.noSliver, this.listPadding - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -49,7 +49,6 @@ class CustomTabContentList extends StatelessWidget { else { return SafeArea( top: false, - bottom: false, child: Builder( builder: (BuildContext context) => CustomScrollView( slivers: [ @@ -72,41 +71,45 @@ class CustomTabContentList extends StatelessWidget { case LoadStatus.loaded: if (noSliver == true) { if (itemsCount > 0) { - return Stack( - children: [ - ListView.builder( - padding: listPadding, - itemCount: itemsCount, - itemBuilder: (context, index) => contentWidget(index), - ), - if (fab != null) AnimatedPositioned( - duration: const Duration(milliseconds: 100), - curve: Curves.easeInOut, - bottom: fabVisible != null && fabVisible == true ? - appConfigProvider.showingSnackbar - ? 70 : 20 - : -70, - right: 20, - child: fab! - ), - ], + 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 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! - ), - ], + 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! + ), + ], + ), ); } } @@ -146,10 +149,10 @@ class CustomTabContentList extends StatelessWidget { curve: Curves.easeInOut, bottom: fabVisible != null && fabVisible == true ? appConfigProvider.showingSnackbar - ? 70 : 20 - : -70, + ? 90 : 20 + : -90, right: 20, - child: fab! + child: SafeArea(child: fab!) ), ], ); @@ -169,7 +172,6 @@ class CustomTabContentList extends StatelessWidget { else { return SafeArea( top: false, - bottom: false, child: Builder( builder: (BuildContext context) => CustomScrollView( slivers: [ diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 82a9884..31a4f66 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -17,7 +17,7 @@ import window_size func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) - FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) SentryFlutterPlugin.register(with: registry.registrar(forPlugin: "SentryFlutterPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index b38b083..9d30918 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -9,28 +9,28 @@ PODS: - FMDB/standard (2.7.5) - package_info_plus (0.0.1): - FlutterMacOS - - Sentry/HybridSDK (8.9.1): - - SentryPrivate (= 8.9.1) + - Sentry/HybridSDK (8.17.2): + - SentryPrivate (= 8.17.2) - sentry_flutter (0.0.1): - Flutter - FlutterMacOS - - Sentry/HybridSDK (= 8.9.1) - - SentryPrivate (8.9.1) + - Sentry/HybridSDK (= 8.17.2) + - SentryPrivate (8.17.2) - sqflite (0.0.2): - FlutterMacOS - FMDB (>= 2.7.5) - - sqlite3 (3.43.0): - - sqlite3/common (= 3.43.0) - - sqlite3/common (3.43.0) - - sqlite3/fts5 (3.43.0): + - sqlite3 (3.44.0): + - sqlite3/common (= 3.44.0) + - sqlite3/common (3.44.0) + - sqlite3/fts5 (3.44.0): - sqlite3/common - - sqlite3/perf-threadsafe (3.43.0): + - sqlite3/perf-threadsafe (3.44.0): - sqlite3/common - - sqlite3/rtree (3.43.0): + - sqlite3/rtree (3.44.0): - sqlite3/common - sqlite3_flutter_libs (0.0.1): - FlutterMacOS - - sqlite3 (~> 3.43.0) + - sqlite3 (~> 3.44.0) - sqlite3/fts5 - sqlite3/perf-threadsafe - sqlite3/rtree @@ -83,15 +83,15 @@ SPEC CHECKSUMS: FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce - Sentry: e3203780941722a1fcfee99e351de14244c7f806 - sentry_flutter: 8f0ffd53088e6a4d50c095852c5cad9e4405025c - SentryPrivate: 5e3683390f66611fc7c6215e27645873adb55d13 + Sentry: 64a9f9c3637af913adcf53deced05bbe452d1410 + sentry_flutter: 57912cf425e09398bdf47f38842a1fcb9836f1be + SentryPrivate: 024c6fed507ac39ae98e6d087034160f942920d5 sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea - sqlite3: 7afcf055d3700254769a4dcba56f27d26b5515c9 - sqlite3_flutter_libs: 03613b0558ba0bb5544aa3ba3d0862c09c7a19b3 + sqlite3: 6e2d4a4879854d0ec86b476bf3c3e30870bac273 + sqlite3_flutter_libs: a25f3a0f522fdcd8fef6a4a50a3d681dd43d8dea url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 window_size: 339dafa0b27a95a62a843042038fa6c3c48de195 PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7 -COCOAPODS: 1.12.1 +COCOAPODS: 1.14.3 diff --git a/pubspec.lock b/pubspec.lock index d0842e2..cf61106 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,18 +13,26 @@ packages: dependency: "direct main" description: name: animations - sha256: ef57563eed3620bd5d75ad96189846aca1e033c0c45fc9a7d26e80ab02b88a70 + sha256: d3d6dcfb218225bbe68e87ccf6378bbb2e32a94900722c5f81611dad089911cb url: "https://pub.dev" source: hosted - version: "2.0.8" + version: "2.0.11" + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "8bf17a8ff6ea17499e40a2d2542c2f481cd7615760c6d34065cb22bfd22e6880" + url: "https://pub.dev" + source: hosted + version: "2.0.2" archive: dependency: transitive description: name: archive - sha256: "49b1fad315e57ab0bbc15bcbb874e83116a1d78f77ebd500a4af6c9407d6b28e" + sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" url: "https://pub.dev" source: hosted - version: "3.3.8" + version: "3.4.10" args: dependency: transitive description: @@ -69,10 +77,10 @@ packages: dependency: transitive description: name: cli_util - sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "0.4.1" clock: dependency: transitive description: @@ -85,10 +93,10 @@ packages: dependency: transitive description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.18.0" contextmenu: dependency: "direct main" description: @@ -133,10 +141,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: "86add5ef97215562d2e090535b0a16f197902b10c369c558a100e74ea06e8659" + sha256: "0042cb3b2a76413ea5f8a2b40cec2a33e01d0c937e91f0f7c211fde4f7739ba6" url: "https://pub.dev" source: hosted - version: "9.0.3" + version: "9.1.1" device_info_plus_platform_interface: dependency: transitive description: @@ -149,10 +157,10 @@ packages: dependency: "direct main" description: name: dynamic_color - sha256: de4798a7069121aee12d5895315680258415de9b00e717723a1bd73d58f0126d + sha256: a866f1f8947bfdaf674d7928e769eac7230388a2e7a2542824fad4bb5b87be3b url: "https://pub.dev" source: hosted - version: "1.6.6" + version: "1.6.9" equatable: dependency: transitive description: @@ -193,14 +201,22 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" fl_chart: dependency: "direct main" description: name: fl_chart - sha256: "48a1b69be9544e2b03d9a8e843affd89e43f3194c9248776222efcb4206bb1ec" + sha256: fe6fec7d85975a99c73b9515a69a6e291364accfa0e4a5b3ce6de814d74b9a1c url: "https://pub.dev" source: hosted - version: "0.62.0" + version: "0.66.0" flutter: dependency: "direct main" description: flutter @@ -242,10 +258,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "3.0.1" flutter_localizations: dependency: "direct main" description: flutter @@ -255,18 +271,18 @@ packages: dependency: "direct main" description: name: flutter_markdown - sha256: d4a1cb250c4e059586af0235f32e02882860a508e189b61f2b31b8810c1e1330 + sha256: "30088ce826b5b9cfbf9e8bece34c716c8a59fa54461dcae1e4ac01a94639e762" url: "https://pub.dev" source: hosted - version: "0.6.17+2" + version: "0.6.18+3" flutter_native_splash: dependency: "direct dev" description: name: flutter_native_splash - sha256: ecff62b3b893f2f665de7e4ad3de89f738941fcfcaaba8ee601e749efafa4698 + sha256: "9cdb5d9665dab5d098dc50feab74301c2c228cd02ca25c9b546ab572cebcd6af" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.9" flutter_reorderable_list: dependency: "direct main" description: @@ -288,10 +304,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: "6ff9fa12892ae074092de2fa6a9938fb21dbabfdaa2ff57dc697ff912fc8d4b2" + sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c url: "https://pub.dev" source: hosted - version: "1.1.6" + version: "2.0.9" flutter_test: dependency: "direct dev" description: flutter @@ -319,13 +335,13 @@ packages: source: hosted version: "0.15.4" http: - dependency: transitive + dependency: "direct main" description: name: http - sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139 url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.2" http_parser: dependency: transitive description: @@ -338,10 +354,10 @@ packages: dependency: transitive description: name: image - sha256: a72242c9a0ffb65d03de1b7113bc4e189686fc07c7147b8b41811d0dd0e0d9bf + sha256: "004a2e90ce080f8627b5a04aecb4cdfac87d2c3f3b520aa291260be5a32c033d" url: "https://pub.dev" source: hosted - version: "4.0.17" + version: "4.1.4" intl: dependency: "direct main" description: @@ -370,10 +386,10 @@ packages: dependency: transitive description: name: lints - sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "3.0.0" list_counter: dependency: transitive description: @@ -410,10 +426,10 @@ packages: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" nested: dependency: transitive description: @@ -426,10 +442,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "6ff267fcd9d48cb61c8df74a82680e8b82e940231bb5f68356672fde0397334a" + sha256: "88bc797f44a94814f2213db1c9bd5badebafdfb8290ca9f78d4b9ee2a3db4d79" url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "5.0.1" package_info_plus_platform_interface: dependency: transitive description: @@ -446,14 +462,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.3" - path_drawing: - dependency: transitive - description: - name: path_drawing - sha256: bbb1934c0cbb03091af082a6389ca2080345291ef07a5fa6d6e078ba8682f977 - url: "https://pub.dev" - source: hosted - version: "1.0.1" path_parsing: dependency: transitive description: @@ -474,7 +482,15 @@ packages: dependency: transitive description: name: petitparser - sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" + pie_chart: + dependency: "direct main" + description: + name: pie_chart + sha256: "58e6a46999ac938bfa1c3e5be414d6e149f037647197dca03ba3614324c12c82" url: "https://pub.dev" source: hosted version: "5.4.0" @@ -482,42 +498,50 @@ packages: dependency: transitive description: name: plugin_platform_interface - sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "2.1.6" + version: "2.1.8" pointycastle: dependency: transitive description: name: pointycastle - sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" url: "https://pub.dev" source: hosted - version: "3.7.3" + version: "3.7.4" provider: dependency: "direct main" description: name: provider - sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" url: "https://pub.dev" source: hosted - version: "6.0.5" + version: "6.1.1" + segmented_button_slide: + dependency: "direct main" + description: + name: segmented_button_slide + sha256: "96d67344fa65f6b98a317d930bfd4e81e47c4f9e3e2ab1ee8925b55f72cded13" + url: "https://pub.dev" + source: hosted + version: "1.0.4" sentry: dependency: transitive description: name: sentry - sha256: "39c23342fc96105da449914f7774139a17a0ca8a4e70d9ad5200171f7e47d6ba" + sha256: "89e426587b0879e53c46a0aae0eb312696d9d2d803ba14b252a65cc24b1416a2" url: "https://pub.dev" source: hosted - version: "7.9.0" + version: "7.14.0" sentry_flutter: dependency: "direct main" description: name: sentry_flutter - sha256: ff68ab31918690da004a42e20204242a3ad9ad57da7e2712da8487060ac9767f + sha256: fd089ee4e75a927be037c56815a0a54af5a519f52b803a5ffecb589bb36e2401 url: "https://pub.dev" source: hosted - version: "7.9.0" + version: "7.14.0" sky_engine: dependency: transitive description: flutter @@ -531,6 +555,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" sqflite: dependency: "direct main" description: @@ -543,42 +575,42 @@ packages: dependency: transitive description: name: sqflite_common - sha256: "1b92f368f44b0dee2425bb861cfa17b6f6cf3961f762ff6f941d20b33355660a" + sha256: "76db4d324c8cbb16ca5b60ad2f3d25cec953107c93ae65aafa480d3e6fb69f14" url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.5.2-1" sqflite_common_ffi: dependency: "direct main" description: name: sqflite_common_ffi - sha256: "0d5cc1be2eb18400ac6701c31211d44164393aa75886093002ecdd947be04f93" + sha256: d0e3f0d04fdf668e57db8db1df758f56c4193cb429092c708e7bfcc6ab04b27e url: "https://pub.dev" source: hosted - version: "2.3.0+2" + version: "2.3.2" sqlite3: dependency: transitive description: name: sqlite3 - sha256: db65233e6b99e99b2548932f55a987961bc06d82a31a0665451fa0b4fff4c3fb + sha256: c4a4c5a4b2a32e2d0f6837b33d7c91a67903891a5b7dbe706cf4b1f6b0c798c5 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.3.0" sqlite3_flutter_libs: dependency: "direct main" description: name: sqlite3_flutter_libs - sha256: fb115050b0c2589afe2085a62d77f5deda4db65db20a5c65a6e0c92fda89b45e + sha256: "3e3583b77cf888a68eae2e49ee4f025f66b86623ef0d83c297c8d903daa14871" url: "https://pub.dev" source: hosted - version: "0.5.16" + version: "0.5.18" stack_trace: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" store_checker: dependency: "direct main" description: @@ -591,10 +623,10 @@ packages: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" string_scanner: dependency: transitive description: @@ -607,10 +639,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" + sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.0+1" term_glyph: dependency: transitive description: @@ -623,10 +655,10 @@ packages: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.1" typed_data: dependency: transitive description: @@ -647,74 +679,98 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27" + sha256: d25bb0ca00432a5e1ee40e69c36c85863addf7cc45e433769d61bed3fe81fd96 url: "https://pub.dev" source: hosted - version: "6.1.14" + version: "6.2.3" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: b04af59516ab45762b2ca6da40fa830d72d0f6045cd97744450b73493fa76330 + sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f" url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.2.2" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "7c65021d5dee51813d652357bc65b8dd4a6177082a9966bc8ba6ee477baa795f" + sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03" url: "https://pub.dev" source: hosted - version: "6.1.5" + version: "6.2.4" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: b651aad005e0cb06a01dbd84b428a301916dc75f0e7ea6165f80057fee2d8e8e + sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.1.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: b55486791f666e62e0e8ff825e58a023fd6b1f71c49926483f1128d3bbd8fe88 + sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "3.1.0" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: "95465b39f83bfe95fcb9d174829d6476216f2d548b79c38ab2506e0458787618" + sha256: a932c3a8082e118f80a475ce692fde89dc20fddb24c57360b96bc56f7035de1f url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.3.1" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: "2942294a500b4fa0b918685aff406773ba0a4cd34b7f42198742a94083020ce5" + sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b url: "https://pub.dev" source: hosted - version: "2.0.20" + version: "2.2.3" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "95fef3129dc7cfaba2bc3d5ba2e16063bb561fc6d78e63eee16162bc70029069" + sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.1.1" uuid: dependency: "direct main" description: name: uuid - sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8 url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "4.3.3" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "18f6690295af52d081f6808f2f7c69f0eed6d7e23a71539d75f4aeb8f0062172" + url: "https://pub.dev" + source: hosted + version: "1.1.9+2" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "531d20465c10dfac7f5cd90b60bbe4dd9921f1ec4ca54c83ebb176dbacb7bb2d" + url: "https://pub.dev" + source: hosted + version: "1.1.9+2" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "03012b0a33775c5530576b70240308080e1d5050f0faf000118c20e6463bc0ad" + url: "https://pub.dev" + source: hosted + version: "1.1.9+2" vector_math: dependency: transitive description: @@ -727,26 +783,26 @@ packages: dependency: transitive description: name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 url: "https://pub.dev" source: hosted - version: "0.1.4-beta" + version: "0.3.0" win32: dependency: transitive description: name: win32 - sha256: "9e82a402b7f3d518fb9c02d0e9ae45952df31b9bf34d77baf19da2de03fc2aaa" + sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" url: "https://pub.dev" source: hosted - version: "5.0.7" + version: "5.2.0" win32_registry: dependency: transitive description: name: win32_registry - sha256: e4506d60b7244251bc59df15656a3093501c37fb5af02105a944d73eb95be4c9 + sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" window_size: dependency: "direct main" description: @@ -760,10 +816,10 @@ packages: dependency: transitive description: name: xml - sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.5.0" yaml: dependency: transitive description: @@ -773,5 +829,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.1.0 <4.0.0" - flutter: ">=3.13.0" + dart: ">=3.2.0 <4.0.0" + flutter: ">=3.16.0" diff --git a/pubspec.yaml b/pubspec.yaml index d18eb72..e4dbaa8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 99.99.99+99 +version: 2.14.1+120 environment: sdk: '>=2.18.1 <3.0.0' @@ -39,26 +39,26 @@ dependencies: flutter_localizations: sdk: flutter intl: any - provider: ^6.0.3 + provider: ^6.1.1 sqflite: ^2.3.0 - package_info_plus: ^4.0.1 + package_info_plus: ^5.0.1 flutter_displaymode: ^0.6.0 - dynamic_color: ^1.6.5 - animations: ^2.0.5 - device_info_plus: ^9.0.1 - uuid: ^3.0.6 + dynamic_color: ^1.6.8 + animations: ^2.0.10 + device_info_plus: ^9.1.1 + uuid: ^4.2.1 expandable: ^5.0.1 - fl_chart: ^0.62.0 + fl_chart: ^0.66.0 flutter_web_browser: ^0.17.1 - flutter_svg: ^1.1.5 - percent_indicator: ^4.2.2 - store_checker: ^1.2.0 - flutter_markdown: ^0.6.14 - markdown: ^7.0.2 - html: ^0.15.2 - flutter_html: ^3.0.0-alpha.6 - sqlite3_flutter_libs: ^0.5.16 - sqflite_common_ffi: ^2.3.0+2 + flutter_svg: ^2.0.9 + percent_indicator: ^4.2.3 + store_checker: ^1.4.0 + flutter_markdown: ^0.6.18+2 + 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 window_size: git: url: https://github.com/google/flutter-desktop-embedding @@ -67,12 +67,15 @@ dependencies: git: url: https://github.com/JGeek00/flutter_split_view ref: master-alt - url_launcher: ^6.1.11 + url_launcher: ^6.2.2 contextmenu: ^3.0.0 - async: ^2.10.0 - sentry_flutter: ^7.9.0 - flutter_dotenv: ^5.0.2 + async: ^2.11.0 + sentry_flutter: ^7.13.2 + flutter_dotenv: ^5.1.0 flutter_reorderable_list: ^1.3.1 + pie_chart: ^5.4.0 + segmented_button_slide: ^1.0.4 + http: ^1.1.2 dev_dependencies: flutter_test: @@ -83,9 +86,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_lints: ^3.0.1 flutter_launcher_icons: ^0.13.1 - flutter_native_splash: ^2.2.10+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 diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt index 930d207..903f489 100644 --- a/windows/flutter/CMakeLists.txt +++ b/windows/flutter/CMakeLists.txt @@ -10,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # 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") @@ -92,7 +97,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS diff --git a/windows/innosetup_installer_builder.iss b/windows/innosetup_installer_builder.iss index b7a348d..92a4b72 100644 --- a/windows/innosetup_installer_builder.iss +++ b/windows/innosetup_installer_builder.iss @@ -37,15 +37,15 @@ Name: "spanish"; MessagesFile: "compiler:Languages\Spanish.isl" Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked [Files] -Source: "..\build\windows\runner\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion -Source: "..\build\windows\runner\Release\dynamic_color_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion -Source: "..\build\windows\runner\Release\flutter_windows.dll"; DestDir: "{app}"; Flags: ignoreversion -Source: "..\build\windows\runner\Release\sentry_flutter_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion -Source: "..\build\windows\runner\Release\sqlite3.dll"; DestDir: "{app}"; Flags: ignoreversion -Source: "..\build\windows\runner\Release\sqlite3_flutter_libs_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion -Source: "..\build\windows\runner\Release\url_launcher_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion -Source: "..\build\windows\runner\Release\window_size_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion -Source: "..\build\windows\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs +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\sentry_flutter_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_size_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] diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp index b25e363..955ee30 100644 --- a/windows/runner/flutter_window.cpp +++ b/windows/runner/flutter_window.cpp @@ -31,6 +31,11 @@ bool FlutterWindow::OnCreate() { 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; }