Compare commits

...

68 commits

Author SHA1 Message Date
Juan Gilsanz Polo
a4bf4cd3c5 Merge branch 'beta' 2025-03-23 19:52:20 +01:00
Juan Gilsanz Polo
61b0f724ba Updated app version 2025-03-23 19:51:58 +01:00
Juan Gilsanz Polo
52945b04ff Removed flutter displaymode 2025-03-23 19:46:52 +01:00
Juan Gilsanz Polo
8e5bbdbd4b Fixed update screen header color 2025-03-23 19:18:51 +01:00
Juan Gilsanz Polo
e6a01ac546 Disabled predictive back gesture 2025-03-23 19:18:43 +01:00
Juan Gilsanz Polo
e8440f7d1d Merge branch 'beta' 2025-03-17 19:13:25 +01:00
Juan Gilsanz Polo
6cc212751b Updated app version 2025-03-17 19:11:41 +01:00
Juan Gilsanz Polo
ae9d23d4a8 Support multiple subdomains 2025-03-17 19:10:06 +01:00
Juan
566254e617
Merge pull request #164 from mikropsoft/patch-1
Update app_tr.arb
2025-03-17 18:55:58 +01:00
𝗛𝗼𝗹𝗶
88339d1c40
Update app_tr.arb 2025-03-09 23:07:05 +03:00
𝗛𝗼𝗹𝗶
254bbcef57
Update app_tr.arb 2025-03-09 23:05:28 +03:00
𝗛𝗼𝗹𝗶
e7aacfbec1
Update app_tr.arb 2025-03-09 23:04:45 +03:00
Juan Gilsanz Polo
7a75a67701 Merge branch 'beta' 2025-03-09 20:37:16 +01:00
Juan Gilsanz Polo
7632d9ef87 Updated app version 2025-03-09 20:36:45 +01:00
Juan Gilsanz Polo
eded494024 Support TLD greater than 6 characters on domains 2025-03-09 20:28:00 +01:00
Juan Gilsanz Polo
e5979edf63 Added upstream dns timeout 2025-03-09 20:17:19 +01:00
Juan Gilsanz Polo
f27b17aad0 Fix filter logs by added client 2025-03-09 19:58:24 +01:00
Juan Gilsanz Polo
fd4daba2aa Merge branch 'master' into beta 2025-03-09 19:45:58 +01:00
Juan Gilsanz Polo
ce7a8e8cc5 Extracted some regexps to external file 2025-03-09 19:45:48 +01:00
Juan Gilsanz Polo
4282792ebd Updated stuff 2025-03-09 19:45:37 +01:00
Juan Gilsanz Polo
4766d1907f Merge branch 'beta' 2025-01-22 14:45:52 +01:00
Juan Gilsanz Polo
7f6f686b2b Updated app version 2025-01-22 14:45:38 +01:00
Juan Gilsanz Polo
02b659c1bc Changed my other apps text 2025-01-22 14:43:20 +01:00
Juan Gilsanz Polo
db6e63c4aa Fix bad performance debug mode 2025-01-22 14:41:04 +01:00
Juan Gilsanz Polo
a666d109d9 Bug fixes 2025-01-22 14:40:39 +01:00
Juan Gilsanz Polo
1f23f1f3ca Merge branch 'beta' 2025-01-22 00:00:36 +01:00
Juan Gilsanz Polo
51b8a6b610 Updated app version 2025-01-22 00:00:26 +01:00
Juan Gilsanz Polo
28034d4b74 Updated app version 2025-01-21 22:00:06 +01:00
Juan Gilsanz Polo
10ff5183f1 Added sorting options custom rules 2025-01-21 21:54:04 +01:00
Juan Gilsanz Polo
47b5313bf3 Removed unused imports 2025-01-21 20:56:56 +01:00
Juan Gilsanz Polo
791400f565 Added settings links 2025-01-21 20:55:54 +01:00
Juan Gilsanz Polo
06c9f7c771 Added support for custom time in logs settings 2025-01-21 20:47:24 +01:00
Juan Gilsanz Polo
2a0db84959 Updated libraries and other stuff 2025-01-21 20:47:10 +01:00
Juan Gilsanz Polo
74cade6553 Merge branch 'beta' 2024-10-01 18:50:46 +02:00
Juan Gilsanz Polo
1b5f258c96 Fixed windows installer builder 2024-10-01 18:50:37 +02:00
Juan Gilsanz Polo
aa511f8c42 Updated macos pods 2024-10-01 16:21:00 +02:00
Juan Gilsanz Polo
c28d2440b1 Merge branch 'beta' 2024-10-01 15:00:27 +02:00
Juan Gilsanz Polo
9d4c002813 Updated app version 2024-10-01 15:00:18 +02:00
Juan Gilsanz Polo
d7392e4b8d Updated compileSdkVersion to 35 2024-10-01 14:59:24 +02:00
Juan
4dc54794bd
Merge pull request #152 from navhaxs/master
Fix for not working 'path' parameter for reverse proxy
2024-09-29 22:26:37 +02:00
Jeremy Wong
a36335ef92 Fix missing 'path' in serverObj 2024-09-29 08:41:07 +10:00
Juan Gilsanz Polo
9096367843 Merge branch 'beta' 2024-09-12 14:47:31 +02:00
Juan Gilsanz Polo
0d0321a5ab Updated app version 2024-09-12 14:47:21 +02:00
Juan Gilsanz Polo
d903da0051 Removed app updates notifier due to a bug on the library 2024-09-12 14:33:32 +02:00
Juan Gilsanz Polo
b65dc35cb7 Exclude some sentry logs 2024-09-11 22:43:26 +02:00
Juan Gilsanz Polo
7bb7ad40c4 Fixed some bugs 2024-09-11 22:32:26 +02:00
Juan Gilsanz Polo
7579e2d580 Updated workflows 2024-09-11 21:14:58 +02:00
Juan Gilsanz Polo
6eb1d73ca4 Merge branch 'beta' 2024-09-11 19:46:53 +02:00
Juan Gilsanz Polo
3055c3582b Updated workflow 2024-09-11 19:46:45 +02:00
Juan Gilsanz Polo
f9f7e8c2c9 Merge branch 'beta' 2024-09-11 19:34:11 +02:00
Juan Gilsanz Polo
fc2305266f Updated workflow 2024-09-11 19:34:04 +02:00
Juan Gilsanz Polo
6df7d89867 Merge branch 'beta' 2024-09-11 19:27:11 +02:00
Juan Gilsanz Polo
67bc6a1716 Updated workflow 2024-09-11 19:27:04 +02:00
Juan Gilsanz Polo
391d1da1ff Merge branch 'beta' 2024-09-11 19:25:56 +02:00
Juan Gilsanz Polo
a171eda41c Updated workflow 2024-09-11 19:25:49 +02:00
Juan Gilsanz Polo
6e0f437c6b Merge branch 'beta' 2024-09-11 19:17:02 +02:00
Juan Gilsanz Polo
2f1e8d38a8 Updated app version 2024-09-11 19:16:41 +02:00
Juan Gilsanz Polo
c0f7449a95 Fixed some colors 2024-09-11 19:05:07 +02:00
Juan Gilsanz Polo
b223076dae Fix bottom padding top items list 2024-09-11 18:53:23 +02:00
Juan Gilsanz Polo
9eb200f5da Changed design top items screen 2024-09-11 18:51:57 +02:00
Juan Gilsanz Polo
f9149056fd Fixed logs search bar 2024-09-11 18:21:46 +02:00
Juan Gilsanz Polo
29af26f118 Added padding bottom list live logs 2024-09-11 18:18:20 +02:00
Juan Gilsanz Polo
f7c3ba0374 Fixed most of the warnings 2024-09-11 18:13:26 +02:00
Juan Gilsanz Polo
5bf2db8a52 Merge branch 'beta' 2024-06-24 03:02:45 +02:00
Juan Gilsanz Polo
1c5f0e46f5 Merge branch 'beta' 2024-06-08 21:45:56 +02:00
Juan Gilsanz Polo
38d2955a8c Merge branch 'beta' 2024-04-24 18:13:01 +02:00
Juan Gilsanz Polo
c4ee498d62 Merge branch 'beta' 2024-04-24 18:01:41 +02:00
Juan Gilsanz Polo
1e84fbfcb0 Merge branch 'beta' 2024-04-24 18:01:08 +02:00
138 changed files with 1612 additions and 1713 deletions

View file

@ -1,123 +0,0 @@
name: Compile and release beta build
on:
workflow_dispatch:
branches:
- beta
jobs:
build-android:
name: Build Android .apk and .aab
runs-on: ubuntu-latest
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
with:
ref: beta
- name: Decode android/app/keystore.jks
run: echo "${{ secrets.KEYSTORE_JKS }}" | base64 --decode > android/app/keystore.jks
- name: Decode android/key.properties
run: echo "${{ secrets.KEY_PROPERTIES }}" | base64 --decode > android/key.properties
- name: Decode .env
run: echo "${{ secrets.ENV }}" | base64 --decode > .env
- 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
run: sed -i 's/#{KEYSTORE_KEY_PASS}#/${{ secrets.KEYSTORE_KEY_PASS }}/g' android/key.properties
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: '18.x'
- uses: subosito/flutter-action@v2
with:
channel: "stable"
- run: flutter clean
- run: flutter pub get
- 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_${{ env.VERSION_NAME }}_Android.apk
- name: Rename 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_${{ env.VERSION_NAME }}_Android.apk AdGuardHomeManager_${{ env.VERSION_NAME }}_Android.apk
- name: Copy aab to project root
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_${{ env.VERSION_NAME }}_Android.aab
AdGuardHomeManager_${{ env.VERSION_NAME }}_Android.apk
release-builds-github:
name: Release beta build to GitHub
runs-on: ubuntu-latest
needs: [build-android]
env:
VERSION_NAME: ${{ needs.build-android.outputs.VERSION_NAME }}
VERSION_NUMBER: ${{ needs.build-android.outputs.VERSION_NUMBER }}
steps:
- uses: actions/checkout@v3
with:
ref: beta
- name: Create builds directory
run: mkdir releases
- name: Download Android artifacts
uses: actions/download-artifact@v3
with:
name: android
path: releases/
- name: Release to GitHub
uses: ncipollo/release-action@v1
with:
artifacts: "releases/*"
token: ${{ secrets.GH_TOKEN }}
tag: '${{ env.VERSION_NAME }}_(${{ env.VERSION_NUMBER }})'
name: v${{ env.VERSION_NAME }}
draft: true
prerelease: true
commit: ${{ github.sha }}
release-build-google-play:
name: Release Android beta build to the Google Play Store
runs-on: ubuntu-latest
needs: [build-android]
env:
VERSION_NAME: ${{ needs.build-android.outputs.VERSION_NAME }}
steps:
- uses: actions/checkout@v3
with:
ref: beta
- name: Download Android artifacts
uses: actions/download-artifact@v3
with:
name: android
- name: Release app to Google Play
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.PLAYSTORE_ACCOUNT_KEY }}
packageName: com.jgeek00.adguard_home_manager
releaseFiles: AdGuardHomeManager_${{ env.VERSION_NAME }}_Android.aab
track: beta
status: draft
releaseName: ${{ env.VERSION_NAME }}

View file

@ -1,302 +0,0 @@
name: Compile and release production build
on:
workflow_dispatch:
branches:
- master
jobs:
build-android:
name: Build Android .apk and .aab
runs-on: ubuntu-latest
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
run: echo "${{ secrets.KEYSTORE_JKS }}" | base64 --decode > android/app/keystore.jks
- name: Decode android/key.properties
run: echo "${{ secrets.KEY_PROPERTIES }}" | base64 --decode > android/key.properties
- name: Decode .env
run: echo "${{ secrets.ENV }}" | base64 --decode > .env
- 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
run: sed -i 's/#{KEYSTORE_KEY_PASS}#/${{ secrets.KEYSTORE_KEY_PASS }}/g' android/key.properties
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: '18.x'
- uses: subosito/flutter-action@v2
with:
channel: "stable"
- run: flutter clean
- run: flutter pub get
- 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_${{ env.VERSION_NAME }}_Android.apk
- name: Rename 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_${{ env.VERSION_NAME }}_Android.apk AdGuardHomeManager_${{ env.VERSION_NAME }}_Android.apk
- name: Copy aab to project root
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_${{ 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: 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
run: cp -r $MACOS_APP_RELEASE_PATH/AdGuard\ Home\ Manager.app $MACOS_APP_RELEASE_PATH/AdGuard\ Home\ Manager/AdGuard\ Home\ Manager.app
- 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_${{ env.VERSION_NAME }}_macOS_Universal.dmg
- name: Copy dmg to project root
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_${{ 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: 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/<REPLACE_VERSION_NUMBER_ACTIONS>/${{ env.VERSION_NAME }}/g' debian/debian.yaml
- name: Update dependencies list
run: sudo apt-get update
- name: Install dependencies
run: sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev
- uses: subosito/flutter-action@v2
with:
channel: "stable"
- run: flutter clean
- run: flutter pub get
- run: flutter build linux --release
- name: Install flutter_to_debian
run: dart pub global activate flutter_to_debian
- name: Generate .deb package
run: flutter_to_debian
- name: Move .deb package to project root
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
with:
command: c
cwd: build/linux/x64/release/bundle
files: |
./data
./lib
./AdGuardHomeManager
outPath: AdGuardHomeManager_${{ env.VERSION_NAME }}_Linux.tar.gz
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: linux
path: |
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: 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
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 '<REPLACE_VERSION_ACTIONS>', '${{ env.VERSION_NAME }}' | Out-File -encoding ASCII windows/innosetup_installer_builder.iss
- uses: subosito/flutter-action@v2
with:
channel: "stable"
- run: flutter clean
- run: flutter pub get
- run: flutter build windows --release
- 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_${{ env.VERSION_NAME }}_Windows_x64.exe
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: windows
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
run: mkdir releases
- name: Download Android artifacts
uses: actions/download-artifact@v3
with:
name: android
path: releases/
- name: Download macOS artifacts
uses: actions/download-artifact@v3
with:
name: macos
path: releases/
- name: Download Linux artifacts
uses: actions/download-artifact@v3
with:
name: linux
path: releases/
- name: Download Windows artifacts
uses: actions/download-artifact@v3
with:
name: windows
path: releases/
- name: Release to GitHub
uses: ncipollo/release-action@v1
with:
artifacts: "releases/*"
token: ${{ secrets.GH_TOKEN }}
tag: '${{ env.VERSION_NAME }}_(${{ env.VERSION_NUMBER }})'
name: v${{ env.VERSION_NAME }}
draft: true
prerelease: false
commit: ${{ github.sha }}
release-build-google-play:
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
uses: actions/download-artifact@v3
with:
name: android
- name: Release app to Google Play
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.PLAYSTORE_ACCOUNT_KEY }}
packageName: com.jgeek00.adguard_home_manager
releaseFiles: AdGuardHomeManager_${{ env.VERSION_NAME }}_Android.aab
track: production
status: draft
releaseName: ${{ env.VERSION_NAME }}

6
.gitignore vendored
View file

@ -5,9 +5,11 @@
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# Env
@ -48,4 +50,6 @@ app.*.map.json
/debian/packages
untranslated.json
untranslated.json
android/app/.cxx

View file

@ -12,5 +12,9 @@
"editor.formatOnSave": false,
"editor.formatOnPaste": false,
"editor.formatOnType": false
}
},
"cSpell.ignorePaths": [
"/pubspec.yaml",
"/.github/workflows"
],
}

View file

@ -113,7 +113,6 @@ If you like the project and you want to contribute with the development, you can
- [expandable](https://pub.dev/packages/expandable)
- [package info plus](https://pub.dev/packages/package_info_plus)
- [flutter phoenix](https://pub.dev/packages/flutter_phoenix)
- [flutter displaymode](https://pub.dev/packages/flutter_displaymode)
- [flutter launcher icons](https://pub.dev/packages/flutter_launcher_icons)
- [flutter native splash](https://pub.dev/packages/flutter_native_splash)
- [intl](https://pub.dev/packages/intl)
@ -142,7 +141,6 @@ If you like the project and you want to contribute with the development, you can
- [flutter custom tabs](https://pub.dev/packages/flutter_custom_tabs)
- [shared preferences](https://pub.dev/packages/shared_preferences)
- [window manager](https://pub.dev/packages/window_manager)
- [install referrer](https://pub.dev/packages/install_referrer)
<br>

View file

@ -29,8 +29,9 @@ if (keystorePropertiesFile.exists()) {
}
android {
compileSdkVersion 34
ndkVersion flutter.ndkVersion
namespace "com.jgeek00.adguard_home_manager"
compileSdkVersion 35
ndkVersion "26.1.10909125"
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@ -46,12 +47,9 @@ android {
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.jgeek00.adguard_home_manager"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
minSdkVersion 26
targetSdkVersion 34
targetSdkVersion 35
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}

View file

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip

View file

@ -19,7 +19,7 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version '7.4.2' apply false
id "com.android.application" version '8.6.0' apply false
id "org.jetbrains.kotlin.android" version "1.8.20" apply false
}

2
debian/debian.yaml vendored
View file

@ -5,7 +5,7 @@ flutter_app:
control:
Package: AdGuardHomeManager
Version: <REPLACE_VERSION_NUMBER_ACTIONS>
Version: 2.20.1
Architecture: amd64
Essential: no
Priority: optional

View file

@ -24,11 +24,15 @@ ThemeData lightTheme(ColorScheme? dynamicColorScheme) => ThemeData(
navigationBarTheme: NavigationBarThemeData(
surfaceTintColor: dynamicColorScheme?.surfaceTint
),
pageTransitionsTheme: const PageTransitionsTheme(
builders: {
TargetPlatform.android: PredictiveBackPageTransitionsBuilder()
}
)
dialogTheme: DialogTheme(
surfaceTintColor: dynamicColorScheme?.surfaceTint
),
// DISABLE PREDICTIVE BACK GESTURE
// pageTransitionsTheme: const PageTransitionsTheme(
// builders: {
// TargetPlatform.android: PredictiveBackPageTransitionsBuilder()
// }
// )
);
ThemeData darkTheme(ColorScheme? dynamicColorScheme) => ThemeData(
@ -56,11 +60,15 @@ ThemeData darkTheme(ColorScheme? dynamicColorScheme) => ThemeData(
navigationBarTheme: NavigationBarThemeData(
surfaceTintColor: dynamicColorScheme?.surfaceTint
),
pageTransitionsTheme: const PageTransitionsTheme(
builders: {
TargetPlatform.android: PredictiveBackPageTransitionsBuilder()
}
)
dialogTheme: DialogTheme(
surfaceTintColor: dynamicColorScheme?.surfaceTint
),
// DISABLE PREDICTIVE BACK GESTURE
// pageTransitionsTheme: const PageTransitionsTheme(
// builders: {
// TargetPlatform.android: PredictiveBackPageTransitionsBuilder()
// }
// )
);
ThemeData lightThemeOldVersions(MaterialColor primaryColor) => ThemeData(
@ -79,11 +87,12 @@ ThemeData lightThemeOldVersions(MaterialColor primaryColor) => ThemeData(
iconColor: Color.fromRGBO(117, 117, 117, 1),
),
brightness: Brightness.light,
pageTransitionsTheme: const PageTransitionsTheme(
builders: {
TargetPlatform.android: PredictiveBackPageTransitionsBuilder()
}
)
// DISABLE PREDICTIVE BACK GESTURE
// pageTransitionsTheme: const PageTransitionsTheme(
// builders: {
// TargetPlatform.android: PredictiveBackPageTransitionsBuilder()
// }
// )
);
ThemeData darkThemeOldVersions(MaterialColor primaryColor) => ThemeData(
@ -105,9 +114,10 @@ ThemeData darkThemeOldVersions(MaterialColor primaryColor) => ThemeData(
iconColor: Color.fromRGBO(187, 187, 187, 1),
),
brightness: Brightness.dark,
pageTransitionsTheme: const PageTransitionsTheme(
builders: {
TargetPlatform.android: PredictiveBackPageTransitionsBuilder()
}
)
// DISABLE PREDICTIVE BACK GESTURE
// pageTransitionsTheme: const PageTransitionsTheme(
// builders: {
// TargetPlatform.android: PredictiveBackPageTransitionsBuilder()
// }
// )
);

View file

@ -1,2 +1,3 @@
enum LoadStatus { loading, loaded, error }
enum HomeTopItems { queriedDomains, blockedDomains, recurrentClients, topUpstreams, avgUpstreamResponseTime }
enum HomeTopItems { queriedDomains, blockedDomains, recurrentClients, topUpstreams, avgUpstreamResponseTime }
enum CustomRulesSorting { topBottom, bottomTop }

View file

@ -0,0 +1,12 @@
class Regexps {
static final wildcardDomain = RegExp(r'^(\*\.)?(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,10}$');
static final domain = RegExp(r'^((?:(?:[a-zA-Z]{1})|(?:[a-zA-Z]{1}[a-zA-Z]{1})|(?:[a-zA-Z]{1}[0-9]{1})|(?:[0-9]{1}[a-zA-Z]{1})|(?:[a-zA-Z0-9][a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]))\.)+([a-zA-Z]{2,10}|[a-zA-Z0-9-]{2,30}\.[a-zA-Z]{2,10})$');
static final ipv4Address = RegExp(r'^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$');
static final ipv6Address = RegExp(r'(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))');
static final subroute = RegExp(r'^\/\b([A-Za-z0-9_\-~/]*)[^\/|\.|\:]$');
static final macAddress = RegExp(r'^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$');
static final url = RegExp(r'^(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})$');
static final certificate = RegExp(r'(-{3,}(\bBEGIN CERTIFICATE\b))|(-{3,}-{3,}(\END CERTIFICATE\b)-{3,})', multiLine: true);
static final privateKey = RegExp(r'(-{3,}(\bBEGIN\b).*(PRIVATE KEY\b))|(-{3,}-{3,}(\bEND\b).*(PRIVATE KEY\b)-{3,})', multiLine: true);
static final path = RegExp(r'^(\/{0,1}(?!\/))[A-Za-z0-9\/\-_]+(\.([a-zA-Z]+))?$');
}

View file

@ -7,4 +7,6 @@ class Urls {
static const String adGuardHomeReleasesTags = "https://api.github.com/repos/AdGuardTeam/AdGuardHome/releases/tags";
static const String googleSearchUrl = "https://www.google.com/search";
static const String connectionInstructions = "https://github.com/JGeek00/adguard-home-manager/wiki/Create-a-connection";
static const String appDetailsWebpage = "https://apps.jgeek00.com/jlfed8mcgyz6laf";
static const String jgeek00AppsWebpage = "https://apps.jgeek00.com";
}

View file

@ -1,57 +0,0 @@
import 'dart:io';
import 'package:install_referrer/install_referrer.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';
Future<GitHubRelease?> checkAppUpdates({
required String currentBuildNumber,
required void Function(GitHubRelease?) setUpdateAvailable,
required InstallationAppReferrer? installationSource,
required bool isBeta
}) async {
var result = isBeta
? await ExternalRequests.getReleasesGitHub()
: await ExternalRequests.getReleaseData();
if (result.successful == true) {
late GitHubRelease gitHubRelease;
if (isBeta) {
gitHubRelease = (result.content as List<GitHubRelease>).firstWhere((r) => r.prerelease == true);
}
else {
gitHubRelease = result.content as GitHubRelease;
}
final update = gitHubUpdateExists(
currentBuildNumber: currentBuildNumber,
gitHubRelease: gitHubRelease,
isBeta: isBeta
);
if (update == true) {
setUpdateAvailable(gitHubRelease);
if (Platform.isAndroid) {
if (installationSource == InstallationAppReferrer.androidManually) {
return gitHubRelease;
}
else {
return null;
}
}
else if (Platform.isIOS) {
return null;
}
else {
return gitHubRelease;
}
}
else {
setUpdateAvailable(null);
}
}
return null;
}

View file

@ -1,7 +1,7 @@
import 'package:adguard_home_manager/constants/regexps.dart';
bool isIpAddress(String value) {
final ipv4Regexp = RegExp(r'^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$');
final ipv6Regexp = RegExp(r'(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))');
if (ipv4Regexp.hasMatch(value) || ipv6Regexp.hasMatch(value)) {
if (Regexps.ipv4Address.hasMatch(value) || Regexps.ipv6Address.hasMatch(value)) {
return true;
}
else {

View file

@ -5,6 +5,15 @@ import 'package:url_launcher/url_launcher.dart' as url_launcher;
import 'package:sentry_flutter/sentry_flutter.dart';
void openUrl(String url) async {
if (!(url.startsWith("http") || url.startsWith("https"))) {
try {
url_launcher.launchUrl(Uri.parse(url));
} catch (e, stackTrace) {
Sentry.captureException(e, stackTrace: stackTrace);
}
return;
}
if (Platform.isAndroid || Platform.isIOS) {
try {
await flutter_custom_tabs.launchUrl(
@ -20,6 +29,7 @@ void openUrl(String url) async {
),
);
} catch (e, stackTrace) {
url_launcher.launchUrl(Uri.parse(url));
Sentry.captureException(e, stackTrace: stackTrace);
}
}

View file

@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
import 'package:adguard_home_manager/providers/app_config_provider.dart';
import 'package:adguard_home_manager/config/globals.dart';
void showSnacbkar({
void showSnackbar({
required AppConfigProvider appConfigProvider,
required String label,
required Color color,

View file

@ -797,5 +797,14 @@
"dnsServerAddressCopied": "DNS server address copied to the clipboard",
"select": "Select",
"liveLogs": "Live logs",
"hereWillAppearRealtimeLogs": "Here there will appear the logs on realtime."
"hereWillAppearRealtimeLogs": "Here there will appear the logs on realtime.",
"applicationDetails": "Application details",
"applicationDetailsDescription": "App repository, stores where it's available, and more",
"myOtherApps": "My other apps",
"myOtherAppsDescription": "Check my other apps, make a donation, contact support, and more",
"topToBottom": "From top to bottom",
"bottomToTop": "From bottom to top",
"upstreamTimeout": "Upstream timeout",
"upstreamTimeoutHelper": "Specifies the number of seconds to wait for a response from the upstream server",
"fieldCannotBeEmpty": "This field cannot be empty"
}

View file

@ -403,7 +403,7 @@
"dnsRewriteRuleDeleted": "Reescritura DNS eliminada correctamente",
"dnsRewriteRuleNotDeleted": "La reescritura DNS no pudo ser eliminada",
"addDnsRewrite": "Añadir reescritura DNS",
"addingRewrite": "Añadiend reescritura...",
"addingRewrite": "Añadiendo reescritura...",
"dnsRewriteRuleAdded": "Regla de reescritura DNS añadida correctamente",
"dnsRewriteRuleNotAdded": "La regla de reescritura DNS no ha podido ser añadida",
"logsSettings": "Ajustes de registros",
@ -797,5 +797,14 @@
"dnsServerAddressCopied": "Dirección del servidor DNS copiada al portapapeles",
"select": "Seleccionar",
"liveLogs": "Registros en directo",
"hereWillAppearRealtimeLogs": "Aquí aparecerán los registros en tiempo real."
"hereWillAppearRealtimeLogs": "Aquí aparecerán los registros en tiempo real.",
"applicationDetails": "Detalles de la aplicación",
"applicationDetailsDescription": "Repositorio de la app, tiendas donde está disponible, y más",
"myOtherApps": "Mis otras apps",
"myOtherAppsDescription": "Comprueba mis otras apps, hacer una donación, contactar al soporte, y más",
"topToBottom": "Desde arriba hacia abajo",
"bottomToTop": "Desde abajo hacia arriba",
"upstreamTimeout": "Tiempo de espera del upstream",
"upstreamTimeoutHelper": "Especifica el número de segundos que se debe esperar para recibir una respuesta del servidor upstream",
"fieldCannotBeEmpty": "El campo no puede estar vacío"
}

View file

@ -791,5 +791,20 @@
"clientDisallowedSuccessfully": "İstemci başarıyla reddedildi",
"changesNotSaved": "Değişiklikler kaydedilemedi",
"allowingClient": "İstemciye izin veriliyor...",
"disallowingClient": "İstemci reddediliyor..."
}
"disallowingClient": "İstemci reddediliyor...",
"clientIpCopied": "İstemci IP'si panoya kopyalandı",
"clientNameCopied": "İstemci adı panoya kopyalandı",
"dnsServerAddressCopied": "DNS sunucu adresi panoya kopyalandı",
"select": "Seç",
"liveLogs": "Canlı günlükler",
"hereWillAppearRealtimeLogs": "Burada gerçek zamanlı günlükler görünecek.",
"applicationDetails": "Uygulama detayları",
"applicationDetailsDescription": "Uygulama deposu, mevcut olduğu mağazalar ve daha fazlası",
"myOtherApps": "Diğer uygulamalarım",
"myOtherAppsDescription": "Diğer uygulamalarımı kontrol et, bağış yap, destekle iletişime geç ve daha fazlası",
"topToBottom": "Yukarıdan aşağıya",
"bottomToTop": "Aşağıdan yukarıya",
"upstreamTimeout": "Üst sunucu zaman aşımı",
"upstreamTimeoutHelper": "Üst DNS sunucusundan yanıt bekleme süresini saniye cinsinden belirtir",
"fieldCannotBeEmpty": "Bu alan boş olamaz"
}

View file

@ -4,9 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:install_referrer/install_referrer.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';
@ -79,11 +77,6 @@ void main() async {
HttpOverrides.global = MyHttpOverrides();
}
if (Platform.isAndroid || Platform.isIOS) {
InstallationAppReferrer installationSource = await InstallReferrer.referrer;
appConfigProvider.setInstallationSource(installationSource);
}
final dbData = await loadDb();
serversProvider.setDbInstance(dbData['dbInstance']);
serversProvider.saveFromDb(dbData['servers']);
@ -169,6 +162,31 @@ void main() async {
(options) {
options.dsn = dotenv.env['SENTRY_DSN'];
options.sendDefaultPii = false;
options.beforeSend = (event, hint) {
if (event.throwable is HttpException) {
return null;
}
if (event.message?.formatted.contains("HttpException") == true) {
return null;
}
if (
event.message?.formatted.contains("Unexpected character") ?? false ||
(event.throwable != null && event.throwable!.toString().contains("Unexpected character"))
) {
return null;
}
if (
event.message?.formatted.contains("Unexpected end of input") ?? false ||
(event.throwable != null && event.throwable!.toString().contains("Unexpected end of input"))
) {
return null;
}
return event;
};
},
appRunner: () => startApp()
);
@ -178,37 +196,9 @@ void main() async {
}
}
class Main extends StatefulWidget {
class Main extends StatelessWidget {
const Main({super.key});
@override
State<Main> createState() => _MainState();
}
class _MainState extends State<Main> {
List<DisplayMode> modes = <DisplayMode>[];
DisplayMode? active;
DisplayMode? preferred;
Future<void> displayMode() async {
try {
modes = await FlutterDisplayMode.supported;
preferred = await FlutterDisplayMode.preferred;
active = await FlutterDisplayMode.active;
await FlutterDisplayMode.setHighRefreshRate();
setState(() {});
} catch (_) {
// ---- //
}
}
@override
void initState() {
displayMode();
super.initState();
}
@override
Widget build(BuildContext context) {
final appConfigProvider = Provider.of<AppConfigProvider>(context);

View file

@ -26,6 +26,7 @@ class DnsInfo {
int? ratelimitSubnetLenIpv4;
int? ratelimitSubnetLenIpv6;
List<String>? ratelimitWhitelist;
int? upstreamTimeout;
DnsInfo({
required this.upstreamDns,
@ -55,6 +56,7 @@ class DnsInfo {
required this.ratelimitSubnetLenIpv4,
required this.ratelimitSubnetLenIpv6,
required this.ratelimitWhitelist,
required this.upstreamTimeout,
});
factory DnsInfo.fromJson(Map<String, dynamic> json) => DnsInfo(
@ -85,6 +87,7 @@ class DnsInfo {
ratelimitSubnetLenIpv4: json["ratelimit_subnet_len_ipv4"],
ratelimitSubnetLenIpv6: json["ratelimit_subnet_len_ipv6"],
ratelimitWhitelist: json["ratelimit_whitelist"] != null ? List<String>.from(json["ratelimit_whitelist"].map((x) => x)) : [],
upstreamTimeout: json["upstream_timeout"],
);
Map<String, dynamic> toJson() => {
@ -115,5 +118,6 @@ class DnsInfo {
"ratelimit_subnet_len_ipv4": ratelimitSubnetLenIpv4,
"ratelimit_subnet_len_ipv6": ratelimitSubnetLenIpv6,
"ratelimit_whitelist": ratelimitWhitelist != null ? List<String>.from(ratelimitWhitelist!.map((x) => x)) : null,
"upstream_timeout": upstreamTimeout,
};
}

View file

@ -5,7 +5,7 @@ DnsStatistics dnsStatisticsFromJson(String str) => DnsStatistics.fromJson(json.d
String dnsStatisticsToJson(DnsStatistics data) => json.encode(data.toJson());
class DnsStatistics {
final String timeUnits;
final String? timeUnits;
final List<Map<String, int>> topQueriedDomains;
final List<Map<String, int>> topClients;
final List<Map<String, int>> topBlockedDomains;
@ -43,9 +43,9 @@ class DnsStatistics {
factory DnsStatistics.fromJson(Map<String, dynamic> json) => DnsStatistics(
timeUnits: json["time_units"],
topQueriedDomains: List<Map<String, int>>.from(json["top_queried_domains"].map((x) => Map.from(x).map((k, v) => MapEntry<String, int>(k, v)))),
topClients: List<Map<String, int>>.from(json["top_clients"].map((x) => Map.from(x).map((k, v) => MapEntry<String, int>(k, v)))),
topBlockedDomains: List<Map<String, int>>.from(json["top_blocked_domains"].map((x) => Map.from(x).map((k, v) => MapEntry<String, int>(k, v)))),
topQueriedDomains: json["top_queried_domains"] != null ? List<Map<String, int>>.from(json["top_queried_domains"].map((x) => Map.from(x).map((k, v) => MapEntry<String, int>(k, v)))) : [],
topClients: json["top_clients"] != null ? List<Map<String, int>>.from(json["top_clients"].map((x) => Map.from(x).map((k, v) => MapEntry<String, int>(k, v)))) : [],
topBlockedDomains: json["top_blocked_domains"] != null ? List<Map<String, int>>.from(json["top_blocked_domains"].map((x) => Map.from(x).map((k, v) => MapEntry<String, int>(k, v)))): [],
topUpstreamResponses: json["top_upstreams_responses"] != null ? List<Map<String, int>>.from(json["top_upstreams_responses"].map((x) => Map.from(x).map((k, v) => MapEntry<String, int>(k, v)))) : null,
topUpstreamsAvgTime: json["top_upstreams_avg_time"] != null ? List<Map<String, double>>.from(json["top_upstreams_avg_time"].map((x) => Map.from(x).map((k, v) => MapEntry<String, double>(k, v)))) : null,
dnsQueries: List<int>.from(json["dns_queries"].map((x) => x)),

View file

@ -63,7 +63,7 @@ class Filter {
Map<String, dynamic> toJson() => {
"url": url,
"name": name,
"last_updated": lastUpdated != null ? lastUpdated!.toIso8601String() : null,
"last_updated": lastUpdated?.toIso8601String(),
"id": id,
"rules_count": rulesCount,
"enabled": enabled,

View file

@ -65,7 +65,7 @@ class Filter {
Map<String, dynamic> toJson() => {
"url": url,
"name": name,
"last_updated": lastUpdated != null ? lastUpdated!.toIso8601String() : null,
"last_updated": lastUpdated?.toIso8601String(),
"id": id,
"rules_count": rulesCount,
"enabled": enabled,

View file

@ -30,7 +30,7 @@ class LogsData {
Map<String, dynamic> toJson() => {
"data": List<dynamic>.from(data.map((x) => x.toJson())),
"oldest": oldest != null ? oldest!.toIso8601String() : null,
"oldest": oldest?.toIso8601String(),
};
}

View file

@ -2,7 +2,6 @@
import 'package:flutter/material.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/scheduler.dart';
import 'package:install_referrer/install_referrer.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:package_info_plus/package_info_plus.dart';
@ -33,7 +32,7 @@ class AppConfigProvider with ChangeNotifier {
int _selectedTheme = 0;
bool _useDynamicColor = true;
int _staticColor = 0;
bool _useThemeColorForStatus = false;
final bool _useThemeColorForStatus = false;
int _selectedClientsTab = 0;
int _selectedFiltersTab = 0;
@ -58,8 +57,6 @@ class AppConfigProvider with ChangeNotifier {
GitHubRelease? _appUpdatesAvailable;
InstallationAppReferrer? _installationSource;
PackageInfo? get getAppInfo {
return _appInfo;
}
@ -162,10 +159,6 @@ class AppConfigProvider with ChangeNotifier {
return _appUpdatesAvailable;
}
InstallationAppReferrer? get installationSource {
return _installationSource;
}
List<HomeTopItems> get homeTopItemsOrder {
return _homeTopItemsOrder;
}
@ -227,11 +220,6 @@ class AppConfigProvider with ChangeNotifier {
notifyListeners();
}
void setInstallationSource(InstallationAppReferrer value) {
_installationSource = value;
notifyListeners();
}
Future<bool> setOverrideSslCheck(bool status) async {
try {
sharedPreferencesInstance.setBool('overrideSslCheck', status);

View file

@ -151,20 +151,26 @@ class DnsProvider with ChangeNotifier {
final result = await _serversProvider!.apiClient2!.setDnsConfig(
data: value
);
void updateValue(dynamic parameter, dynamic value) {
if (value != null) {
parameter = value;
}
}
if (result.successful == true) {
DnsInfo data = dnsInfo!;
data.ratelimit = value['ratelimit'];
data.ednsCsEnabled = value['edns_cs_enabled'];
data.dnssecEnabled = value['dnssec_enabled'];
data.disableIpv6 = value['disable_ipv6'];
data.blockingMode = value['blocking_mode'];
data.blockingIpv4 = value['blocking_ipv4'];
data.blockingIpv6 = value['blocking_ipv6'];
data.blockedResponseTtl = value['blocked_response_ttl'];
data.ratelimitSubnetLenIpv4 = value['ratelimit_subnet_len_ipv4'];
data.ratelimitSubnetLenIpv6 = value['ratelimit_subnet_len_ipv6'];
data.ratelimitWhitelist = value['ratelimit_whitelist'];
updateValue(data.ratelimit, value['ratelimit']);
updateValue(data.ednsCsEnabled, value['edns_cs_enabled']);
updateValue(data.dnssecEnabled, value['dnssec_enabled']);
updateValue(data.disableIpv6, value['disable_ipv6']);
updateValue(data.blockingMode, value['blocking_mode']);
updateValue(data.blockingIpv4, value['blocking_ipv4']);
updateValue(data.blockingIpv6, value['blocking_ipv6']);
updateValue(data.blockedResponseTtl, value['blocked_response_ttl']);
updateValue(data.ratelimitSubnetLenIpv4, value['ratelimit_subnet_len_ipv4']);
updateValue(data.ratelimitSubnetLenIpv6, value['ratelimit_subnet_len_ipv6']);
updateValue(data.ratelimitWhitelist, value['ratelimit_whitelist']);
setDnsInfoData(data);
return result;
}

View file

@ -82,14 +82,14 @@ class _AddedListState extends State<AddedList> {
processModal.close();
if (result == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.clientUpdatedSuccessfully,
color: Colors.green
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.clientNotUpdated,
color: Colors.red
@ -109,14 +109,14 @@ class _AddedListState extends State<AddedList> {
if (widget.splitView == true) {
Navigator.of(clientsNavigatorKey.currentContext!).popUntil((route) => false);
}
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.clientDeletedSuccessfully,
color: Colors.green
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.clientNotDeleted,
color: Colors.red

View file

@ -1,11 +1,11 @@
import 'package:flutter/material.dart';
class ClientPlaceholder extends StatelessWidget {
const ClientPlaceholder({Key? key}) : super(key: key);
const ClientPlaceholder({super.key});
@override
Widget build(BuildContext context) {
return Center(
return const Center(
child: Text("Select a client"),
);
}

View file

@ -5,9 +5,9 @@ class RemoveClientModal extends StatelessWidget {
final void Function() onConfirm;
const RemoveClientModal({
Key? key,
super.key,
required this.onConfirm
}) : super(key: key);
});
@override
Widget build(BuildContext context) {

View file

@ -11,11 +11,11 @@ class SafeSearchModal extends StatefulWidget {
final void Function(SafeSearch) onConfirm;
const SafeSearchModal({
Key? key,
super.key,
required this.safeSearch,
required this.disabled,
required this.onConfirm
}) : super(key: key);
});
@override
State<SafeSearchModal> createState() => _SafeSearchModalState();

View file

@ -10,10 +10,10 @@ class ServicesModal extends StatefulWidget {
final void Function(List<String>) onConfirm;
const ServicesModal({
Key? key,
super.key,
required this.blockedServices,
required this.onConfirm,
}) : super(key: key);
});
@override
State<ServicesModal> createState() => _ServicesModalStateWidget();

View file

@ -25,22 +25,26 @@ class ClientsFab extends StatelessWidget {
final width = MediaQuery.of(context).size.width;
void confirmAddClient(Client client) async {
if (!context.mounted) return;
ProcessModal processModal = ProcessModal();
processModal.open(AppLocalizations.of(context)!.addingClient);
final result = await clientsProvider.addClient(client);
if (!context.mounted) return;
processModal.close();
if (result == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.clientAddedSuccessfully,
color: Colors.green
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.clientNotAdded,
color: Colors.red

View file

@ -95,14 +95,14 @@ class _SearchClientsState extends State<SearchClients> {
processModal.close();
if (result == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.clientDeletedSuccessfully,
color: Colors.green
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.clientNotDeleted,
color: Colors.red
@ -119,14 +119,14 @@ class _SearchClientsState extends State<SearchClients> {
processModal.close();
if (result == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.clientUpdatedSuccessfully,
color: Colors.green
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.clientNotUpdated,
color: Colors.red
@ -205,7 +205,7 @@ class _SearchClientsState extends State<SearchClients> {
height: 1,
decoration: BoxDecoration(
color: showDivider == true
? Theme.of(context).colorScheme.surfaceVariant
? Theme.of(context).colorScheme.surfaceContainerHighest
: Colors.transparent
),
),

View file

@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:adguard_home_manager/widgets/add_server/add_server_functions.dart';
class FabConnect extends StatelessWidget {
const FabConnect({Key? key}) : super(key: key);
const FabConnect({super.key});
@override
Widget build(BuildContext context) {

View file

@ -33,6 +33,8 @@ class AddFiltersButton extends StatelessWidget {
final width = MediaQuery.of(context).size.width;
void confirmAddRule(String rule) async {
if (!context.mounted) return;
ProcessModal processModal = ProcessModal();
processModal.open(AppLocalizations.of(context)!.addingRule);
@ -42,14 +44,14 @@ class AddFiltersButton extends StatelessWidget {
if (!context.mounted) return;
if (result == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.ruleAddedSuccessfully,
color: Colors.green
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.ruleNotAdded,
color: Colors.red
@ -58,6 +60,8 @@ class AddFiltersButton extends StatelessWidget {
}
void confirmEditCustomRules(List<String> rules) async {
if (!context.mounted) return;
ProcessModal processModal = ProcessModal();
processModal.open(AppLocalizations.of(context)!.savingCustomRules);
@ -67,14 +71,14 @@ class AddFiltersButton extends StatelessWidget {
if (!context.mounted) return;
if (result == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.customRulesUpdatedSuccessfully,
color: Colors.green
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.customRulesNotUpdated,
color: Colors.red
@ -137,6 +141,8 @@ class AddFiltersButton extends StatelessWidget {
}
void confirmAddList({required String name, required String url, required String type}) async {
if (!context.mounted) return;
ProcessModal processModal = ProcessModal();
processModal.open(AppLocalizations.of(context)!.addingList);
@ -146,28 +152,28 @@ class AddFiltersButton extends StatelessWidget {
if (!context.mounted) return;
if (result['success'] == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: "${AppLocalizations.of(context)!.listAdded} ${result['data']}.",
color: Colors.green
);
}
else if (result['success'] == false && result['error'] == 'invalid_url') {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.listUrlInvalid,
color: Colors.red
);
}
else if (result['success'] == false && result['error'] == 'url_exists') {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.listAlreadyAdded,
color: Colors.red
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.listNotAdded,
color: Colors.red

View file

@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/filters/add_button.dart';
import 'package:adguard_home_manager/screens/filters/modals/custom_rules/sort_rules.dart';
import 'package:adguard_home_manager/widgets/tab_content_list.dart';
import 'package:adguard_home_manager/functions/snackbar.dart';
@ -34,6 +35,8 @@ class CustomRulesList extends StatefulWidget {
class _CustomRulesListState extends State<CustomRulesList> {
late bool isVisible;
CustomRulesSorting _sortingMethod = CustomRulesSorting.topBottom;
@override
initState(){
super.initState();
@ -60,6 +63,8 @@ class _CustomRulesListState extends State<CustomRulesList> {
final filteringProvider = Provider.of<FilteringProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final renderData = _sortingMethod == CustomRulesSorting.bottomTop ? widget.data.reversed.toList() : widget.data.toList();
bool checkIfComment(String value) {
final regex = RegExp(r'^(!|#).*$');
if (regex.hasMatch(value)) {
@ -104,6 +109,16 @@ class _CustomRulesListState extends State<CustomRulesList> {
}
}
void showSortingMethodModal() {
showDialog(
context: context,
builder: (ctx) => SortCustomRulesModal(
sortingMethod: _sortingMethod,
onSelect: (value) => setState(() => _sortingMethod = value),
),
);
}
return CustomTabContentList(
loadingGenerator: () => SizedBox(
width: double.maxFinite,
@ -124,10 +139,10 @@ class _CustomRulesListState extends State<CustomRulesList> {
],
),
),
itemsCount: widget.data.length,
itemsCount: renderData.length,
contentWidget: (index) => ListTile(
title: Text(
widget.data[index],
renderData[index],
style: TextStyle(
color: checkIfComment(widget.data[index]) == true
? Theme.of(context).colorScheme.onSurface.withOpacity(0.6)
@ -135,9 +150,9 @@ class _CustomRulesListState extends State<CustomRulesList> {
fontWeight: FontWeight.normal,
),
),
subtitle: generateSubtitle(widget.data[index]),
subtitle: generateSubtitle(renderData[index]),
trailing: IconButton(
onPressed: () => widget.onRemoveCustomRule(widget.data[index]),
onPressed: () => widget.onRemoveCustomRule(renderData[index]),
icon: const Icon(Icons.delete)
),
),
@ -162,8 +177,8 @@ class _CustomRulesListState extends State<CustomRulesList> {
TextButton.icon(
onPressed: () async {
final result = await filteringProvider.fetchFilters();
if (result == false) {
showSnacbkar(
if (result == false && context.mounted) {
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.errorLoadFilters,
color: Colors.red
@ -203,7 +218,7 @@ class _CustomRulesListState extends State<CustomRulesList> {
onRefresh: () async {
final result = await filteringProvider.fetchFilters();
if (result == false) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.errorLoadFilters,
color: Colors.red
@ -212,6 +227,11 @@ class _CustomRulesListState extends State<CustomRulesList> {
},
fab: Column(
children: [
FloatingActionButton.small(
onPressed: showSortingMethodModal,
child: Icon(Icons.sort_rounded),
),
const SizedBox(height: 16),
AddFiltersButton(
type: 'edit_custom_rule',
widget: (fn) => FloatingActionButton.small(
@ -229,7 +249,7 @@ class _CustomRulesListState extends State<CustomRulesList> {
),
],
),
heightFabHidden: -120,
heightFabHidden: -180,
fabVisible: isVisible,
);
}

View file

@ -1,5 +1,6 @@
import 'dart:io';
import 'package:adguard_home_manager/constants/regexps.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -97,8 +98,7 @@ class _ContentState extends State<_Content> {
}
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)) {
if (Regexps.url.hasMatch(value)) {
setState(() => urlError = null);
}
else {

View file

@ -1,5 +1,6 @@
// ignore_for_file: use_build_context_synchronously
import 'package:adguard_home_manager/constants/regexps.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -63,8 +64,7 @@ class _ContentState extends State<_Content> {
Widget? resultWidget;
void validateDomain(String value) {
final domainRegex = RegExp(r'^([a-z0-9|-]+\.)*[a-z0-9|-]+\.[a-z]+$');
if (domainRegex.hasMatch(value)) {
if (Regexps.domain.hasMatch(value)) {
setState(() => domainError = null);
}
else {

View file

@ -94,14 +94,14 @@ class _ListDetailsScreenState extends State<ListDetailsScreen> {
);
processModal.close();
if (result == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.listDataUpdated,
color: Colors.green
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.listDataNotUpdated,
color: Colors.red
@ -175,6 +175,7 @@ class _ListDetailsScreenState extends State<ListDetailsScreen> {
_Content(
isDialog: widget.dialog,
list: list,
type: widget.type,
)
],
),
@ -216,6 +217,7 @@ class _ListDetailsScreenState extends State<ListDetailsScreen> {
_Content(
isDialog: widget.dialog,
list: list,
type: widget.type,
)
],
),
@ -261,10 +263,12 @@ class _ListDetailsScreenState extends State<ListDetailsScreen> {
class _Content extends StatelessWidget {
final Filter list;
final bool isDialog;
final String type;
const _Content({
required this.list,
required this.isDialog
required this.isDialog,
required this.type,
});
@override
@ -339,7 +343,7 @@ class _Content extends StatelessWidget {
CustomListTile(
icon: Icons.shield_rounded,
title: AppLocalizations.of(context)!.listType,
subtitle: isDialog == 'whitelist'
subtitle: type == 'whitelist'
? AppLocalizations.of(context)!.whitelist
: AppLocalizations.of(context)!.blacklist,
padding: isDialog == true
@ -431,7 +435,7 @@ class _Actions extends StatelessWidget {
);
processModal.close();
if (result == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.listDeleted,
color: Colors.green
@ -439,7 +443,7 @@ class _Actions extends StatelessWidget {
Navigator.pop(context);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.listNotDeleted,
color: Colors.red

View file

@ -24,7 +24,7 @@ import 'package:adguard_home_manager/constants/enums.dart';
import 'package:adguard_home_manager/models/clients.dart';
class Filters extends StatefulWidget {
const Filters({Key? key}) : super(key: key);
const Filters({super.key});
@override
State<Filters> createState() => _FiltersState();
@ -57,14 +57,14 @@ class _FiltersState extends State<Filters> {
if (!mounted) return;
processModal.close();
if (result['success'] == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: "${result['data']['updated']} ${AppLocalizations.of(context)!.listsUpdated}",
color: Colors.green
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.listsNotUpdated,
color: Colors.red
@ -109,14 +109,14 @@ class _FiltersState extends State<Filters> {
processModal.close();
if (result == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.filteringStatusUpdated,
color: Colors.green
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.filteringStatusNotUpdated,
color: Colors.red
@ -133,14 +133,14 @@ class _FiltersState extends State<Filters> {
processModal.close();
if (result == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.updateFrequencyChanged,
color: Colors.green
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.updateFrequencyNotChanged,
color: Colors.red
@ -163,14 +163,14 @@ class _FiltersState extends State<Filters> {
processModal.close();
if (result == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.ruleRemovedSuccessfully,
color: Colors.green
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.ruleNotRemoved,
color: Colors.red

View file

@ -27,13 +27,13 @@ class FiltersList extends StatefulWidget {
final void Function(Filter, String) onOpenDetailsScreen;
const FiltersList({
Key? key,
super.key,
required this.loadStatus,
required this.scrollController,
required this.data,
required this.type,
required this.onOpenDetailsScreen
}) : super(key: key);
});
@override
State<FiltersList> createState() => _FiltersListState();
@ -133,7 +133,7 @@ class _FiltersListState extends State<FiltersList> {
onPressed: () async {
final result = await filteringProvider.fetchFilters();
if (result == false && mounted) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.errorLoadFilters,
color: Colors.red
@ -173,7 +173,7 @@ class _FiltersListState extends State<FiltersList> {
onRefresh: () async {
final result = await filteringProvider.fetchFilters();
if (result == false && mounted) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.errorLoadFilters,
color: Colors.red

View file

@ -84,7 +84,7 @@ class FiltersTripleColumn extends StatelessWidget {
onPressed: () async {
final result = await filteringProvider.fetchFilters();
if (result == false && context.mounted) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.errorLoadFilters,
color: Colors.red

View file

@ -59,14 +59,14 @@ class ListOptionsMenu extends StatelessWidget {
if (!context.mounted) return;
if (result == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.listDataUpdated,
color: Colors.green
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.listDataNotUpdated,
color: Colors.red

View file

@ -68,14 +68,14 @@ class _BlockedServicesScreenStateWidget extends State<BlockedServicesScreen> {
processModal.close();
if (result == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.blockedServicesUpdated,
color: Colors.green
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.blockedServicesNotUpdated,
color: Colors.red

View file

@ -1,3 +1,4 @@
import 'package:adguard_home_manager/constants/regexps.dart';
import 'package:flutter/material.dart';
import 'package:segmented_button_slide/segmented_button_slide.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -41,8 +42,7 @@ class _AddCustomRuleState extends State<AddCustomRule> {
}
void validateDomain(String value) {
final domainRegex = RegExp(r'^(([a-zA-Z]{1})|([a-zA-Z]{1}[a-zA-Z]{1})|([a-zA-Z]{1}[0-9]{1})|([0-9]{1}[a-zA-Z]{1})|([a-zA-Z0-9][a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]))\.([a-zA-Z]{2,6}|[a-zA-Z0-9-]{2,30}\.[a-zA-Z]{2,3})$');
if (domainRegex.hasMatch(value)) {
if (Regexps.domain.hasMatch(value)) {
setState(() => _domainError = null);
}
else {

View file

@ -0,0 +1,128 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/constants/enums.dart';
class SortCustomRulesModal extends StatelessWidget {
final CustomRulesSorting sortingMethod;
final void Function(CustomRulesSorting) onSelect;
const SortCustomRulesModal({
super.key,
required this.sortingMethod,
required this.onSelect,
});
@override
Widget build(BuildContext context) {
return AlertDialog(
contentPadding: const EdgeInsets.symmetric(vertical: 16),
scrollable: true,
title: Column(
children: [
Icon(
Icons.sort_rounded,
size: 24,
color: Theme.of(context).listTileTheme.iconColor
),
const SizedBox(height: 16),
Text(
AppLocalizations.of(context)!.sortingOptions,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface
),
)
],
),
content: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 500
),
child: Column(
children: [
_CustomListTileDialog(
title: AppLocalizations.of(context)!.topToBottom,
icon: Icons.arrow_downward_rounded,
onTap: () {
Navigator.pop(context);
onSelect(CustomRulesSorting.topBottom);
},
isSelected: sortingMethod == CustomRulesSorting.topBottom
),
_CustomListTileDialog(
title: AppLocalizations.of(context)!.bottomToTop,
icon: Icons.arrow_upward_rounded,
onTap: () {
Navigator.pop(context);
onSelect(CustomRulesSorting.bottomTop);
},
isSelected: sortingMethod == CustomRulesSorting.bottomTop
),
]
),
),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(AppLocalizations.of(context)!.close)
)
],
)
],
);
}
}
class _CustomListTileDialog extends StatelessWidget {
final String title;
final IconData? icon;
final void Function()? onTap;
final bool isSelected;
const _CustomListTileDialog({
required this.title,
required this.icon,
required this.onTap,
required this.isSelected,
});
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: Row(
children: [
Icon(
icon,
color: Theme.of(context).colorScheme.onSurface,
),
const SizedBox(width: 24),
Flexible(
child: Text(
title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurface,
),
),
),
const SizedBox(width: 24),
Icon(
isSelected == true ? Icons.radio_button_checked_rounded : Icons.radio_button_unchecked_rounded,
color: Theme.of(context).colorScheme.primary,
)
],
),
),
),
);
}
}

View file

@ -5,9 +5,9 @@ class RemoveCustomRule extends StatelessWidget {
final void Function() onConfirm;
const RemoveCustomRule({
Key? key,
super.key,
required this.onConfirm
}) : super(key: key);
});
@override
Widget build(BuildContext context) {

View file

@ -235,10 +235,10 @@ class _Content extends StatelessWidget {
}
: null,
style: ButtonStyle(
overlayColor: MaterialStateProperty.all(
overlayColor: WidgetStateProperty.all(
Theme.of(context).colorScheme.primary.withOpacity(0.1)
),
foregroundColor: MaterialStateProperty.all(
foregroundColor: WidgetStateProperty.all(
selectedOption != null
? Theme.of(context).colorScheme.primary
: Colors.grey,

View file

@ -9,11 +9,11 @@ class DeleteSelectionModal extends StatefulWidget {
final void Function() onDelete;
const DeleteSelectionModal({
Key? key,
super.key,
required this.selectedBlacklists,
required this.selectedWhitelists,
required this.onDelete,
}) : super(key: key);
});
@override
State<DeleteSelectionModal> createState() => _DeleteSelectionModalState();

View file

@ -9,11 +9,11 @@ class EnableDisableSelectionModal extends StatefulWidget {
final void Function() onDelete;
const EnableDisableSelectionModal({
Key? key,
super.key,
required this.selectedBlacklists,
required this.selectedWhitelists,
required this.onDelete,
}) : super(key: key);
});
@override
State<EnableDisableSelectionModal> createState() => _EnableDisableSelectionModalState();

View file

@ -13,13 +13,13 @@ class SelectionList extends StatelessWidget {
final void Function() unselectAll;
const SelectionList({
Key? key,
super.key,
required this.lists,
required this.selectedLists,
required this.onSelect,
required this.selectAll,
required this.unselectAll,
}) : super(key: key);
});
@override
Widget build(BuildContext context) {
@ -231,11 +231,10 @@ class _CheckboxTile extends StatelessWidget {
final bool isSelected;
const _CheckboxTile({
Key? key,
required this.list,
required this.onSelect,
required this.isSelected,
}) : super(key: key);
});
@override
Widget build(BuildContext context) {

View file

@ -82,8 +82,8 @@ class _SelectionScreenState extends State<SelectionScreen> with TickerProviderSt
blacklists: _selectedBlacklists,
whitelists: _selectedWhitelists
);
if (!mounted) return;
processModal.close();
if (!context.mounted) return;
showDialog(
context: context,
builder: (ctx) => SelectionResultModal(
@ -113,8 +113,8 @@ class _SelectionScreenState extends State<SelectionScreen> with TickerProviderSt
blacklists: _selectedBlacklists,
whitelists: _selectedWhitelists
);
if (!mounted) return;
processModal.close();
if (!context.mounted) return;
showDialog(
context: context,
builder: (ctx) => SelectionResultModal(

View file

@ -34,6 +34,7 @@ class HomeAppBar extends StatelessWidget {
void navigateServers() {
Future.delayed(const Duration(milliseconds: 0), (() {
if (!context.mounted) return;
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const Servers())
);

View file

@ -37,7 +37,7 @@ class CombinedChartItem {
}
class CombinedHomeChart extends StatelessWidget {
const CombinedHomeChart({Key? key}) : super(key: key);
const CombinedHomeChart({super.key});
List<int>? removeZero(List<int> list) {
final removed = list.where((i) => i > 0);
@ -266,12 +266,11 @@ class _Legend extends StatelessWidget {
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) {

View file

@ -7,7 +7,7 @@ import 'package:adguard_home_manager/providers/status_provider.dart';
import 'package:adguard_home_manager/constants/enums.dart';
class HomeFab extends StatelessWidget {
const HomeFab({Key? key}) : super(key: key);
const HomeFab({super.key});
@override
Widget build(BuildContext context) {

View file

@ -93,7 +93,7 @@ class _HomeState extends State<Home> {
onRefresh: () async {
final result = await statusProvider.getServerStatus();
if (mounted && result == false) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.serverStatusNotRefreshed,
color: Colors.red

View file

@ -82,7 +82,7 @@ class _ManagementModalState extends State<ManagementModal> with SingleTickerProv
time: time
);
if (mounted && result == false) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.invalidUsernamePassword,
color: Colors.red

View file

@ -8,13 +8,13 @@ class SmallSwitch extends StatelessWidget {
final bool disabled;
const SmallSwitch({
Key? key,
super.key,
required this.label,
required this.icon,
required this.value,
required this.onChange,
required this.disabled,
}) : super(key: key);
});
@override
Widget build(BuildContext context) {

View file

@ -83,14 +83,14 @@ class TopItemsLists extends StatelessWidget {
if (!context.mounted) return;
if (rules == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.userFilteringRulesUpdated,
color: Colors.green
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.userFilteringRulesNotUpdated,
color: Colors.red
@ -121,21 +121,21 @@ class TopItemsLists extends StatelessWidget {
processModal.close();
if (result.successful == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.clientAddedSuccessfully,
color: Colors.green
);
}
else if (result.successful == false && result.content == 'client_another_list') {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.clientAnotherList,
color: Colors.red
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: newList == AccessSettingsList.allowed || newList == AccessSettingsList.disallowed
? AppLocalizations.of(context)!.clientNotRemoved

View file

@ -9,6 +9,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/widgets/options_menu.dart';
import 'package:adguard_home_manager/widgets/custom_list_tile.dart';
import 'package:adguard_home_manager/widgets/floating_search_bar.dart';
import 'package:adguard_home_manager/models/menu_option.dart';
import 'package:adguard_home_manager/constants/enums.dart';
@ -16,6 +17,7 @@ import 'package:adguard_home_manager/functions/number_format.dart';
import 'package:adguard_home_manager/providers/status_provider.dart';
enum _SortingOptions { highestToLowest, lowestToHighest }
final GlobalKey _searchButtonKey = GlobalKey();
class TopItemsScreen extends StatefulWidget {
final HomeTopItems type;
@ -47,15 +49,18 @@ class TopItemsScreen extends StatefulWidget {
class _TopItemsScreenState extends State<TopItemsScreen> {
_SortingOptions _sortingOptions = _SortingOptions.highestToLowest;
bool searchActive = false;
final TextEditingController searchController = TextEditingController();
String? _currentSearchValue = "";
List<Map<String, dynamic>> data = [];
List<Map<String, dynamic>> screenData = [];
void search(String value) {
List<Map<String, dynamic>> newValues = widget.data.where((item) => item.keys.toList()[0].contains(value)).toList();
setState(() => screenData = newValues);
setState(() {
screenData = newValues;
_currentSearchValue = searchController.text;
});
}
@override
@ -75,118 +80,132 @@ class _TopItemsScreenState extends State<TopItemsScreen> {
final sortedValues = _sortingOptions == _SortingOptions.lowestToHighest
? screenData.reversed.toList()
: screenData.toList();
void showSearchDialog() {
showDialog(
context: context,
builder: (context) => FloatingSearchBar(
existingSearchValue: _currentSearchValue,
searchButtonRenderBox: _searchButtonKey.currentContext?.findRenderObject() as RenderBox?,
onSearchCompleted: (v) {
List<Map<String, dynamic>> newValues = widget.data.where((item) => item.keys.toList()[0].contains(v)).toList();
setState(() {
screenData = newValues;
_currentSearchValue = v;
});
},
),
);
}
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
return Scaffold(
body: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar.large(
title: Text(widget.title),
actions: [
IconButton(
key: _searchButtonKey,
onPressed: showSearchDialog,
icon: const Icon(Icons.search_rounded),
tooltip: AppLocalizations.of(context)!.search,
),
PopupMenuButton(
icon: const Icon(Icons.sort_rounded),
itemBuilder: (context) => [
PopupMenuItem(
onTap: () => setState(() => _sortingOptions = _SortingOptions.highestToLowest),
child: Row(
children: [
const Icon(Icons.arrow_downward_rounded),
const SizedBox(width: 8),
Expanded(
child: Text(AppLocalizations.of(context)!.fromHighestToLowest)
),
const SizedBox(width: 16),
Icon(
_sortingOptions == _SortingOptions.highestToLowest
? Icons.radio_button_checked_rounded
: Icons.radio_button_unchecked_rounded,
color: _sortingOptions == _SortingOptions.highestToLowest
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurfaceVariant,
)
],
)
),
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,
)
],
)
],
)
),
],
),
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: 8)
],
)
)
],
body: SafeArea(
top: false,
bottom: false,
child: Builder(
builder: (context) => CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
if (sortedValues.isEmpty) Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text(
AppLocalizations.of(context)!.noItemsSearch,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 22,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
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,
)
],
)
),
),
),
if (sortedValues.isNotEmpty) SliverPadding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewPadding.bottom),
sliver: SliverList.builder(
itemCount: sortedValues.length,
itemBuilder: (context, index) => _Item(
data: sortedValues[index],
isClient: widget.isClient,
options: widget.options,
total: total,
withProgressBar: widget.withProgressBar,
onTapEntry: widget.onTapEntry,
buildValue: widget.buildValue,
),
),
),
],
),
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,
),
),
)
)
),
);
}
@ -242,14 +261,29 @@ class _TopItemsScreenState extends State<TopItemsScreen> {
),
),
Expanded(
child: _Content(
buildValue: widget.buildValue,
isClient: widget.isClient,
onTapEntry: widget.onTapEntry,
options: widget.options,
screenData: sortedValues,
total: total,
withProgressBar: widget.withProgressBar,
child: sortedValues.isNotEmpty ? ListView.builder(
itemCount: sortedValues.length,
itemBuilder: (context, index) => _Item(
data: sortedValues[index],
isClient: widget.isClient,
options: widget.options,
withProgressBar: widget.withProgressBar,
onTapEntry: widget.onTapEntry,
buildValue: widget.buildValue,
total: total,
),
) : Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text(
AppLocalizations.of(context)!.noItemsSearch,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 22,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
),
),
],
@ -260,8 +294,8 @@ class _TopItemsScreenState extends State<TopItemsScreen> {
}
}
class _Content extends StatelessWidget {
final List<Map<String, dynamic>> screenData;
class _Item extends StatelessWidget {
final dynamic data;
final bool? isClient;
final List<MenuOption> Function(dynamic) options;
final bool withProgressBar;
@ -269,8 +303,8 @@ class _Content extends StatelessWidget {
final String Function(dynamic) buildValue;
final double total;
const _Content({
required this.screenData,
const _Item({
required this.data,
required this.isClient,
required this.options,
required this.withProgressBar,
@ -283,98 +317,75 @@ class _Content extends StatelessWidget {
Widget build(BuildContext context) {
final statusProvider = Provider.of<StatusProvider>(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),
],
),
],
)
),
);
}
);
String? name;
if (isClient != null && isClient == true) {
try {
name = statusProvider.serverStatus!.clients.firstWhere((c) => c.ids.contains(data.keys.toList()[0])).name;
} catch (e) {
// ---- //
}
}
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,
),
return OptionsMenu(
options: options,
value: data.keys.toList()[0],
onTap: onTapEntry != null
? (v) {
onTapEntry!(v);
Navigator.pop(context);
}
: null,
child: CustomListTile(
title: data.keys.toList()[0],
trailing: Text(
buildValue(data.values.toList()[0]),
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant
),
),
);
}
subtitleWidget: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (name != null) ...[
Text(
name,
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurface
),
),
const SizedBox(height: 5),
],
if (withProgressBar == true) Row(
children: [
SizedBox(
width: 50,
child: Text(
"${doubleFormat((data.values.toList()[0]/total*100), Platform.localeName)}%",
style: TextStyle(
color: Theme.of(context).listTileTheme.textColor
),
),
),
const SizedBox(width: 10),
Flexible(
child: LinearPercentIndicator(
animation: true,
lineHeight: 4,
animationDuration: 500,
curve: Curves.easeOut,
percent: data.values.toList()[0]/total,
barRadius: const Radius.circular(5),
progressColor: Theme.of(context).colorScheme.primary,
backgroundColor: Theme.of(context).colorScheme.surfaceTint.withOpacity(0.2),
),
),
const SizedBox(width: 10),
],
),
],
)
),
);
}
}

View file

@ -52,14 +52,14 @@ class LogDetailsScreen extends StatelessWidget {
processModal.close();
if (rules == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.userFilteringRulesUpdated,
color: Colors.green
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.userFilteringRulesNotUpdated,
color: Colors.red
@ -139,7 +139,6 @@ class LogDetailsScreen extends StatelessWidget {
);
}
else {
print(twoColumns);
return Scaffold(
body: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [

View file

@ -114,10 +114,12 @@ class _ClientsModalState extends State<ClientsModal> {
}
void searchAddedClient(_ClientLog client) {
final notIps = client.ids?.where((e) => isIpAddress(e) == false).toList();
if (notIps == null) return;
logsProvider.setSearchText('"${notIps[0]}"');
final ips = client.ids?.where((e) => isIpAddress(e) == true).toList();
if (ips == null || ips.isEmpty) return;
logsProvider.setSearchText(ips[0]);
logsProvider.filterLogs();
Navigator.of(context).pop();
Navigator.pop(context);
}
if (widget.dialog == true) {
@ -302,7 +304,7 @@ class _ClientsModalState extends State<ClientsModal> {
subtitle: _selectedList == 0 ? _filteredClients[index].name : _filteredClients[index].ids?.join(", "),
checkboxActive: logsProvider.selectedClients.contains(_filteredClients[index].ip),
isAddedClient: _selectedList == 1,
onSearchAddedClient: () => searchAddedClient(_filteredClients[index]),
onSearchAddedClient: _filteredClients[index].ids != null && _filteredClients[index].ids!.where((e) => isIpAddress(e) == true).isNotEmpty ? () => searchAddedClient(_filteredClients[index]) : null,
onChanged: (isSelected) {
if (isSelected == true) {
logsProvider.setSelectedClients([
@ -376,7 +378,7 @@ class _ListItem extends StatelessWidget {
final bool checkboxActive;
final void Function(bool) onChanged;
final bool isAddedClient;
final void Function() onSearchAddedClient;
final void Function()? onSearchAddedClient;
const _ListItem({
required this.title,

View file

@ -51,34 +51,37 @@ class LiveLogsList extends StatelessWidget {
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontSize: 24
fontSize: 22
),
),
),
),
),
if (liveLogsProvider.logs.isNotEmpty) SliverList.builder(
itemCount: liveLogsProvider.logs.length,
itemBuilder: (context, index) => LogTile(
log: liveLogsProvider.logs[index],
length: liveLogsProvider.logs.length,
index: index,
onLogTap: (log) {
if (!twoColumns) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => LogDetailsScreen(
log: log,
dialog: false,
twoColumns: twoColumns,
if (liveLogsProvider.logs.isNotEmpty) SliverPadding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewPadding.bottom),
sliver: SliverList.builder(
itemCount: liveLogsProvider.logs.length,
itemBuilder: (context, index) => LogTile(
log: liveLogsProvider.logs[index],
length: liveLogsProvider.logs.length,
index: index,
onLogTap: (log) {
if (!twoColumns) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => LogDetailsScreen(
log: log,
dialog: false,
twoColumns: twoColumns,
)
)
)
);
}
onLogSelected(log);
},
isLogSelected: selectedLog == liveLogsProvider.logs[index],
twoColumns: twoColumns
);
}
onLogSelected(log);
},
isLogSelected: selectedLog == liveLogsProvider.logs[index],
twoColumns: twoColumns
),
),
)
],

View file

@ -96,6 +96,8 @@ class LogTile extends StatelessWidget {
}
void blockUnblock({required String domain, required String newStatus}) async {
if (!context.mounted) return;
final ProcessModal processModal = ProcessModal();
processModal.open(AppLocalizations.of(context)!.savingUserFilters);
@ -108,14 +110,14 @@ class LogTile extends StatelessWidget {
if (!context.mounted) return;
if (rules == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.userFilteringRulesUpdated,
color: Colors.green
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.userFilteringRulesNotUpdated,
color: Colors.red
@ -124,6 +126,8 @@ class LogTile extends StatelessWidget {
}
void confirmAddClient(Client client) async {
if (!context.mounted) return;
ProcessModal processModal = ProcessModal();
processModal.open(AppLocalizations.of(context)!.addingClient);
@ -134,14 +138,14 @@ class LogTile extends StatelessWidget {
if (!context.mounted) return;
if (result == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.clientAddedSuccessfully,
color: Colors.green
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.clientNotAdded,
color: Colors.red
@ -150,6 +154,8 @@ class LogTile extends StatelessWidget {
}
void blockUnblockRuleClient() async {
if (!context.mounted) return;
ProcessModal processModal = ProcessModal();
processModal.open(AppLocalizations.of(context)!.addingRule);
@ -163,7 +169,7 @@ class LogTile extends StatelessWidget {
if (!context.mounted) return;
if (result == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: isDomainBlocked(log.reason) == true
? AppLocalizations.of(context)!.domainUnblockedThisClient(log.question.name!)
@ -172,7 +178,7 @@ class LogTile extends StatelessWidget {
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.ruleNotAdded,
color: Colors.red
@ -199,21 +205,21 @@ class LogTile extends StatelessWidget {
if (!context.mounted) return;
if (result.successful == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.clientAddedSuccessfully,
color: Colors.green
);
}
else if (result.successful == false && result.content == 'client_another_list') {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.clientAnotherList,
color: Colors.red
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.changesNotSaved,
color: Colors.red

View file

@ -42,7 +42,7 @@ class _LogsListWidgetState extends State<LogsListWidget> {
final result = await statusProvider.getFilteringRules();
if (mounted && result == false) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.couldntGetFilteringStatus,
color: Colors.red
@ -56,7 +56,7 @@ class _LogsListWidgetState extends State<LogsListWidget> {
final result = await clientsProvider.fetchClients();
if (mounted && result == false) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.couldntGetFilteringStatus,
color: Colors.red

View file

@ -8,8 +8,8 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/logs/live/live_logs_screen.dart';
import 'package:adguard_home_manager/screens/logs/filters/logs_filters_modal.dart';
import 'package:adguard_home_manager/widgets/floating_search_bar.dart';
import 'package:adguard_home_manager/config/globals.dart';
import 'package:adguard_home_manager/constants/enums.dart';
import 'package:adguard_home_manager/functions/desktop_mode.dart';
import 'package:adguard_home_manager/providers/live_logs_provider.dart';
@ -61,9 +61,10 @@ class LogsListAppBar extends StatelessWidget {
void showSearchDialog() {
showDialog(
context: context,
builder: (context) => _Search(
builder: (context) => FloatingSearchBar(
existingSearchValue: logsProvider.appliedFilters.searchText ?? "",
searchButtonRenderBox: _searchButtonKey.currentContext?.findRenderObject() as RenderBox?,
onSearch: (v) {
onSearchCompleted: (v) {
logsProvider.setAppliedFilters(
AppliedFiters(
selectedResultStatus: logsProvider.appliedFilters.selectedResultStatus,
@ -73,11 +74,22 @@ class LogsListAppBar extends StatelessWidget {
);
logsProvider.filterLogs();
},
onSearchFieldUpdated: (v) {
if (v == "") {
logsProvider.setSearchText(null);
return;
}
logsProvider.setSearchText(v);
},
onSearchFieldCleared: () {
logsProvider.setSearchText(null);
},
),
);
}
void openLiveLogsScreen() {
if (!context.mounted) return;
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => MultiProvider(
@ -279,105 +291,3 @@ class LogsListAppBar extends StatelessWidget {
}
}
class _Search extends StatefulWidget {
final void Function(String) onSearch;
final RenderBox? searchButtonRenderBox;
const _Search({
required this.onSearch,
required this.searchButtonRenderBox,
});
@override
State<_Search> createState() => _SearchState();
}
class _SearchState extends State<_Search> {
final _searchController = TextEditingController();
@override
void initState() {
final logsProvider = Provider.of<LogsProvider>(context, listen: false);
_searchController.text = logsProvider.appliedFilters.searchText ?? "";
super.initState();
}
@override
Widget build(BuildContext context) {
final logsProvider = Provider.of<LogsProvider>(context);
final position = widget.searchButtonRenderBox?.localToGlobal(Offset.zero);
final topPadding = MediaQuery.of(globalNavigatorKey.currentContext!).viewPadding.top;
return GestureDetector(
onTap: () => Navigator.pop(context),
child: Material(
color: Colors.transparent,
child: LayoutBuilder(
builder: (context, constraints) {
final double width = constraints.maxWidth - 32 > 500 ? 500 : constraints.maxWidth - 32;
return Stack(
alignment: Alignment.topCenter,
children: [
Positioned(
top: position != null ? position.dy - topPadding : topPadding,
child: SizedBox(
width: width,
child: GestureDetector(
onTap: () => {},
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(16)
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: TextFormField(
controller: _searchController,
onChanged: (v) {
if (v == "") {
logsProvider.setSearchText(null);
return;
}
logsProvider.setSearchText(v);
},
onFieldSubmitted: (v) {
widget.onSearch(v);
Navigator.pop(context);
},
autofocus: true,
decoration: InputDecoration(
hintText: AppLocalizations.of(context)!.search,
prefixIcon: const Icon(Icons.search_rounded),
border: InputBorder.none,
filled: true,
fillColor: Colors.grey.withOpacity(0.2),
suffixIcon: _searchController.text != ""
? IconButton(
onPressed: () {
_searchController.text = "";
logsProvider.setSearchText(null);
},
icon: const Icon(
Icons.close_rounded,
size: 20,
),
tooltip: AppLocalizations.of(context)!.clearSearch,
)
: null
),
),
),
),
),
),
)
],
);
}
),
),
);
}
}

View file

@ -71,7 +71,7 @@ class _ClientsListState extends State<ClientsList> {
Future refetchClients() async {
final result = await clientsProvider.fetchClients();
if (result == false && mounted) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.clientsNotLoaded,
color: Colors.red
@ -104,21 +104,21 @@ class _ClientsListState extends State<ClientsList> {
processModal.close();
if (result.successful == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.clientRemovedSuccessfully,
color: Colors.green
);
}
else if (result.successful == false && result.content == 'client_another_list') {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.clientAnotherList,
color: Colors.red
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: type == AccessSettingsList.allowed || type == AccessSettingsList.disallowed
? AppLocalizations.of(context)!.clientNotRemoved
@ -137,21 +137,21 @@ class _ClientsListState extends State<ClientsList> {
processModal.close();
if (result.successful == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.clientAddedSuccessfully,
color: Colors.green
);
}
else if (result.successful == false && result.content == 'client_another_list') {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.clientAnotherList,
color: Colors.red
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: type == AccessSettingsList.allowed || type == AccessSettingsList.disallowed
? AppLocalizations.of(context)!.clientNotRemoved

View file

@ -26,14 +26,14 @@ class AdvancedSettings extends StatelessWidget {
final result = await function(newStatus);
if (!context.mounted) return;
if (result == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.settingsUpdatedSuccessfully,
color: Colors.green
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.cannotUpdateSettings,
color: Colors.red

View file

@ -31,7 +31,7 @@ class ColorItem extends StatelessWidget {
child: InkWell(
onTap: () => onChanged(numericValue),
borderRadius: BorderRadius.circular(50),
overlayColor: const MaterialStatePropertyAll(Colors.grey),
overlayColor: const WidgetStatePropertyAll(Colors.grey),
child: Container(
width: 50,
height: 50,

View file

@ -9,14 +9,14 @@ class ThemeModeButton extends StatelessWidget {
final bool? disabled;
const ThemeModeButton({
Key? key,
super.key,
required this.icon,
required this.value,
required this.selected,
required this.label,
required this.onChanged,
this.disabled
}) : super(key: key);
});
@override
Widget build(BuildContext context) {
@ -32,19 +32,19 @@ class ThemeModeButton extends StatelessWidget {
? () => onChanged(value)
: null,
style: ButtonStyle(
elevation: MaterialStateProperty.all(0),
shape: MaterialStateProperty.all(
elevation: WidgetStateProperty.all(0),
shape: WidgetStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
)
),
backgroundColor: MaterialStateProperty.all(
backgroundColor: WidgetStateProperty.all(
value == selected
? disabled == null || disabled == false
? Theme.of(context).colorScheme.primary
: greyBackgroundColor
: disabled == null || disabled == false
? Theme.of(context).colorScheme.surfaceVariant
? Theme.of(context).colorScheme.surfaceTint.withOpacity(0.1)
: greyBackgroundColor,
)
),

View file

@ -1,3 +1,4 @@
import 'package:adguard_home_manager/constants/regexps.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -67,8 +68,7 @@ class __ContentState extends State<_Content> {
bool validData = false;
void validateMac(String value) {
final RegExp macRegex = RegExp(r'^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$');
if (macRegex.hasMatch(value)) {
if (Regexps.macAddress.hasMatch(value)) {
setState(() => macError = null);
}
else {
@ -78,8 +78,7 @@ class __ContentState extends State<_Content> {
}
void validateIp(String value) {
RegExp ipAddress = RegExp(r'^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$');
if (ipAddress.hasMatch(value) == true) {
if (Regexps.ipv4Address.hasMatch(value) == true) {
setState(() => ipError = null);
}
else {

View file

@ -5,9 +5,9 @@ class DeleteStaticLeaseModal extends StatelessWidget {
final void Function() onConfirm;
const DeleteStaticLeaseModal({
Key? key,
super.key,
required this.onConfirm
}) : super(key: key);
});
@override
Widget build(BuildContext context) {

View file

@ -2,6 +2,7 @@
import 'dart:io';
import 'package:adguard_home_manager/constants/regexps.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -100,8 +101,8 @@ class _DhcpScreenState extends State<DhcpScreen> {
break;
}
}
final regex = RegExp(r'^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$');
if (regex.hasMatch(value)) {
if (Regexps.ipv4Address.hasMatch(value)) {
setValue(null);
}
else {
@ -126,8 +127,8 @@ class _DhcpScreenState extends State<DhcpScreen> {
break;
}
}
final regex = RegExp(r'^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$');
if (regex.hasMatch(value)) {
if (Regexps.ipv6Address.hasMatch(value)) {
setValue(null);
}
else {
@ -221,14 +222,14 @@ class _DhcpScreenState extends State<DhcpScreen> {
if (!mounted) return;
processModal.close();
if (result.successful == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.settingsSaved,
color: Colors.green
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.settingsNotSaved,
color: Colors.red
@ -245,14 +246,14 @@ class _DhcpScreenState extends State<DhcpScreen> {
processModal.close();
if (result.successful == true) {
clearAll();
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.configRestored,
color: Colors.green
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.configNotRestored,
color: Colors.red
@ -276,14 +277,14 @@ class _DhcpScreenState extends State<DhcpScreen> {
data.dhcpStatus!.leases = [];
dhcpProvider.setDhcpData(data);
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.leasesRestored,
color: Colors.green
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.leasesNotRestored,
color: Colors.red

View file

@ -43,14 +43,14 @@ class DhcpLeases extends StatelessWidget {
processModal.close();
if (result == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.staticLeaseDeleted,
color: Colors.green
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.staticLeaseNotDeleted,
color: Colors.red
@ -67,28 +67,28 @@ class DhcpLeases extends StatelessWidget {
processModal.close();
if (result.successful == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.staticLeaseCreated,
color: Colors.green
);
}
else if (result.successful == false && result.content == "already_exists") {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.staticLeaseExists,
color: Colors.red
);
}
else if (result.successful == false && result.content == "server_not_configured") {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.serverNotConfigured,
color: Colors.red
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.staticLeaseNotCreated,
color: Colors.red

View file

@ -1,5 +1,6 @@
// ignore_for_file: use_build_context_synchronously
import 'package:adguard_home_manager/constants/regexps.dart';
import 'package:adguard_home_manager/functions/desktop_mode.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@ -23,8 +24,7 @@ class _BootstrapDnsScreenState extends State<BootstrapDnsScreen> {
bool validValues = false;
void validateIp(Map<String, dynamic> field, String value) {
RegExp ipAddress = RegExp(r'(?:^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}$)|(?:^(?:(?:[a-fA-F\d]{1,4}:){7}(?:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){5}(?::(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,2}|:)|(?:[a-fA-F\d]{1,4}:){4}(?:(?::[a-fA-F\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,3}|:)|(?:[a-fA-F\d]{1,4}:){3}(?:(?::[a-fA-F\d]{1,4}){0,2}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,4}|:)|(?:[a-fA-F\d]{1,4}:){2}(?:(?::[a-fA-F\d]{1,4}){0,3}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,5}|:)|(?:[a-fA-F\d]{1,4}:){1}(?:(?::[a-fA-F\d]{1,4}){0,4}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,6}|:)|(?::(?:(?::[a-fA-F\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,7}|:)))(?:%[0-9a-zA-Z]{1,})?$)');
if (ipAddress.hasMatch(value) == true) {
if (Regexps.ipv4Address.hasMatch(value) == true || Regexps.ipv6Address.hasMatch(value) == true) {
setState(() => field['error'] = null);
}
else {
@ -80,21 +80,21 @@ class _BootstrapDnsScreenState extends State<BootstrapDnsScreen> {
processModal.close();
if (result.successful == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.dnsConfigSaved,
color: Colors.green
);
}
else if (result.successful == false && result.statusCode == 400) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.someValueNotValid,
color: Colors.red
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.dnsConfigNotSaved,
color: Colors.red
@ -197,7 +197,7 @@ class _BootstrapDnsScreenState extends State<BootstrapDnsScreen> {
)
],
),
)).toList(),
)),
Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,

View file

@ -86,21 +86,21 @@ class _CacheConfigDnsScreenState extends State<CacheConfigDnsScreen> {
processModal.close();
if (result.successful == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.dnsConfigSaved,
color: Colors.green
);
}
else if (result.successful== false && result.statusCode == 400) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.someValueNotValid,
color: Colors.red
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.dnsConfigNotSaved,
color: Colors.red
@ -139,14 +139,14 @@ class _CacheConfigDnsScreenState extends State<CacheConfigDnsScreen> {
void clearCache() async {
final result = await clearDnsCache(context, serversProvider.selectedServer!);
if (result.successful == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.dnsCacheCleared,
color: Colors.green
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.dnsCacheNotCleared,
color: Colors.red

View file

@ -66,14 +66,14 @@ class _DnsSettingsState extends State<DnsSettings> {
void clearCache() async {
final result = await clearDnsCache(context, serversProvider.selectedServer!);
if (result.successful == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.dnsCacheCleared,
color: Colors.green
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.dnsCacheNotCleared,
color: Colors.red

View file

@ -183,21 +183,21 @@ class _DnsServerSettingsScreenState extends State<DnsServerSettingsScreen> {
processModal.close();
if (result.successful == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.dnsConfigSaved,
color: Colors.green
);
}
else if (result.successful == false && result.statusCode == 400) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.someValueNotValid,
color: Colors.red
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.dnsConfigNotSaved,
color: Colors.red

View file

@ -1,5 +1,6 @@
import 'dart:io';
import 'package:adguard_home_manager/constants/regexps.dart';
import 'package:adguard_home_manager/screens/settings/dns/comment_modal.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@ -24,9 +25,7 @@ class _FallbackDnsScreenState extends State<FallbackDnsScreen> {
bool validValues = false;
void validateIp(Map<String, dynamic> 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,})?$)');
RegExp url = RegExp(r'(https?|tls):\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)');
if (ipAddress.hasMatch(value) == true || url.hasMatch(value) == true) {
if (Regexps.ipv4Address.hasMatch(value) == true || Regexps.ipv6Address.hasMatch(value) || Regexps.url.hasMatch(value) == true) {
setState(() => field['error'] = null);
}
else {
@ -91,21 +90,21 @@ class _FallbackDnsScreenState extends State<FallbackDnsScreen> {
if (!context.mounted) return;
if (result.successful == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.dnsConfigSaved,
color: Colors.green
);
}
else if (result.successful == false && result.statusCode == 400) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.someValueNotValid,
color: Colors.red
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.dnsConfigNotSaved,
color: Colors.red

View file

@ -112,21 +112,21 @@ class _PrivateReverseDnsServersScreenState extends State<PrivateReverseDnsServer
processModal.close();
if (result.successful == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.dnsConfigSaved,
color: Colors.green
);
}
else if (result.successful == false && result.statusCode == 400) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.someValueNotValid,
color: Colors.red
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.dnsConfigNotSaved,
color: Colors.red

View file

@ -1,3 +1,4 @@
import 'package:adguard_home_manager/constants/regexps.dart';
import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -33,8 +34,7 @@ class _RateLimitAllowlistModalState extends State<RateLimitAllowlistModal> {
List<_IpListItemController> _controllersList = [];
void _validateIp(String value, _IpListItemController item) {
final regexp = RegExp(r'^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$');
if (regexp.hasMatch(value)) {
if (Regexps.ipv4Address.hasMatch(value)) {
setState(() => _controllersList = _controllersList.map((e) {
if (e.id == item.id) {
return _IpListItemController(

View file

@ -1,5 +1,3 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'package:flutter/material.dart';
@ -30,6 +28,9 @@ class _UpstreamDnsScreenState extends State<UpstreamDnsScreen> {
bool validValues = false;
final upstreamTimeoutController = TextEditingController();
String? upstreamTimeoutError = null;
checkValidValues() {
if (
dnsServers.isNotEmpty &&
@ -61,6 +62,7 @@ class _UpstreamDnsScreenState extends State<UpstreamDnsScreen> {
}
}
upstreamMode = dnsProvider.dnsInfo!.upstreamMode ?? "";
upstreamTimeoutController.text = dnsProvider.dnsInfo!.upstreamTimeout != null ? dnsProvider.dnsInfo!.upstreamTimeout.toString() : "";
validValues = true;
super.initState();
}
@ -72,6 +74,23 @@ class _UpstreamDnsScreenState extends State<UpstreamDnsScreen> {
final width = MediaQuery.of(context).size.width;
void validateTimeout(String value) {
if (value != '' && int.tryParse(value) != null && int.parse(value) > 0) {
setState(() {
upstreamTimeoutError = null;
validValues = true;
});
}
else {
setState(() {
upstreamTimeoutError = value == ''
? AppLocalizations.of(context)!.fieldCannotBeEmpty
: AppLocalizations.of(context)!.invalidValue;
validValues = false;
});
}
}
void openAddCommentModal() {
if (width > 900 || !(Platform.isAndroid || Platform.isIOS)) {
showDialog(
@ -146,27 +165,29 @@ class _UpstreamDnsScreenState extends State<UpstreamDnsScreen> {
final result = await dnsProvider.saveUpstreamDnsConfig({
"upstream_dns": dnsServers.map((e) => e['controller'] != null ? e['controller'].text : e['comment']).toList(),
"upstream_mode": upstreamMode
"upstream_mode": upstreamMode,
"upstream_timeout": int.tryParse(upstreamTimeoutController.text)
});
processModal.close();
if (!context.mounted) return;
if (result.successful == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.dnsConfigSaved,
color: Colors.green
);
}
else if (result.successful == false && result.statusCode == 400) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.someValueNotValid,
color: Colors.red
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.dnsConfigNotSaved,
color: Colors.red
@ -312,6 +333,27 @@ class _UpstreamDnsScreenState extends State<UpstreamDnsScreen> {
subtitle: AppLocalizations.of(context)!.fastestIpAddressDescription,
onChanged: (value) => setState(() => upstreamMode = value),
),
const SizedBox(height: 16),
SectionLabel(label: AppLocalizations.of(context)!.others),
Padding(
padding: const EdgeInsets.all(16),
child: TextFormField(
controller: upstreamTimeoutController,
onChanged: validateTimeout,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.timer_rounded),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10)
)
),
labelText: AppLocalizations.of(context)!.upstreamTimeout,
helperText: AppLocalizations.of(context)!.upstreamTimeoutHelper,
helperMaxLines: 2,
errorText: upstreamTimeoutError
)
),
),
],
),
),

View file

@ -5,9 +5,9 @@ class DeleteDnsRewrite extends StatelessWidget {
final void Function() onConfirm;
const DeleteDnsRewrite({
Key? key,
super.key,
required this.onConfirm
}) : super(key: key);
});
@override
Widget build(BuildContext context) {

View file

@ -1,5 +1,6 @@
import 'dart:io';
import 'package:adguard_home_manager/constants/regexps.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -82,8 +83,7 @@ class _ContentState extends State<_Content> {
bool validData = false;
void validateDomain(String value) {
final domainRegex = RegExp(r'^([a-z0-9|-]+\.)*[a-z0-9|-]+\.[a-z]+$');
if (domainRegex.hasMatch(value)) {
if (Regexps.wildcardDomain.hasMatch(value)) {
setState(() => domainError = null);
}
else {

View file

@ -67,14 +67,14 @@ class _DnsRewritesScreenState extends State<DnsRewritesScreen> {
processModal.close();
if (result == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.dnsRewriteRuleDeleted,
color: Colors.green
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.dnsRewriteRuleNotDeleted,
color: Colors.red
@ -91,14 +91,14 @@ class _DnsRewritesScreenState extends State<DnsRewritesScreen> {
processModal.close();
if (result == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.dnsRewriteRuleAdded,
color: Colors.green
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.dnsRewriteRuleNotAdded,
color: Colors.red
@ -115,14 +115,14 @@ class _DnsRewritesScreenState extends State<DnsRewritesScreen> {
processModal.close();
if (result == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.dnsRewriteRuleUpdated,
color: Colors.green
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.dnsRewriteRuleNotUpdated,
color: Colors.red
@ -169,7 +169,7 @@ class _DnsRewritesScreenState extends State<DnsRewritesScreen> {
onRefresh: () async {
final result = await rewriteRulesProvider.fetchRules();
if (result == false) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.rewriteRulesNotLoaded,
color: Colors.red

View file

@ -5,9 +5,9 @@ class EncryptionErrorModal extends StatelessWidget {
final String error;
const EncryptionErrorModal({
Key? key,
super.key,
required this.error,
}) : super(key: key);
});
@override
Widget build(BuildContext context) {

View file

@ -12,7 +12,7 @@ class EncryptionTextField extends StatelessWidget {
final String? helperText;
const EncryptionTextField({
Key? key,
super.key,
required this.enabled,
required this.controller,
required this.icon,
@ -22,7 +22,7 @@ class EncryptionTextField extends StatelessWidget {
this.keyboardType,
this.multiline,
this.helperText,
}) : super(key: key);
});
@override
Widget build(BuildContext context) {

View file

@ -233,14 +233,14 @@ class _EncryptionSettingsState extends State<EncryptionSettings> {
processModal.close();
if (result.successful == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.encryptionConfigSaved,
color: Colors.green
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.encryptionConfigNotSaved,
color: Colors.red
@ -282,14 +282,14 @@ class _EncryptionSettingsState extends State<EncryptionSettings> {
processModal.close();
if (result.successful == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.configurationResetSuccessfully,
color: Colors.green
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.configurationResetError,
color: Colors.red

View file

@ -1,11 +1,11 @@
import 'package:adguard_home_manager/constants/regexps.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/providers/app_config_provider.dart';
String? validateDomain(BuildContext context, String domain) {
RegExp regExp = RegExp(r'^([a-z0-9|-]+\.)*[a-z0-9|-]+\.[a-z]+$');
if (regExp.hasMatch(domain)) {
if (Regexps.domain.hasMatch(domain)) {
return null;
}
else {
@ -23,8 +23,7 @@ String? validatePort(BuildContext context, String value) {
}
String? validateCertificate(BuildContext context, String cert) {
final regExp = RegExp(r'(-{3,}(\bBEGIN CERTIFICATE\b))|(-{3,}-{3,}(\END CERTIFICATE\b)-{3,})', multiLine: true);
if (regExp.hasMatch(cert.replaceAll('\n', ''))) {
if (Regexps.certificate.hasMatch(cert.replaceAll('\n', ''))) {
return null;
}
else {
@ -33,8 +32,7 @@ String? validateCertificate(BuildContext context, String cert) {
}
String? validatePrivateKey(BuildContext context, String cert) {
final regExp = RegExp(r'(-{3,}(\bBEGIN\b).*(PRIVATE KEY\b))|(-{3,}-{3,}(\bEND\b).*(PRIVATE KEY\b)-{3,})', multiLine: true);
if (regExp.hasMatch(cert.replaceAll('\n', ''))) {
if (Regexps.privateKey.hasMatch(cert.replaceAll('\n', ''))) {
return null;
}
else {
@ -43,8 +41,7 @@ String? validatePrivateKey(BuildContext context, String cert) {
}
String? validatePath(BuildContext context, String cert) {
final regExp = RegExp(r'^(\/{0,1}(?!\/))[A-Za-z0-9\/\-_]+(\.([a-zA-Z]+))?$');
if (regExp.hasMatch(cert)) {
if (Regexps.path.hasMatch(cert)) {
return null;
}
else {

View file

@ -5,9 +5,9 @@ class ErrorMessageEncryption extends StatelessWidget {
final String errorMessage;
const ErrorMessageEncryption({
Key? key,
super.key,
required this.errorMessage,
}) : super(key: key);
});
@override
Widget build(BuildContext context) {

View file

@ -6,10 +6,10 @@ class EncryptionMasterSwitch extends StatelessWidget {
final void Function(bool) onChange;
const EncryptionMasterSwitch({
Key? key,
super.key,
required this.value,
required this.onChange
}) : super(key: key);
});
@override
Widget build(BuildContext context) {

View file

@ -8,10 +8,10 @@ class Status extends StatelessWidget {
final String label;
const Status({
Key? key,
super.key,
required this.valid,
required this.label
}) : super(key: key);
});
@override
Widget build(BuildContext context) {

View file

@ -1,9 +1,4 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:install_referrer/install_referrer.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -13,11 +8,8 @@ import 'package:adguard_home_manager/screens/settings/general_settings/top_items
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 {
@ -48,15 +40,16 @@ class _GeneralSettingsState extends State<GeneralSettings> {
required Future Function(bool) function
}) async {
final result = await function(newStatus);
if (!context.mounted) return;
if (result == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.settingsUpdatedSuccessfully,
color: Colors.green
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.cannotUpdateSettings,
color: Colors.red
@ -64,60 +57,60 @@ class _GeneralSettingsState extends State<GeneralSettings> {
}
}
Future checkUpdatesAvailable() async {
setState(() => appUpdatesStatus = AppUpdatesStatus.checking);
// Future checkUpdatesAvailable() async {
// setState(() => appUpdatesStatus = AppUpdatesStatus.checking);
final res = await checkAppUpdates(
currentBuildNumber: appConfigProvider.getAppInfo!.buildNumber,
setUpdateAvailable: appConfigProvider.setAppUpdatesAvailable,
installationSource: appConfigProvider.installationSource,
isBeta: appConfigProvider.getAppInfo!.version.contains('beta'),
);
// final res = await checkAppUpdates(
// currentBuildNumber: appConfigProvider.getAppInfo!.buildNumber,
// setUpdateAvailable: appConfigProvider.setAppUpdatesAvailable,
// installationSource: appConfigProvider.installationSource,
// isBeta: appConfigProvider.getAppInfo!.version.contains('beta'),
// );
if (!mounted) return;
if (res != null) {
setState(() => appUpdatesStatus = AppUpdatesStatus.available);
}
else {
setState(() => appUpdatesStatus = AppUpdatesStatus.recheck);
}
}
// if (!mounted) return;
// if (res != null) {
// setState(() => appUpdatesStatus = AppUpdatesStatus.available);
// }
// else {
// setState(() => appUpdatesStatus = AppUpdatesStatus.recheck);
// }
// }
Widget generateAppUpdateStatus() {
if (appUpdatesStatus == AppUpdatesStatus.available) {
return IconButton(
onPressed: appConfigProvider.appUpdatesAvailable != null
? () async {
final link = getAppUpdateDownloadLink(appConfigProvider.appUpdatesAvailable!);
if (link != null) {
openUrl(link);
}
}
: null,
icon: const Icon(Icons.download_rounded),
tooltip: AppLocalizations.of(context)!.downloadUpdate,
);
}
else if (appUpdatesStatus == AppUpdatesStatus.checking) {
return const Padding(
padding: EdgeInsets.only(right: 16),
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 3,
)
),
);
}
else {
return IconButton(
onPressed: checkUpdatesAvailable,
icon: const Icon(Icons.refresh_rounded),
tooltip: AppLocalizations.of(context)!.checkUpdates,
);
}
}
// Widget generateAppUpdateStatus() {
// if (appUpdatesStatus == AppUpdatesStatus.available) {
// return IconButton(
// onPressed: appConfigProvider.appUpdatesAvailable != null
// ? () async {
// final link = getAppUpdateDownloadLink(appConfigProvider.appUpdatesAvailable!);
// if (link != null) {
// openUrl(link);
// }
// }
// : null,
// icon: const Icon(Icons.download_rounded),
// tooltip: AppLocalizations.of(context)!.downloadUpdate,
// );
// }
// else if (appUpdatesStatus == AppUpdatesStatus.checking) {
// return const Padding(
// padding: EdgeInsets.only(right: 16),
// child: SizedBox(
// width: 24,
// height: 24,
// child: CircularProgressIndicator(
// strokeWidth: 3,
// )
// ),
// );
// }
// else {
// return IconButton(
// onPressed: checkUpdatesAvailable,
// icon: const Icon(Icons.refresh_rounded),
// tooltip: AppLocalizations.of(context)!.checkUpdates,
// );
// }
// }
return Scaffold(
appBar: AppBar(
@ -253,17 +246,17 @@ class _GeneralSettingsState extends State<GeneralSettings> {
right: 10
)
),
if (!(Platform.isAndroid || Platform.isIOS) || (Platform.isAndroid && (appConfigProvider.installationSource == InstallationAppReferrer.androidManually ))) ...[
SectionLabel(label: AppLocalizations.of(context)!.application),
CustomListTile(
icon: Icons.system_update_rounded,
title: AppLocalizations.of(context)!.appUpdates,
subtitle: appConfigProvider.appUpdatesAvailable != null
? AppLocalizations.of(context)!.updateAvailable
: AppLocalizations.of(context)!.usingLatestVersion,
trailing: generateAppUpdateStatus()
)
]
// if (!(Platform.isAndroid || Platform.isIOS) || (Platform.isAndroid && (appConfigProvider.installationSource == InstallationAppReferrer.androidManually ))) ...[
// SectionLabel(label: AppLocalizations.of(context)!.application),
// CustomListTile(
// icon: Icons.system_update_rounded,
// title: AppLocalizations.of(context)!.appUpdates,
// subtitle: appConfigProvider.appUpdatesAvailable != null
// ? AppLocalizations.of(context)!.updateAvailable
// : AppLocalizations.of(context)!.usingLatestVersion,
// trailing: generateAppUpdateStatus()
// )
// ]
],
),
)

View file

@ -43,14 +43,14 @@ class _TopItemsListSettingsState extends State<TopItemsListSettings> with Ticker
final result = await appConfigProvider.setHomeTopItemsOrder(persistHomeTopItemsList);
if (!context.mounted) return;
if (result == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.settingsSaved,
color: Colors.green
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.settingsNotSaved,
color: Colors.red

View file

@ -1,3 +1,4 @@
import 'package:adguard_home_manager/constants/regexps.dart';
import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -12,13 +13,16 @@ class LogsConfigOptions extends StatelessWidget {
final void Function(bool) updateGeneralSwitch;
final bool anonymizeClientIp;
final void Function(bool) updateAnonymizeClientIp;
final List<int> retentionItems;
final double? retentionTime;
final void Function(double?) updateRetentionTime;
final List<String> retentionItems;
final String? retentionTime;
final void Function(String?) updateRetentionTime;
final void Function() onClear;
final void Function() onConfirm;
final List<DomainListItemController> ignoredDomainsControllers;
final void Function(List<DomainListItemController>) updateIgnoredDomainsControllers;
final TextEditingController customTimeController;
final String? customTimeError;
final void Function(String) validateCustomTime;
const LogsConfigOptions({
super.key,
@ -32,7 +36,10 @@ class LogsConfigOptions extends StatelessWidget {
required this.onClear,
required this.onConfirm,
required this.ignoredDomainsControllers,
required this.updateIgnoredDomainsControllers
required this.updateIgnoredDomainsControllers,
required this.customTimeController,
required this.customTimeError,
required this.validateCustomTime,
});
@override
@ -40,6 +47,7 @@ class LogsConfigOptions extends StatelessWidget {
const Uuid uuid = Uuid();
final List<String> dropdownItemTranslation = [
AppLocalizations.of(context)!.custom,
AppLocalizations.of(context)!.hours6,
AppLocalizations.of(context)!.hours24,
AppLocalizations.of(context)!.days7,
@ -48,9 +56,8 @@ class LogsConfigOptions extends StatelessWidget {
];
void validateDomain(String value, String id) {
final domainRegex = RegExp(r'^([a-z0-9|-]+\.)*[a-z0-9|-]+\.[a-z]+$');
bool error = false;
if (domainRegex.hasMatch(value)) {
if (Regexps.domain.hasMatch(value)) {
error = false;
}
else {
@ -67,7 +74,7 @@ class LogsConfigOptions extends StatelessWidget {
}).toList()
);
}
print(retentionTime);
return ListView(
children: [
const SizedBox(height: 16),
@ -92,7 +99,7 @@ class LogsConfigOptions extends StatelessWidget {
child: Text(dropdownItemTranslation[item.key]),
)).toList(),
value: retentionTime,
onChanged: (value) => updateRetentionTime(double.tryParse(value.toString())),
onChanged: (value) => updateRetentionTime(value.toString()),
decoration: InputDecoration(
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
@ -104,6 +111,24 @@ class LogsConfigOptions extends StatelessWidget {
borderRadius: BorderRadius.circular(20),
),
),
if (retentionTime == "custom") Padding(
padding: const EdgeInsets.only(top: 24, left: 16, right: 16),
child: TextFormField(
controller: customTimeController,
onChanged: validateCustomTime,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.schedule_rounded),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10)
)
),
labelText: AppLocalizations.of(context)!.customTimeInHours,
errorText: customTimeError
),
keyboardType: TextInputType.number,
),
),
Padding(
padding: const EdgeInsets.only(top: 24, bottom: 8),
child: Row(

View file

@ -37,15 +37,18 @@ class _LogsSettingsState extends State<LogsSettings> {
bool generalSwitch = false;
bool anonymizeClientIp = false;
double? retentionTime;
String? retentionTime;
List<DomainListItemController> _ignoredDomainsControllers = [];
final _customTimeController = TextEditingController();
String? _customTimeError = null;
List<int> retentionItems = [
21600000,
86400000,
604800000,
2592000000,
7776000000
List<String> retentionItems = [
"custom",
"21600000",
"86400000",
"604800000",
"2592000000",
"7776000000"
];
LoadStatus loadStatus = LoadStatus.loading;
@ -61,9 +64,10 @@ class _LogsSettingsState extends State<LogsSettings> {
setState(() {
generalSwitch = data.enabled ?? false;
anonymizeClientIp = data.anonymizeClientIp ?? false;
retentionTime = data.interval != null
? double.parse(data.interval.toString())
: null;
retentionTime = retentionItems.contains(data.interval.toString()) ? data.interval.toString() : "custom";
if (data.interval != null && !retentionItems.contains(data.interval.toString())) {
_customTimeController.text = (data.interval!/3.6e+6).toInt().toString();
}
if (data.ignored != null) {
_ignoredDomainsControllers = data.ignored!.map((e) => DomainListItemController(
id: uuid.v4(),
@ -79,6 +83,34 @@ class _LogsSettingsState extends State<LogsSettings> {
}
}
void validateCustomTime(String value) {
try {
final regex = RegExp(r'^\d+$');
final parsed = int.parse(value);
if (!regex.hasMatch(value)) {
setState(() => _customTimeError = AppLocalizations.of(context)!.invalidTime);
}
else if (parsed < 1) {
setState(() => _customTimeError = AppLocalizations.of(context)!.notLess1Hour);
}
else {
setState(() => _customTimeError = null);
}
} catch (_) {
setState(() => _customTimeError = AppLocalizations.of(context)!.invalidTime);
}
}
bool checkValidValues() {
if (_ignoredDomainsControllers.where((d) => d.controller.text == "" || d.error == true).isNotEmpty) {
return false;
}
if (retentionTime == "custom" && (_customTimeError != null || _customTimeController.text == "")) {
return false;
}
return true;
}
@override
void initState() {
loadData();
@ -90,9 +122,7 @@ class _LogsSettingsState extends State<LogsSettings> {
final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final validValues = _ignoredDomainsControllers.where(
(d) => d.controller.text == "" || d.error == true
).isEmpty;
final validValues = checkValidValues();
void clearQueries() async {
ProcessModal processModal = ProcessModal();
@ -102,17 +132,17 @@ class _LogsSettingsState extends State<LogsSettings> {
processModal.close();
if (!mounted) return;
if (!context.mounted) return;
if (result.successful == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.logsCleared,
color: Colors.green
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.logsNotCleared,
color: Colors.red
@ -127,7 +157,7 @@ class _LogsSettingsState extends State<LogsSettings> {
final result = await serversProvider.apiClient2!.updateQueryLogParameters(
data: {
"enabled": generalSwitch,
"interval": retentionTime,
"interval": retentionTime == "custom" ? int.parse(_customTimeController.text)*3.6e+6 : int.parse(retentionTime!) ,
"anonymize_client_ip": anonymizeClientIp,
"ignored": _ignoredDomainsControllers.map((e) => e.controller.text).toList()
}
@ -135,17 +165,17 @@ class _LogsSettingsState extends State<LogsSettings> {
processModal.close();
if (!mounted) return;
if (!context.mounted) return;
if (result.successful == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.logsConfigUpdated,
color: Colors.green
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.logsConfigNotUpdated,
color: Colors.red
@ -198,6 +228,9 @@ class _LogsSettingsState extends State<LogsSettings> {
onConfirm: updateConfig,
ignoredDomainsControllers: _ignoredDomainsControllers,
updateIgnoredDomainsControllers: (v) => setState(() => _ignoredDomainsControllers = v),
customTimeController: _customTimeController,
customTimeError: _customTimeError,
validateCustomTime: validateCustomTime,
);
case LoadStatus.error:

View file

@ -90,7 +90,7 @@ class _SafeSearchSettingsScreenState extends State<SafeSearchSettingsScreen> {
processModal.close();
if (result == true) {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.settingsUpdatedSuccessfully,
color: Colors.green,
@ -98,7 +98,7 @@ class _SafeSearchSettingsScreenState extends State<SafeSearchSettingsScreen> {
);
}
else {
showSnacbkar(
showSnackbar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.settingsNotSaved,
color: Colors.red,

View file

@ -1,8 +1,6 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/settings/server_info/server_info.dart';
@ -25,7 +23,6 @@ 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/constants/urls.dart';
import 'package:adguard_home_manager/providers/status_provider.dart';
@ -273,37 +270,18 @@ class _SettingsWidgetState extends State<_SettingsWidget> {
subtitle: appConfigProvider.getAppInfo!.version,
),
CustomListTile(
title: AppLocalizations.of(context)!.createdBy,
subtitle: Strings.createdBy,
title: AppLocalizations.of(context)!.applicationDetails,
subtitle: AppLocalizations.of(context)!.applicationDetailsDescription,
trailing: Icon(Icons.open_in_new_rounded),
onTap: () => openUrl(Urls.appDetailsWebpage),
),
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,
),
],
),
)
CustomListTile(
title: AppLocalizations.of(context)!.myOtherApps,
subtitle: AppLocalizations.of(context)!.myOtherAppsDescription,
trailing: Icon(Icons.open_in_new_rounded),
onTap: () => openUrl(Urls.jgeek00AppsWebpage),
),
SizedBox(height: 16)
],
)
],

Some files were not shown because too many files have changed in this diff Show more