backup: before proxies page modernization
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -56,5 +56,7 @@ app.*.map.json
|
|||||||
/android/app/profile
|
/android/app/profile
|
||||||
/android/app/release
|
/android/app/release
|
||||||
|
|
||||||
|
|
||||||
/data
|
/data
|
||||||
|
|
||||||
|
# FVM Version Cache
|
||||||
|
.fvm/
|
||||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -15,7 +15,6 @@
|
|||||||
"editor.tabCompletion": "onlySnippets",
|
"editor.tabCompletion": "onlySnippets",
|
||||||
"editor.wordBasedSuggestions": "off"
|
"editor.wordBasedSuggestions": "off"
|
||||||
},
|
},
|
||||||
|
|
||||||
"html.format.wrapLineLength": 250,
|
"html.format.wrapLineLength": 250,
|
||||||
|
"dart.flutterSdkPath": ".fvm/versions/3.24.0"
|
||||||
}
|
}
|
||||||
328
BUILD_INSTRUCTIONS.md
Normal file
328
BUILD_INSTRUCTIONS.md
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
# Инструкция по сборке Hiddify v2.5.7
|
||||||
|
|
||||||
|
## Успешная сборка выполнена 25 декабря 2025 г.
|
||||||
|
|
||||||
|
### Системные требования
|
||||||
|
|
||||||
|
- **ОС**: Linux (Ubuntu 24.04)
|
||||||
|
- **Java**: OpenJDK 17
|
||||||
|
- **Go**: 1.25.5 (для libcore, если собирать самостоятельно)
|
||||||
|
- **Android SDK**: Platform 34, Build Tools, NDK 26.1.10909125
|
||||||
|
- **Disk Space**: ~15GB свободного места
|
||||||
|
|
||||||
|
### Версии компонентов
|
||||||
|
|
||||||
|
#### Flutter
|
||||||
|
```bash
|
||||||
|
# Установка FVM (Flutter Version Manager)
|
||||||
|
flutter pub global activate fvm
|
||||||
|
|
||||||
|
# Установка Flutter 3.24.0 (НЕ 3.24.3!)
|
||||||
|
fvm install 3.24.0
|
||||||
|
fvm use 3.24.0 --force
|
||||||
|
```
|
||||||
|
|
||||||
|
**ВАЖНО**: В pubspec.yaml указано `flutter: ">=3.24.0 <=3.24.3"`, но в официальной сборке используется **3.24.0** (не 3.24.3). При использовании 3.24.3 возникает конфликт зависимостей intl.
|
||||||
|
|
||||||
|
#### Gradle & Android
|
||||||
|
- **Gradle**: 8.7 (обновлен с 7.6.1)
|
||||||
|
- **AGP** (Android Gradle Plugin): 8.2.0 (обновлен с 7.4.2)
|
||||||
|
- **Kotlin**: 1.9.22 (обновлен с 1.8.21)
|
||||||
|
- **compileSdk**: 34 (НЕ 35 - несовместим с libcore)
|
||||||
|
- **targetSdk**: 34
|
||||||
|
- **minSdk**: 21
|
||||||
|
|
||||||
|
#### Java
|
||||||
|
```bash
|
||||||
|
# Проверка версии
|
||||||
|
java -version
|
||||||
|
# Должно быть: openjdk 17.x.x
|
||||||
|
|
||||||
|
# Настройка Flutter для использования Java 17
|
||||||
|
flutter config --jdk-dir="/usr/lib/jvm/java-1.17.0-openjdk-amd64"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Пошаговая инструкция сборки
|
||||||
|
|
||||||
|
#### 1. Клонирование репозитория
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/vodorod/dorod
|
||||||
|
git clone --depth 1 --branch v2.5.7 --recurse-submodules https://github.com/hiddify/hiddify-app.git Umbrix-hid
|
||||||
|
cd Umbrix-hid
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Исправление зависимостей
|
||||||
|
|
||||||
|
**Проблема**: `flutter_easy_permission` устарел и не собирается с современными версиями Android.
|
||||||
|
|
||||||
|
**Решение**: Закомментировать в `pubspec.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Строки 90-92, было:
|
||||||
|
#flutter_easy_permission: ^1.1.2
|
||||||
|
flutter_easy_permission:
|
||||||
|
git: https://github.com/unger1984/flutter_easy_permission.git
|
||||||
|
|
||||||
|
# Изменить на:
|
||||||
|
#flutter_easy_permission: ^1.1.2
|
||||||
|
#flutter_easy_permission:
|
||||||
|
# git: https://github.com/unger1984/flutter_easy_permission.git
|
||||||
|
```
|
||||||
|
|
||||||
|
**Изменения в коде** (`lib/features/common/qr_code_scanner_screen.dart`):
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// Закомментировать импорт:
|
||||||
|
// import 'package:flutter_easy_permission/easy_permissions.dart';
|
||||||
|
|
||||||
|
// Закомментировать константы:
|
||||||
|
// const permissions = [Permissions.CAMERA];
|
||||||
|
// const permissionGroup = [PermissionGroup.Camera];
|
||||||
|
|
||||||
|
// В методе _requestCameraPermission() упростить:
|
||||||
|
Future<bool> _requestCameraPermission() async {
|
||||||
|
// Simplified: assuming permission is granted
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// В dispose() закомментировать:
|
||||||
|
// FlutterEasyPermission().dispose();
|
||||||
|
|
||||||
|
// Во всех методах, где проверяется hasPermission, заменить на:
|
||||||
|
final hasPermission = true;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Обновление Gradle и AGP
|
||||||
|
|
||||||
|
**android/settings.gradle**:
|
||||||
|
```gradle
|
||||||
|
plugins {
|
||||||
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
|
id "com.android.application" version "8.2.0" apply false // было 7.4.2
|
||||||
|
id "org.jetbrains.kotlin.android" version "1.9.22" apply false // было 1.8.21
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**android/gradle/wrapper/gradle-wrapper.properties**:
|
||||||
|
```properties
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||||
|
# Было: gradle-7.6.1-bin.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Скачивание libcore
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p android/app/libs
|
||||||
|
curl -L https://github.com/hiddify/hiddify-next-core/releases/download/v3.1.8/hiddify-core-android.tar.gz | tar xz -C android/app/libs/
|
||||||
|
```
|
||||||
|
|
||||||
|
Должен появиться файл `android/app/libs/libcore.aar` (~119MB).
|
||||||
|
|
||||||
|
#### 5. Генерация кода
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$HOME/.pub-cache/bin/fvm flutter pub get
|
||||||
|
$HOME/.pub-cache/bin/fvm flutter pub run build_runner build --delete-conflicting-outputs
|
||||||
|
```
|
||||||
|
|
||||||
|
Эта команда генерирует:
|
||||||
|
- Riverpod providers
|
||||||
|
- Freezed модели
|
||||||
|
- Drift database код
|
||||||
|
- Localization файлы
|
||||||
|
- Asset файлы
|
||||||
|
|
||||||
|
#### 6. Сборка APK
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Debug версия (без shrink для быстрой сборки)
|
||||||
|
$HOME/.pub-cache/bin/fvm flutter build apk --debug --no-shrink
|
||||||
|
|
||||||
|
# Release версия (требует keystore)
|
||||||
|
# $HOME/.pub-cache/bin/fvm flutter build apk --release
|
||||||
|
```
|
||||||
|
|
||||||
|
**Результат**:
|
||||||
|
```
|
||||||
|
✓ Built build/app/outputs/flutter-apk/app-debug.apk (193 MB)
|
||||||
|
✓ Built build/app/outputs/flutter-apk/app-arm64-v8a-debug.apk (88 MB)
|
||||||
|
✓ Built build/app/outputs/flutter-apk/app-armeabi-v7a-debug.apk (83 MB)
|
||||||
|
✓ Built build/app/outputs/flutter-apk/app-x86_64-debug.apk (86 MB)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 7. Установка на устройство/эмулятор
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Запуск эмулятора (если есть)
|
||||||
|
$HOME/Android/Sdk/emulator/emulator -avd <ИМЯ_AVD>
|
||||||
|
|
||||||
|
# Проверка подключенных устройств
|
||||||
|
$HOME/.pub-cache/bin/fvm flutter devices
|
||||||
|
|
||||||
|
# Установка APK
|
||||||
|
$HOME/Android/Sdk/platform-tools/adb install -r build/app/outputs/flutter-apk/app-x86_64-debug.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
### Типичные ошибки и решения
|
||||||
|
|
||||||
|
#### Ошибка 1: intl version conflict
|
||||||
|
```
|
||||||
|
Because hiddify depends on flutter_localizations from sdk which depends on intl 0.20.2,
|
||||||
|
intl 0.20.2 is required.
|
||||||
|
So, because hiddify depends on intl ^0.19.0, version solving failed.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Причина**: Flutter 3.24.3 требует intl 0.20.2, но проект использует 0.19.0.
|
||||||
|
|
||||||
|
**Решение**: Использовать Flutter 3.24.0 (не 3.24.3).
|
||||||
|
|
||||||
|
#### Ошибка 2: flutter_easy_permission compilation error
|
||||||
|
```
|
||||||
|
error: package pub.devrel.easypermissions does not exist
|
||||||
|
```
|
||||||
|
|
||||||
|
**Причина**: Пакет устарел и не имеет зависимости EasyPermissions.
|
||||||
|
|
||||||
|
**Решение**: Закомментировать в pubspec.yaml и коде (см. шаг 2).
|
||||||
|
|
||||||
|
#### Ошибка 3: Unresolved reference: nekohasekai
|
||||||
|
```
|
||||||
|
e: Unresolved reference: nekohasekai
|
||||||
|
```
|
||||||
|
|
||||||
|
**Причина**: Отсутствует libcore.aar.
|
||||||
|
|
||||||
|
**Решение**: Скачать libcore v3.1.8 (см. шаг 4).
|
||||||
|
|
||||||
|
#### Ошибка 4: Error while dexing
|
||||||
|
```
|
||||||
|
ERROR:D8: com.android.tools.r8.kotlin.H
|
||||||
|
Execution failed for task ':app:mergeExtDexDebug'.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Причина**: Несовместимость AGP 7.4.2 с Gradle 8.7.
|
||||||
|
|
||||||
|
**Решение**: Обновить AGP до 8.2.0 и Kotlin до 1.9.22 (см. шаг 3).
|
||||||
|
|
||||||
|
#### Ошибка 5: Namespace not specified
|
||||||
|
```
|
||||||
|
Namespace not specified. Specify a namespace in the module's build file.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Причина**: flutter_easy_permission не закомментирован полностью в pubspec.yaml.
|
||||||
|
|
||||||
|
**Решение**: Проверить что ВСЕ строки (включая git секцию) закомментированы.
|
||||||
|
|
||||||
|
#### Ошибка 6: Android resource linking failed
|
||||||
|
```
|
||||||
|
aapt2 E: Failed to load resources table in APK '.../android-35/android.jar'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Причина**: compileSdk 35 несовместим с AGP 7.x или поврежден SDK.
|
||||||
|
|
||||||
|
**Решение**: Использовать compileSdk 34.
|
||||||
|
|
||||||
|
### Структура изменений
|
||||||
|
|
||||||
|
#### Измененные файлы:
|
||||||
|
|
||||||
|
1. **pubspec.yaml**
|
||||||
|
- Закомментирован flutter_easy_permission
|
||||||
|
|
||||||
|
2. **lib/features/common/qr_code_scanner_screen.dart**
|
||||||
|
- Упрощена проверка permissions
|
||||||
|
- Удалены вызовы FlutterEasyPermission
|
||||||
|
|
||||||
|
3. **android/settings.gradle**
|
||||||
|
- AGP: 7.4.2 → 8.2.0
|
||||||
|
- Kotlin: 1.8.21 → 1.9.22
|
||||||
|
|
||||||
|
4. **android/gradle/wrapper/gradle-wrapper.properties**
|
||||||
|
- Gradle: 7.6.1 → 8.7
|
||||||
|
|
||||||
|
5. **android/app/build.gradle**
|
||||||
|
- Без изменений (compileSdk 34, targetSdk 34 остались)
|
||||||
|
|
||||||
|
#### Добавленные файлы:
|
||||||
|
|
||||||
|
- **android/app/libs/libcore.aar** (119 MB) - Core библиотека sing-box
|
||||||
|
|
||||||
|
### Очистка после ошибок
|
||||||
|
|
||||||
|
Если сборка не удалась:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Полная очистка
|
||||||
|
rm -rf ~/.gradle/caches/
|
||||||
|
rm -rf ~/.pub-cache/git/flutter_easy_permission-*
|
||||||
|
rm -rf .flutter-plugins*
|
||||||
|
rm -f pubspec.lock
|
||||||
|
|
||||||
|
# Пересборка
|
||||||
|
$HOME/.pub-cache/bin/fvm flutter clean
|
||||||
|
$HOME/.pub-cache/bin/fvm flutter pub get
|
||||||
|
$HOME/.pub-cache/bin/fvm flutter pub run build_runner build --delete-conflicting-outputs
|
||||||
|
./android/gradlew -p android clean
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проверка окружения
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Flutter
|
||||||
|
$HOME/.pub-cache/bin/fvm flutter doctor -v
|
||||||
|
|
||||||
|
# Java
|
||||||
|
java -version
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
./android/gradlew -p android --version
|
||||||
|
|
||||||
|
# Android SDK
|
||||||
|
ls -la $HOME/Android/Sdk/platforms/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Время сборки
|
||||||
|
|
||||||
|
- **Первая сборка**: ~3-5 минут (с загрузкой зависимостей)
|
||||||
|
- **Повторная сборка**: ~1.5-2 минуты
|
||||||
|
- **Сборка после clean**: ~2-3 минуты
|
||||||
|
|
||||||
|
### Размер артефактов
|
||||||
|
|
||||||
|
- **app-debug.apk** (universal): 193 MB
|
||||||
|
- **app-arm64-v8a-debug.apk**: 88 MB (рекомендуется для современных устройств)
|
||||||
|
- **app-armeabi-v7a-debug.apk**: 83 MB (для старых устройств)
|
||||||
|
- **app-x86_64-debug.apk**: 86 MB (для эмуляторов)
|
||||||
|
|
||||||
|
### Примечания
|
||||||
|
|
||||||
|
1. **Не используйте системный Flutter** - только через FVM с версией 3.24.0
|
||||||
|
2. **Java 17 обязателен** - Java 21 не совместим с Gradle 7.x/8.x конфигурацией проекта
|
||||||
|
3. **libcore нельзя пропустить** - без него будут ошибки Kotlin компиляции
|
||||||
|
4. **AGP 8.2+ обязателен** для Gradle 8.7
|
||||||
|
5. **flutter_easy_permission** должен быть полностью закомментирован, включая git секцию
|
||||||
|
|
||||||
|
### Дополнительная информация
|
||||||
|
|
||||||
|
- **Официальный репозиторий**: https://github.com/hiddify/hiddify-app
|
||||||
|
- **Релиз v2.5.7**: https://github.com/hiddify/hiddify-app/releases/tag/v2.5.7
|
||||||
|
- **libcore v3.1.8**: https://github.com/hiddify/hiddify-next-core/releases/tag/v3.1.8
|
||||||
|
- **CI/CD конфигурация**: `.github/workflows/build.yml` (использует Flutter 3.24.0)
|
||||||
|
|
||||||
|
### Контрольные суммы
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Проверка libcore.aar
|
||||||
|
ls -lh android/app/libs/libcore.aar
|
||||||
|
# Должно быть: ~119M
|
||||||
|
|
||||||
|
# Проверка APK
|
||||||
|
ls -lh build/app/outputs/flutter-apk/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Дата создания документа**: 25 декабря 2025 г.
|
||||||
|
**Версия Hiddify**: 2.5.7
|
||||||
|
**Статус сборки**: ✅ Успешно
|
||||||
@@ -54,7 +54,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "app.hiddify.com"
|
applicationId "com.hiddify.app.test"
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 34
|
targetSdkVersion 34
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ pluginManagement {
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
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.2.0" apply false
|
||||||
id "org.jetbrains.kotlin.android" version "1.8.21" apply false
|
id "org.jetbrains.kotlin.android" version "1.9.22" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
include ":app"
|
include ":app"
|
||||||
|
|||||||
@@ -220,14 +220,32 @@
|
|||||||
"perAppProxyModes": {
|
"perAppProxyModes": {
|
||||||
"off": "All",
|
"off": "All",
|
||||||
"offMsg": "Proxy All Apps",
|
"offMsg": "Proxy All Apps",
|
||||||
"include": "Proxy",
|
"include": "Include",
|
||||||
"includeMsg": "Proxy Only Selected Apps",
|
"includeMsg": "Proxy Only Selected Apps",
|
||||||
"exclude": "Bypass",
|
"exclude": "Exclude",
|
||||||
"excludeMsg": "Do Not Proxy Selected Apps"
|
"excludeMsg": "Do Not Proxy Selected Apps"
|
||||||
},
|
},
|
||||||
"showSystemApps": "Show System Apps",
|
"showSystemApps": "Show System Apps",
|
||||||
"hideSystemApps": "Hide System Apps",
|
"hideSystemApps": "Hide System Apps",
|
||||||
"clearSelection": "Clear Selection"
|
"clearSelection": "Clear Selection",
|
||||||
|
"excludedDomains": {
|
||||||
|
"pageTitle": "Exclusions",
|
||||||
|
"domainsTab": "Domains",
|
||||||
|
"appsTab": "Applications",
|
||||||
|
"addButton": "Add Domains or Zones",
|
||||||
|
"addModalTitle": "+ Add Domains",
|
||||||
|
"addOwnDomain": "Add your site:",
|
||||||
|
"domainInputHint": "site.com",
|
||||||
|
"selectReadyZones": "Or select ready-made zones:",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"ok": "OK",
|
||||||
|
"helpTitle": "Excluded Domains",
|
||||||
|
"helpDescription": "Domains and domain zones from this list will bypass VPN and use direct connection.",
|
||||||
|
"helpButton": "Got it",
|
||||||
|
"emptyState": "No Excluded Domains",
|
||||||
|
"emptyStateDescription": "Add domains that should bypass VPN",
|
||||||
|
"fabButton": "Add"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"geoAssets": {
|
"geoAssets": {
|
||||||
"pageTitle": "Routing Assets",
|
"pageTitle": "Routing Assets",
|
||||||
|
|||||||
@@ -220,14 +220,32 @@
|
|||||||
"perAppProxyModes": {
|
"perAppProxyModes": {
|
||||||
"off": "Все",
|
"off": "Все",
|
||||||
"offMsg": "Проксировать все приложения",
|
"offMsg": "Проксировать все приложения",
|
||||||
"include": "Прокси",
|
"include": "Включить",
|
||||||
"includeMsg": "Проксировать выбранные приложения",
|
"includeMsg": "Проксировать выбранные приложения",
|
||||||
"exclude": "Обход",
|
"exclude": "Исключить",
|
||||||
"excludeMsg": "Не проксировать выбранные приложения"
|
"excludeMsg": "Не проксировать выбранные приложения"
|
||||||
},
|
},
|
||||||
"showSystemApps": "Показать системные приложения",
|
"showSystemApps": "Показать системные приложения",
|
||||||
"hideSystemApps": "Скрыть системные приложения",
|
"hideSystemApps": "Скрыть системные приложения",
|
||||||
"clearSelection": "Очистить выбор"
|
"clearSelection": "Очистить выбор",
|
||||||
|
"excludedDomains": {
|
||||||
|
"pageTitle": "Исключения",
|
||||||
|
"domainsTab": "Домены",
|
||||||
|
"appsTab": "Приложения",
|
||||||
|
"addButton": "Добавить домены или зоны",
|
||||||
|
"addModalTitle": "+ Добавить домены",
|
||||||
|
"addOwnDomain": "Добавьте свой сайт:",
|
||||||
|
"domainInputHint": "site.com",
|
||||||
|
"selectReadyZones": "Или выберите готовые зоны:",
|
||||||
|
"cancel": "Отмена",
|
||||||
|
"ok": "ОК",
|
||||||
|
"helpTitle": "Исключённые домены",
|
||||||
|
"helpDescription": "Домены и доменные зоны из этого списка будут обходить VPN и использовать прямое подключение.",
|
||||||
|
"helpButton": "Понятно",
|
||||||
|
"emptyState": "Нет исключённых доменов",
|
||||||
|
"emptyStateDescription": "Добавьте домены, которые должны обходить VPN",
|
||||||
|
"fabButton": "Добавить"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"geoAssets": {
|
"geoAssets": {
|
||||||
"pageTitle": "Активы маршрутизации",
|
"pageTitle": "Активы маршрутизации",
|
||||||
|
|||||||
@@ -113,3 +113,20 @@ class PerAppProxyList extends _$PerAppProxyList {
|
|||||||
return _exclude.write(value);
|
return _exclude.write(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
class ExcludedDomainsList extends _$ExcludedDomainsList {
|
||||||
|
late final _pref = PreferencesEntry(
|
||||||
|
preferences: ref.watch(sharedPreferencesProvider).requireValue,
|
||||||
|
key: "excluded_domains_list",
|
||||||
|
defaultValue: <String>[],
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<String> build() => _pref.read();
|
||||||
|
|
||||||
|
Future<void> update(List<String> value) {
|
||||||
|
state = value;
|
||||||
|
return _pref.write(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ part 'app_router.g.dart';
|
|||||||
|
|
||||||
bool _debugMobileRouter = false;
|
bool _debugMobileRouter = false;
|
||||||
|
|
||||||
final useMobileRouter =
|
final useMobileRouter = !PlatformUtils.isDesktop || (kDebugMode && _debugMobileRouter);
|
||||||
!PlatformUtils.isDesktop || (kDebugMode && _debugMobileRouter);
|
|
||||||
final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>();
|
final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
// TODO: test and improve handling of deep link
|
// TODO: test and improve handling of deep link
|
||||||
@@ -53,6 +52,7 @@ GoRouter router(RouterRef ref) {
|
|||||||
final tabLocations = [
|
final tabLocations = [
|
||||||
const HomeRoute().location,
|
const HomeRoute().location,
|
||||||
const ProxiesRoute().location,
|
const ProxiesRoute().location,
|
||||||
|
const PerAppProxyRoute().location,
|
||||||
const ConfigOptionsRoute().location,
|
const ConfigOptionsRoute().location,
|
||||||
const SettingsRoute().location,
|
const SettingsRoute().location,
|
||||||
const LogsOverviewRoute().location,
|
const LogsOverviewRoute().location,
|
||||||
@@ -77,9 +77,7 @@ void switchTab(int index, BuildContext context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
class RouterListenable extends _$RouterListenable
|
class RouterListenable extends _$RouterListenable with AppLogger implements Listenable {
|
||||||
with AppLogger
|
|
||||||
implements Listenable {
|
|
||||||
VoidCallback? _routerListener;
|
VoidCallback? _routerListener;
|
||||||
bool _introCompleted = false;
|
bool _introCompleted = false;
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ class AppTheme {
|
|||||||
final String fontFamily;
|
final String fontFamily;
|
||||||
|
|
||||||
ThemeData lightTheme(ColorScheme? lightColorScheme) {
|
ThemeData lightTheme(ColorScheme? lightColorScheme) {
|
||||||
final ColorScheme scheme = lightColorScheme ??
|
final ColorScheme scheme = lightColorScheme ?? ColorScheme.fromSeed(seedColor: const Color(0xFF293CA0));
|
||||||
ColorScheme.fromSeed(seedColor: const Color(0xFF293CA0));
|
|
||||||
return ThemeData(
|
return ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
colorScheme: scheme,
|
colorScheme: scheme,
|
||||||
@@ -29,8 +28,7 @@ class AppTheme {
|
|||||||
return ThemeData(
|
return ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
colorScheme: scheme,
|
colorScheme: scheme,
|
||||||
scaffoldBackgroundColor:
|
scaffoldBackgroundColor: mode.trueBlack ? Colors.black : scheme.background,
|
||||||
mode.trueBlack ? Colors.black : scheme.background,
|
|
||||||
fontFamily: fontFamily,
|
fontFamily: fontFamily,
|
||||||
extensions: const <ThemeExtension<dynamic>>{
|
extensions: const <ThemeExtension<dynamic>>{
|
||||||
ConnectionButtonTheme.light,
|
ConnectionButtonTheme.light,
|
||||||
|
|||||||
@@ -8,19 +8,13 @@ part 'theme_preferences.g.dart';
|
|||||||
class ThemePreferences extends _$ThemePreferences {
|
class ThemePreferences extends _$ThemePreferences {
|
||||||
@override
|
@override
|
||||||
AppThemeMode build() {
|
AppThemeMode build() {
|
||||||
final persisted = ref
|
final persisted = ref.watch(sharedPreferencesProvider).requireValue.getString("theme_mode");
|
||||||
.watch(sharedPreferencesProvider)
|
|
||||||
.requireValue
|
|
||||||
.getString("theme_mode");
|
|
||||||
if (persisted == null) return AppThemeMode.system;
|
if (persisted == null) return AppThemeMode.system;
|
||||||
return AppThemeMode.values.byName(persisted);
|
return AppThemeMode.values.byName(persisted);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> changeThemeMode(AppThemeMode value) async {
|
Future<void> changeThemeMode(AppThemeMode value) async {
|
||||||
state = value;
|
state = value;
|
||||||
await ref
|
await ref.read(sharedPreferencesProvider).requireValue.setString("theme_mode", value.name);
|
||||||
.read(sharedPreferencesProvider)
|
|
||||||
.requireValue
|
|
||||||
.setString("theme_mode", value.name);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
abstract interface class RootScaffold {
|
abstract interface class RootScaffold {
|
||||||
static final stateKey = GlobalKey<ScaffoldState>();
|
static final stateKey = GlobalKey<ScaffoldState>();
|
||||||
|
|
||||||
static bool canShowDrawer(BuildContext context) =>
|
static bool canShowDrawer(BuildContext context) => Breakpoints.small.isActive(context);
|
||||||
Breakpoints.small.isActive(context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class AdaptiveRootScaffold extends HookConsumerWidget {
|
class AdaptiveRootScaffold extends HookConsumerWidget {
|
||||||
@@ -26,13 +25,20 @@ class AdaptiveRootScaffold extends HookConsumerWidget {
|
|||||||
|
|
||||||
final destinations = [
|
final destinations = [
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
icon: const Icon(FluentIcons.power_20_filled),
|
icon: const Icon(FluentIcons.home_20_regular),
|
||||||
|
selectedIcon: const Icon(FluentIcons.home_20_filled),
|
||||||
label: t.home.pageTitle,
|
label: t.home.pageTitle,
|
||||||
),
|
),
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
icon: const Icon(FluentIcons.filter_20_filled),
|
icon: const Icon(FluentIcons.list_20_regular),
|
||||||
|
selectedIcon: const Icon(FluentIcons.list_20_filled),
|
||||||
label: t.proxies.pageTitle,
|
label: t.proxies.pageTitle,
|
||||||
),
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
icon: const Icon(FluentIcons.more_vertical_20_regular),
|
||||||
|
selectedIcon: const Icon(FluentIcons.more_vertical_20_filled),
|
||||||
|
label: t.settings.network.excludedDomains.pageTitle,
|
||||||
|
),
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
icon: const Icon(FluentIcons.box_edit_20_filled),
|
icon: const Icon(FluentIcons.box_edit_20_filled),
|
||||||
label: t.config.pageTitle,
|
label: t.config.pageTitle,
|
||||||
@@ -58,8 +64,8 @@ class AdaptiveRootScaffold extends HookConsumerWidget {
|
|||||||
switchTab(index, context);
|
switchTab(index, context);
|
||||||
},
|
},
|
||||||
destinations: destinations,
|
destinations: destinations,
|
||||||
drawerDestinationRange: useMobileRouter ? (2, null) : (0, null),
|
drawerDestinationRange: useMobileRouter ? (3, null) : (0, null),
|
||||||
bottomDestinationRange: (0, 2),
|
bottomDestinationRange: (0, 3),
|
||||||
useBottomSheet: useMobileRouter,
|
useBottomSheet: useMobileRouter,
|
||||||
sidebarTrailing: const Expanded(
|
sidebarTrailing: const Expanded(
|
||||||
child: Align(
|
child: Align(
|
||||||
@@ -93,18 +99,14 @@ class _CustomAdaptiveScaffold extends HookConsumerWidget {
|
|||||||
final Widget? sidebarTrailing;
|
final Widget? sidebarTrailing;
|
||||||
final Widget body;
|
final Widget body;
|
||||||
|
|
||||||
List<NavigationDestination> destinationsSlice((int, int?) range) =>
|
List<NavigationDestination> destinationsSlice((int, int?) range) => destinations.sublist(range.$1, range.$2);
|
||||||
destinations.sublist(range.$1, range.$2);
|
|
||||||
|
|
||||||
int? selectedWithOffset((int, int?) range) {
|
int? selectedWithOffset((int, int?) range) {
|
||||||
final index = selectedIndex - range.$1;
|
final index = selectedIndex - range.$1;
|
||||||
return index < 0 || (range.$2 != null && index > (range.$2! - 1))
|
return index < 0 || (range.$2 != null && index > (range.$2! - 1)) ? null : index;
|
||||||
? null
|
|
||||||
: index;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void selectWithOffset(int index, (int, int?) range) =>
|
void selectWithOffset(int index, (int, int?) range) => onSelectedIndexChange(index + range.$1);
|
||||||
onSelectedIndexChange(index + range.$1);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -113,14 +115,67 @@ class _CustomAdaptiveScaffold extends HookConsumerWidget {
|
|||||||
drawer: Breakpoints.small.isActive(context)
|
drawer: Breakpoints.small.isActive(context)
|
||||||
? Drawer(
|
? Drawer(
|
||||||
width: (MediaQuery.sizeOf(context).width * 0.88).clamp(1, 304),
|
width: (MediaQuery.sizeOf(context).width * 0.88).clamp(1, 304),
|
||||||
child: NavigationRail(
|
child: Column(
|
||||||
extended: true,
|
children: [
|
||||||
selectedIndex: selectedWithOffset(drawerDestinationRange),
|
// Логотип и название приложения
|
||||||
destinations: destinationsSlice(drawerDestinationRange)
|
Container(
|
||||||
.map((dest) => AdaptiveScaffold.toRailDestination(dest))
|
padding: const EdgeInsets.symmetric(vertical: 32),
|
||||||
.toList(),
|
child: Column(
|
||||||
onDestinationSelected: (index) =>
|
children: [
|
||||||
selectWithOffset(index, drawerDestinationRange),
|
Container(
|
||||||
|
width: 96,
|
||||||
|
height: 96,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
color: Theme.of(context).colorScheme.primaryContainer,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.shield_outlined,
|
||||||
|
size: 56,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Umbrix',
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Список пунктов меню
|
||||||
|
Expanded(
|
||||||
|
child: ListView(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
children: [
|
||||||
|
// Главная
|
||||||
|
_DrawerMenuItem(
|
||||||
|
icon: FluentIcons.home_20_regular,
|
||||||
|
selectedIcon: FluentIcons.home_20_filled,
|
||||||
|
label: destinationsSlice(drawerDestinationRange)[0].label,
|
||||||
|
isSelected: selectedWithOffset(drawerDestinationRange) == 0,
|
||||||
|
onTap: () => selectWithOffset(0, drawerDestinationRange),
|
||||||
|
),
|
||||||
|
// Остальные пункты
|
||||||
|
...List.generate(
|
||||||
|
destinationsSlice(drawerDestinationRange).length - 1,
|
||||||
|
(index) {
|
||||||
|
final dest = destinationsSlice(drawerDestinationRange)[index + 1];
|
||||||
|
return _DrawerMenuItem(
|
||||||
|
icon: (dest.icon as Icon).icon!,
|
||||||
|
selectedIcon: dest.selectedIcon != null ? (dest.selectedIcon as Icon).icon! : (dest.icon as Icon).icon!,
|
||||||
|
label: dest.label,
|
||||||
|
isSelected: selectedWithOffset(drawerDestinationRange) == index + 1,
|
||||||
|
onTap: () => selectWithOffset(index + 1, drawerDestinationRange),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
@@ -131,9 +186,7 @@ class _CustomAdaptiveScaffold extends HookConsumerWidget {
|
|||||||
key: const Key('primaryNavigation'),
|
key: const Key('primaryNavigation'),
|
||||||
builder: (_) => AdaptiveScaffold.standardNavigationRail(
|
builder: (_) => AdaptiveScaffold.standardNavigationRail(
|
||||||
selectedIndex: selectedIndex,
|
selectedIndex: selectedIndex,
|
||||||
destinations: destinations
|
destinations: destinations.map((dest) => AdaptiveScaffold.toRailDestination(dest)).toList(),
|
||||||
.map((dest) => AdaptiveScaffold.toRailDestination(dest))
|
|
||||||
.toList(),
|
|
||||||
onDestinationSelected: onSelectedIndexChange,
|
onDestinationSelected: onSelectedIndexChange,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -142,9 +195,7 @@ class _CustomAdaptiveScaffold extends HookConsumerWidget {
|
|||||||
builder: (_) => AdaptiveScaffold.standardNavigationRail(
|
builder: (_) => AdaptiveScaffold.standardNavigationRail(
|
||||||
extended: true,
|
extended: true,
|
||||||
selectedIndex: selectedIndex,
|
selectedIndex: selectedIndex,
|
||||||
destinations: destinations
|
destinations: destinations.map((dest) => AdaptiveScaffold.toRailDestination(dest)).toList(),
|
||||||
.map((dest) => AdaptiveScaffold.toRailDestination(dest))
|
|
||||||
.toList(),
|
|
||||||
onDestinationSelected: onSelectedIndexChange,
|
onDestinationSelected: onSelectedIndexChange,
|
||||||
trailing: sidebarTrailing,
|
trailing: sidebarTrailing,
|
||||||
),
|
),
|
||||||
@@ -167,10 +218,52 @@ class _CustomAdaptiveScaffold extends HookConsumerWidget {
|
|||||||
? NavigationBar(
|
? NavigationBar(
|
||||||
selectedIndex: selectedWithOffset(bottomDestinationRange) ?? 0,
|
selectedIndex: selectedWithOffset(bottomDestinationRange) ?? 0,
|
||||||
destinations: destinationsSlice(bottomDestinationRange),
|
destinations: destinationsSlice(bottomDestinationRange),
|
||||||
onDestinationSelected: (index) =>
|
onDestinationSelected: (index) => selectWithOffset(index, bottomDestinationRange),
|
||||||
selectWithOffset(index, bottomDestinationRange),
|
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _DrawerMenuItem extends StatelessWidget {
|
||||||
|
const _DrawerMenuItem({
|
||||||
|
required this.icon,
|
||||||
|
required this.selectedIcon,
|
||||||
|
required this.label,
|
||||||
|
required this.isSelected,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final IconData icon;
|
||||||
|
final IconData selectedIcon;
|
||||||
|
final String label;
|
||||||
|
final bool isSelected;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 4),
|
||||||
|
child: ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
isSelected ? selectedIcon : icon,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
selected: isSelected,
|
||||||
|
selectedTileColor: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
onTap: onTap,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ bool showDrawerButton(BuildContext context) {
|
|||||||
final String location = GoRouterState.of(context).uri.path;
|
final String location = GoRouterState.of(context).uri.path;
|
||||||
if (location == const HomeRoute().location || location == const ProfilesOverviewRoute().location) return true;
|
if (location == const HomeRoute().location || location == const ProfilesOverviewRoute().location) return true;
|
||||||
if (location.startsWith(const ProxiesRoute().location)) return true;
|
if (location.startsWith(const ProxiesRoute().location)) return true;
|
||||||
|
if (location.startsWith(const PerAppProxyRoute().location)) return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,11 +32,13 @@ class NestedAppBar extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
RootScaffold.canShowDrawer(context);
|
final hasDrawer = RootScaffold.stateKey.currentState?.hasDrawer ?? false;
|
||||||
|
final shouldShowDrawer = showDrawerButton(context);
|
||||||
|
|
||||||
return SliverAppBar(
|
return SliverAppBar(
|
||||||
leading: (RootScaffold.stateKey.currentState?.hasDrawer ?? false) && showDrawerButton(context)
|
leading: hasDrawer && shouldShowDrawer
|
||||||
? DrawerButton(
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.menu),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
RootScaffold.stateKey.currentState?.openDrawer();
|
RootScaffold.stateKey.currentState?.openDrawer();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ import 'dart:developer';
|
|||||||
import 'package:dartx/dartx.dart';
|
import 'package:dartx/dartx.dart';
|
||||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_easy_permission/easy_permissions.dart';
|
// import 'package:flutter_easy_permission/easy_permissions.dart';
|
||||||
import 'package:hiddify/core/localization/translations.dart';
|
import 'package:hiddify/core/localization/translations.dart';
|
||||||
import 'package:hiddify/utils/utils.dart';
|
import 'package:hiddify/utils/utils.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||||
// import 'package:permission_handler/permission_handler.dart';
|
// import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
const permissions = [Permissions.CAMERA];
|
// const permissions = [Permissions.CAMERA];
|
||||||
const permissionGroup = [PermissionGroup.Camera];
|
// const permissionGroup = [PermissionGroup.Camera];
|
||||||
|
|
||||||
class QRCodeScannerScreen extends StatefulHookConsumerWidget {
|
class QRCodeScannerScreen extends StatefulHookConsumerWidget {
|
||||||
const QRCodeScannerScreen({super.key});
|
const QRCodeScannerScreen({super.key});
|
||||||
@@ -62,6 +62,11 @@ class _QRCodeScannerScreenState extends ConsumerState<QRCodeScannerScreen> with
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _requestCameraPermission() async {
|
Future<bool> _requestCameraPermission() async {
|
||||||
|
// Simplified: assuming permission is granted
|
||||||
|
// Original code used flutter_easy_permission which is obsolete
|
||||||
|
return true;
|
||||||
|
|
||||||
|
/* Original code:
|
||||||
final hasPermission = await FlutterEasyPermission.has(
|
final hasPermission = await FlutterEasyPermission.has(
|
||||||
perms: permissions,
|
perms: permissions,
|
||||||
permsGroup: permissionGroup,
|
permsGroup: permissionGroup,
|
||||||
@@ -95,6 +100,7 @@ class _QRCodeScannerScreenState extends ConsumerState<QRCodeScannerScreen> with
|
|||||||
);
|
);
|
||||||
|
|
||||||
return completer.future;
|
return completer.future;
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initializeScanner() async {
|
Future<void> _initializeScanner() async {
|
||||||
@@ -110,7 +116,7 @@ class _QRCodeScannerScreenState extends ConsumerState<QRCodeScannerScreen> with
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
controller.dispose();
|
controller.dispose();
|
||||||
// _easyPermission.dispose();
|
// _easyPermission.dispose();
|
||||||
FlutterEasyPermission().dispose();
|
// FlutterEasyPermission().dispose();
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@@ -124,10 +130,14 @@ class _QRCodeScannerScreenState extends ConsumerState<QRCodeScannerScreen> with
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _checkPermissionAndStartScanner() async {
|
Future<void> _checkPermissionAndStartScanner() async {
|
||||||
|
// Simplified: assuming permission is granted
|
||||||
|
final hasPermission = true;
|
||||||
|
/* Original:
|
||||||
final hasPermission = await FlutterEasyPermission.has(
|
final hasPermission = await FlutterEasyPermission.has(
|
||||||
perms: permissions,
|
perms: permissions,
|
||||||
permsGroup: permissionGroup,
|
permsGroup: permissionGroup,
|
||||||
);
|
);
|
||||||
|
*/
|
||||||
if (hasPermission) {
|
if (hasPermission) {
|
||||||
_startScanner();
|
_startScanner();
|
||||||
} else {
|
} else {
|
||||||
@@ -148,10 +158,14 @@ class _QRCodeScannerScreenState extends ConsumerState<QRCodeScannerScreen> with
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> startQrScannerIfPermissionIsGranted() async {
|
Future<void> startQrScannerIfPermissionIsGranted() async {
|
||||||
|
// Simplified: assuming permission is granted
|
||||||
|
final hasPermission = true;
|
||||||
|
/* Original:
|
||||||
final hasPermission = await FlutterEasyPermission.has(
|
final hasPermission = await FlutterEasyPermission.has(
|
||||||
perms: permissions,
|
perms: permissions,
|
||||||
permsGroup: permissionGroup,
|
permsGroup: permissionGroup,
|
||||||
);
|
);
|
||||||
|
*/
|
||||||
if (hasPermission) {
|
if (hasPermission) {
|
||||||
_startScanner();
|
_startScanner();
|
||||||
// } else {
|
// } else {
|
||||||
@@ -176,23 +190,31 @@ class _QRCodeScannerScreenState extends ConsumerState<QRCodeScannerScreen> with
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
void _showPermissionDialog() {
|
void _showPermissionDialog() {
|
||||||
|
// Simplified: no dialog for now
|
||||||
|
/* Original:
|
||||||
FlutterEasyPermission.showAppSettingsDialog(
|
FlutterEasyPermission.showAppSettingsDialog(
|
||||||
title: "Camera Access Required",
|
title: "Camera Access Required",
|
||||||
rationale: "Permission to camera to scan QR Code",
|
rationale: "Permission to camera to scan QR Code",
|
||||||
positiveButtonText: "Settings",
|
positiveButtonText: "Settings",
|
||||||
negativeButtonText: "Cancel",
|
negativeButtonText: "Cancel",
|
||||||
);
|
);
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final Translations t = ref.watch(translationsProvider);
|
final Translations t = ref.watch(translationsProvider);
|
||||||
|
|
||||||
|
// Simplified: assuming permission is granted
|
||||||
|
final hasPermission = true;
|
||||||
return FutureBuilder(
|
return FutureBuilder(
|
||||||
|
future: Future.value(hasPermission),
|
||||||
|
/* Original:
|
||||||
future: FlutterEasyPermission.has(
|
future: FlutterEasyPermission.has(
|
||||||
perms: permissions,
|
perms: permissions,
|
||||||
permsGroup: permissionGroup,
|
permsGroup: permissionGroup,
|
||||||
),
|
),
|
||||||
|
*/
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
|||||||
@@ -95,6 +95,10 @@ class ConnectionButton extends HookConsumerWidget {
|
|||||||
_ => Assets.images.disconnectNorouz,
|
_ => Assets.images.disconnectNorouz,
|
||||||
},
|
},
|
||||||
useImage: today.day >= 19 && today.day <= 23 && today.month == 3,
|
useImage: today.day >= 19 && today.day <= 23 && today.month == 3,
|
||||||
|
isConnected: switch (connectionStatus) {
|
||||||
|
AsyncData(value: Connected()) => true,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,6 +111,7 @@ class _ConnectionButton extends StatelessWidget {
|
|||||||
required this.buttonColor,
|
required this.buttonColor,
|
||||||
required this.image,
|
required this.image,
|
||||||
required this.useImage,
|
required this.useImage,
|
||||||
|
required this.isConnected,
|
||||||
});
|
});
|
||||||
|
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
@@ -115,6 +120,7 @@ class _ConnectionButton extends StatelessWidget {
|
|||||||
final Color buttonColor;
|
final Color buttonColor;
|
||||||
final AssetGenImage image;
|
final AssetGenImage image;
|
||||||
final bool useImage;
|
final bool useImage;
|
||||||
|
final bool isConnected;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -136,8 +142,8 @@ class _ConnectionButton extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
width: 148,
|
width: 120,
|
||||||
height: 148,
|
height: 120,
|
||||||
child: Material(
|
child: Material(
|
||||||
key: const ValueKey("home_connection_button"),
|
key: const ValueKey("home_connection_button"),
|
||||||
shape: const CircleBorder(),
|
shape: const CircleBorder(),
|
||||||
@@ -145,7 +151,7 @@ class _ConnectionButton extends StatelessWidget {
|
|||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(36),
|
padding: const EdgeInsets.all(30),
|
||||||
child: TweenAnimationBuilder(
|
child: TweenAnimationBuilder(
|
||||||
tween: ColorTween(end: buttonColor),
|
tween: ColorTween(end: buttonColor),
|
||||||
duration: const Duration(milliseconds: 250),
|
duration: const Duration(milliseconds: 250),
|
||||||
@@ -153,11 +159,11 @@ class _ConnectionButton extends StatelessWidget {
|
|||||||
if (useImage) {
|
if (useImage) {
|
||||||
return image.image(filterQuality: FilterQuality.medium);
|
return image.image(filterQuality: FilterQuality.medium);
|
||||||
} else {
|
} else {
|
||||||
return Assets.images.logo.svg(
|
// Определяем какую иконку показывать: play для отключенного, stop для подключенного
|
||||||
colorFilter: ColorFilter.mode(
|
return Icon(
|
||||||
value!,
|
isConnected ? Icons.stop_rounded : Icons.play_arrow_rounded,
|
||||||
BlendMode.srcIn,
|
color: value,
|
||||||
),
|
size: 60,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ class HomePage extends HookConsumerWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Expanded(
|
const Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(top: 160),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -77,6 +79,7 @@ class HomePage extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
if (MediaQuery.sizeOf(context).width < 840) const ActiveProxyFooter(),
|
if (MediaQuery.sizeOf(context).width < 840) const ActiveProxyFooter(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hiddify/core/localization/translations.dart';
|
import 'package:hiddify/core/localization/translations.dart';
|
||||||
import 'package:hiddify/core/preferences/general_preferences.dart';
|
import 'package:hiddify/core/preferences/general_preferences.dart';
|
||||||
import 'package:hiddify/core/widget/adaptive_icon.dart';
|
import 'package:hiddify/core/router/routes.dart';
|
||||||
|
import 'package:hiddify/features/common/nested_app_bar.dart';
|
||||||
import 'package:hiddify/features/per_app_proxy/model/installed_package_info.dart';
|
import 'package:hiddify/features/per_app_proxy/model/installed_package_info.dart';
|
||||||
import 'package:hiddify/features/per_app_proxy/model/per_app_proxy_mode.dart';
|
import 'package:hiddify/features/per_app_proxy/model/per_app_proxy_mode.dart';
|
||||||
import 'package:hiddify/features/per_app_proxy/overview/per_app_proxy_notifier.dart';
|
import 'package:hiddify/features/per_app_proxy/overview/per_app_proxy_notifier.dart';
|
||||||
import 'package:hiddify/utils/utils.dart';
|
import 'package:hiddify/utils/utils.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:sliver_tools/sliver_tools.dart';
|
|
||||||
|
|
||||||
class PerAppProxyPage extends HookConsumerWidget with PresLogger {
|
class PerAppProxyPage extends HookConsumerWidget with PresLogger {
|
||||||
const PerAppProxyPage({super.key});
|
const PerAppProxyPage({super.key});
|
||||||
@@ -28,6 +28,9 @@ class PerAppProxyPage extends HookConsumerWidget with PresLogger {
|
|||||||
final showSystemApps = useState(true);
|
final showSystemApps = useState(true);
|
||||||
final isSearching = useState(false);
|
final isSearching = useState(false);
|
||||||
final searchQuery = useState("");
|
final searchQuery = useState("");
|
||||||
|
final currentTab = useState(0);
|
||||||
|
final domainInputController = useTextEditingController();
|
||||||
|
final tabController = useTabController(initialLength: 2);
|
||||||
|
|
||||||
final filteredPackages = useMemoized(
|
final filteredPackages = useMemoized(
|
||||||
() {
|
() {
|
||||||
@@ -42,9 +45,7 @@ class PerAppProxyPage extends HookConsumerWidget with PresLogger {
|
|||||||
}
|
}
|
||||||
if (!searchQuery.value.isBlank) {
|
if (!searchQuery.value.isBlank) {
|
||||||
result = result.filter(
|
result = result.filter(
|
||||||
(e) => e.name
|
(e) => e.name.toLowerCase().contains(searchQuery.value.toLowerCase()),
|
||||||
.toLowerCase()
|
|
||||||
.contains(searchQuery.value.toLowerCase()),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return result.toList();
|
return result.toList();
|
||||||
@@ -54,9 +55,27 @@ class PerAppProxyPage extends HookConsumerWidget with PresLogger {
|
|||||||
[asyncPackages, showSystemApps.value, searchQuery.value],
|
[asyncPackages, showSystemApps.value, searchQuery.value],
|
||||||
);
|
);
|
||||||
|
|
||||||
return Scaffold(
|
final appBar = NestedAppBar(
|
||||||
appBar: isSearching.value
|
title: Text(t.settings.network.excludedDomains.pageTitle),
|
||||||
? AppBar(
|
actions: [
|
||||||
|
if (currentTab.value == 1 && !isSearching.value)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(FluentIcons.search_24_regular),
|
||||||
|
onPressed: () => isSearching.value = true,
|
||||||
|
tooltip: localizations.searchFieldLabel,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
bottom: TabBar(
|
||||||
|
controller: tabController,
|
||||||
|
onTap: (index) => currentTab.value = index,
|
||||||
|
tabs: [
|
||||||
|
Tab(text: t.settings.network.excludedDomains.domainsTab),
|
||||||
|
Tab(text: t.settings.network.excludedDomains.appsTab),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final searchAppBar = SliverAppBar(
|
||||||
title: TextFormField(
|
title: TextFormField(
|
||||||
onChanged: (value) => searchQuery.value = value,
|
onChanged: (value) => searchQuery.value = value,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
@@ -80,110 +99,252 @@ class PerAppProxyPage extends HookConsumerWidget with PresLogger {
|
|||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
tooltip: localizations.cancelButtonLabel,
|
tooltip: localizations.cancelButtonLabel,
|
||||||
),
|
),
|
||||||
)
|
bottom: TabBar(
|
||||||
: AppBar(
|
controller: tabController,
|
||||||
title: Text(t.settings.network.perAppProxyPageTitle),
|
onTap: (index) => currentTab.value = index,
|
||||||
actions: [
|
tabs: [
|
||||||
IconButton(
|
Tab(text: t.settings.network.excludedDomains.domainsTab),
|
||||||
icon: const Icon(FluentIcons.search_24_regular),
|
Tab(text: t.settings.network.excludedDomains.appsTab),
|
||||||
onPressed: () => isSearching.value = true,
|
],
|
||||||
tooltip: localizations.searchFieldLabel,
|
|
||||||
),
|
),
|
||||||
PopupMenuButton(
|
);
|
||||||
icon: Icon(AdaptiveIcon(context).more),
|
|
||||||
itemBuilder: (context) {
|
return Scaffold(
|
||||||
return [
|
body: CustomScrollView(
|
||||||
PopupMenuItem(
|
slivers: [
|
||||||
child: Text(
|
isSearching.value ? searchAppBar : appBar,
|
||||||
showSystemApps.value
|
SliverFillRemaining(
|
||||||
? t.settings.network.hideSystemApps
|
child: TabBarView(
|
||||||
: t.settings.network.showSystemApps,
|
controller: tabController,
|
||||||
),
|
children: [
|
||||||
onTap: () =>
|
_buildDomainsTab(context, t, ref, domainInputController),
|
||||||
showSystemApps.value = !showSystemApps.value,
|
_buildAppsTab(
|
||||||
),
|
context,
|
||||||
PopupMenuItem(
|
ref,
|
||||||
child: Text(t.settings.network.clearSelection),
|
t,
|
||||||
onTap: () => ref
|
perAppProxyMode,
|
||||||
.read(perAppProxyListProvider.notifier)
|
filteredPackages,
|
||||||
.update([]),
|
perAppProxyList,
|
||||||
),
|
showSystemApps,
|
||||||
];
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
SliverPinnedHeader(
|
|
||||||
child: DecoratedBox(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
|
||||||
),
|
),
|
||||||
child: Column(
|
],
|
||||||
|
),
|
||||||
|
floatingActionButton: currentTab.value == 0
|
||||||
|
? FloatingActionButton.extended(
|
||||||
|
onPressed: () => _showAddDomainModal(context, ref, domainInputController),
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: Text(t.settings.network.excludedDomains.fabButton),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
bottomNavigationBar: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
...PerAppProxyMode.values.map(
|
if (currentTab.value == 1)
|
||||||
(e) => RadioListTile<PerAppProxyMode>(
|
Padding(
|
||||||
title: Text(e.present(t).message),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||||
dense: true,
|
child: Row(
|
||||||
value: e,
|
children: [
|
||||||
groupValue: perAppProxyMode,
|
Expanded(
|
||||||
onChanged: (value) async {
|
child: OutlinedButton(
|
||||||
await ref
|
onPressed: perAppProxyMode == PerAppProxyMode.include
|
||||||
.read(Preferences.perAppProxyMode.notifier)
|
? null
|
||||||
.update(e);
|
: () async {
|
||||||
if (e == PerAppProxyMode.off && context.mounted) {
|
await ref.read(Preferences.perAppProxyMode.notifier).update(PerAppProxyMode.include);
|
||||||
context.pop();
|
},
|
||||||
|
child: Text(t.settings.network.perAppProxyModes.include),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: perAppProxyMode == PerAppProxyMode.exclude
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
await ref.read(Preferences.perAppProxyMode.notifier).update(PerAppProxyMode.exclude);
|
||||||
|
},
|
||||||
|
child: Text(t.settings.network.perAppProxyModes.exclude),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.help_outline),
|
||||||
|
onPressed: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text(t.settings.network.perAppProxyPageTitle),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"${t.settings.network.perAppProxyModes.include}:",
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
Text(t.settings.network.perAppProxyModes.includeMsg),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
"${t.settings.network.perAppProxyModes.exclude}:",
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
Text(t.settings.network.perAppProxyModes.excludeMsg),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text(MaterialLocalizations.of(context).okButtonLabel),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
tooltip: t.settings.network.perAppProxyPageTitle,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
NavigationBar(
|
||||||
|
selectedIndex: 2,
|
||||||
|
destinations: [
|
||||||
|
NavigationDestination(
|
||||||
|
icon: const Icon(FluentIcons.home_20_regular),
|
||||||
|
selectedIcon: const Icon(FluentIcons.home_20_filled),
|
||||||
|
label: t.home.pageTitle,
|
||||||
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
icon: const Icon(FluentIcons.list_20_regular),
|
||||||
|
selectedIcon: const Icon(FluentIcons.list_20_filled),
|
||||||
|
label: t.proxies.pageTitle,
|
||||||
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
icon: const Icon(FluentIcons.more_vertical_20_regular),
|
||||||
|
selectedIcon: const Icon(FluentIcons.more_vertical_20_filled),
|
||||||
|
label: t.settings.network.excludedDomains.pageTitle,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onDestinationSelected: (index) {
|
||||||
|
if (index == 0) {
|
||||||
|
const HomeRoute().go(context);
|
||||||
|
} else if (index == 1) {
|
||||||
|
const ProxiesRoute().go(context);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
|
||||||
const Divider(height: 1),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDomainsTab(
|
||||||
|
BuildContext context,
|
||||||
|
Translations t,
|
||||||
|
WidgetRef ref,
|
||||||
|
TextEditingController domainInputController,
|
||||||
|
) {
|
||||||
|
final excludedDomains = ref.watch(excludedDomainsListProvider);
|
||||||
|
|
||||||
|
return CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
if (excludedDomains.isEmpty)
|
||||||
|
SliverFillRemaining(
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.public_off, size: 64, color: Colors.grey[400]),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
t.settings.network.excludedDomains.emptyState,
|
||||||
|
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
t.settings.network.excludedDomains.emptyStateDescription,
|
||||||
|
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) {
|
||||||
|
final domain = excludedDomains[index];
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(Icons.language),
|
||||||
|
title: Text(domain),
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(Icons.delete_outline),
|
||||||
|
onPressed: () {
|
||||||
|
final newList = List<String>.from(excludedDomains)..removeAt(index);
|
||||||
|
ref.read(excludedDomainsListProvider.notifier).update(newList);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
childCount: excludedDomains.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAppsTab(
|
||||||
|
BuildContext context,
|
||||||
|
WidgetRef ref,
|
||||||
|
Translations t,
|
||||||
|
PerAppProxyMode perAppProxyMode,
|
||||||
|
AsyncValue<List<InstalledPackageInfo>> filteredPackages,
|
||||||
|
List<String> perAppProxyList,
|
||||||
|
ValueNotifier<bool> showSystemApps,
|
||||||
|
) {
|
||||||
|
return CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: CheckboxListTile(
|
||||||
|
title: Text(t.settings.network.hideSystemApps),
|
||||||
|
value: !showSystemApps.value,
|
||||||
|
onChanged: (value) => showSystemApps.value = !(value ?? false),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
switch (filteredPackages) {
|
switch (filteredPackages) {
|
||||||
AsyncData(value: final packages) => SliverList.builder(
|
AsyncData(value: final packages) => packages.isEmpty
|
||||||
itemBuilder: (context, index) {
|
? SliverFillRemaining(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Text('No packages found'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: SliverList.builder(
|
||||||
|
itemBuilder: (_, index) {
|
||||||
final package = packages[index];
|
final package = packages[index];
|
||||||
final selected =
|
final isSelected = perAppProxyList.contains(package.packageName);
|
||||||
perAppProxyList.contains(package.packageName);
|
|
||||||
return CheckboxListTile(
|
return CheckboxListTile(
|
||||||
title: Text(
|
value: isSelected,
|
||||||
package.name,
|
onChanged: (_) async {
|
||||||
overflow: TextOverflow.ellipsis,
|
final newList = List<String>.from(perAppProxyList);
|
||||||
),
|
if (isSelected) {
|
||||||
subtitle: Text(
|
newList.remove(package.packageName);
|
||||||
package.packageName,
|
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
value: selected,
|
|
||||||
onChanged: (value) async {
|
|
||||||
final List<String> newSelection;
|
|
||||||
if (selected) {
|
|
||||||
newSelection = perAppProxyList
|
|
||||||
.exceptElement(package.packageName)
|
|
||||||
.toList();
|
|
||||||
} else {
|
} else {
|
||||||
newSelection = [
|
newList.add(package.packageName);
|
||||||
...perAppProxyList,
|
|
||||||
package.packageName,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
await ref
|
await ref.read(perAppProxyListProvider.notifier).update(newList);
|
||||||
.read(perAppProxyListProvider.notifier)
|
|
||||||
.update(newSelection);
|
|
||||||
},
|
},
|
||||||
|
title: Text(package.name),
|
||||||
|
subtitle: Text(package.packageName),
|
||||||
secondary: SizedBox(
|
secondary: SizedBox(
|
||||||
width: 48,
|
width: 48,
|
||||||
height: 48,
|
height: 48,
|
||||||
child: ref
|
child: ref.watch(packageIconProvider(package.packageName)).when(
|
||||||
.watch(packageIconProvider(package.packageName))
|
|
||||||
.when(
|
|
||||||
data: (data) => Image(image: data),
|
data: (data) => Image(image: data),
|
||||||
error: (error, _) =>
|
error: (error, _) => const Icon(FluentIcons.error_circle_24_regular),
|
||||||
const Icon(FluentIcons.error_circle_24_regular),
|
|
||||||
loading: () => const Center(
|
loading: () => const Center(
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
),
|
),
|
||||||
@@ -193,12 +354,158 @@ class PerAppProxyPage extends HookConsumerWidget with PresLogger {
|
|||||||
},
|
},
|
||||||
itemCount: packages.length,
|
itemCount: packages.length,
|
||||||
),
|
),
|
||||||
AsyncLoading() => const SliverLoadingBodyPlaceholder(),
|
AsyncError() => SliverFillRemaining(
|
||||||
AsyncError(:final error) =>
|
child: Column(
|
||||||
SliverErrorBodyPlaceholder(error.toString()),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
_ => const SliverToBoxAdapter(),
|
children: [
|
||||||
|
const Text('Error loading packages'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_ => const SliverFillRemaining(
|
||||||
|
child: Center(child: CircularProgressIndicator()),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showAddDomainModal(
|
||||||
|
BuildContext context,
|
||||||
|
WidgetRef ref,
|
||||||
|
TextEditingController controller,
|
||||||
|
) {
|
||||||
|
final t = ref.read(translationsProvider);
|
||||||
|
final excludedDomains = ref.read(excludedDomainsListProvider);
|
||||||
|
|
||||||
|
final presetZones = [
|
||||||
|
'.ru',
|
||||||
|
'.рф',
|
||||||
|
'.su',
|
||||||
|
'.by',
|
||||||
|
'.kz',
|
||||||
|
'.ua',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Локальное состояние для выбранных зон
|
||||||
|
final selectedZones = Set<String>.from(excludedDomains.where((d) => presetZones.contains(d)));
|
||||||
|
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder: (context) => StatefulBuilder(
|
||||||
|
builder: (context, setState) => Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
top: 16,
|
||||||
|
bottom: MediaQuery.of(context).viewInsets.bottom + MediaQuery.of(context).padding.bottom + 16,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
t.settings.network.excludedDomains.addModalTitle,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.help_outline),
|
||||||
|
onPressed: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text(t.settings.network.excludedDomains.helpTitle),
|
||||||
|
content: Text(t.settings.network.excludedDomains.helpDescription),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text(MaterialLocalizations.of(context).okButtonLabel),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
t.settings.network.excludedDomains.addOwnDomain,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextField(
|
||||||
|
controller: controller,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: t.settings.network.excludedDomains.domainInputHint,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
t.settings.network.excludedDomains.selectReadyZones,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
...presetZones.map((zone) {
|
||||||
|
final isSelected = selectedZones.contains(zone);
|
||||||
|
return CheckboxListTile(
|
||||||
|
dense: true,
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: Text(zone),
|
||||||
|
value: isSelected,
|
||||||
|
onChanged: (selected) {
|
||||||
|
setState(() {
|
||||||
|
if (selected == true) {
|
||||||
|
selectedZones.add(zone);
|
||||||
|
} else {
|
||||||
|
selectedZones.remove(zone);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text(t.settings.network.excludedDomains.cancel),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
final newList = List<String>.from(excludedDomains);
|
||||||
|
|
||||||
|
// Удаляем все preset зоны из списка
|
||||||
|
newList.removeWhere((d) => presetZones.contains(d));
|
||||||
|
|
||||||
|
// Добавляем выбранные зоны
|
||||||
|
newList.addAll(selectedZones);
|
||||||
|
|
||||||
|
// Добавляем свой домен если введён
|
||||||
|
final domain = controller.text.trim();
|
||||||
|
if (domain.isNotEmpty && !newList.contains(domain)) {
|
||||||
|
newList.add(domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.read(excludedDomainsListProvider.notifier).update(newList);
|
||||||
|
controller.clear();
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: Text(t.settings.network.excludedDomains.ok),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,10 +62,10 @@ class AddProfileModal extends HookConsumerWidget {
|
|||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
child: AnimatedSize(
|
child: AnimatedSize(
|
||||||
duration: const Duration(milliseconds: 250),
|
duration: const Duration(milliseconds: 250),
|
||||||
child: LayoutBuilder(
|
child: Builder(
|
||||||
builder: (context, constraints) {
|
builder: (context) {
|
||||||
// temporary solution, aspect ratio widget relies on height and in a row there no height!
|
// Fixed button width instead of using LayoutBuilder
|
||||||
final buttonWidth = constraints.maxWidth / 2 - (buttonsPadding + (buttonsGap / 2));
|
final buttonWidth = (MediaQuery.of(context).size.width / 2) - (buttonsPadding + (buttonsGap / 2));
|
||||||
|
|
||||||
return AnimatedCrossFade(
|
return AnimatedCrossFade(
|
||||||
firstChild: SizedBox(
|
firstChild: SizedBox(
|
||||||
|
|||||||
@@ -62,9 +62,8 @@ class ProfileTile extends HookConsumerWidget {
|
|||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
shadowColor: Colors.transparent,
|
shadowColor: Colors.transparent,
|
||||||
child: IntrinsicHeight(
|
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
if (profile is RemoteProfileEntity || !isMain) ...[
|
if (profile is RemoteProfileEntity || !isMain) ...[
|
||||||
SizedBox(
|
SizedBox(
|
||||||
@@ -167,7 +166,6 @@ class ProfileTile extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,7 @@ class ProxiesOverviewPage extends HookConsumerWidget with PresLogger {
|
|||||||
final sortBy = ref.watch(proxiesSortNotifierProvider);
|
final sortBy = ref.watch(proxiesSortNotifierProvider);
|
||||||
|
|
||||||
final selectActiveProxyMutation = useMutation(
|
final selectActiveProxyMutation = useMutation(
|
||||||
initialOnFailure: (error) =>
|
initialOnFailure: (error) => CustomToast.error(t.presentShortError(error)).show(context),
|
||||||
CustomToast.error(t.presentShortError(error)).show(context),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
final appBar = NestedAppBar(
|
final appBar = NestedAppBar(
|
||||||
@@ -85,8 +84,7 @@ class ProxiesOverviewPage extends HookConsumerWidget with PresLogger {
|
|||||||
proxy,
|
proxy,
|
||||||
selected: group.selected == proxy.tag,
|
selected: group.selected == proxy.tag,
|
||||||
onSelect: () async {
|
onSelect: () async {
|
||||||
if (selectActiveProxyMutation
|
if (selectActiveProxyMutation.state.isInProgress) {
|
||||||
.state.isInProgress) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
selectActiveProxyMutation.setFuture(
|
selectActiveProxyMutation.setFuture(
|
||||||
@@ -132,7 +130,7 @@ class ProxiesOverviewPage extends HookConsumerWidget with PresLogger {
|
|||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
onPressed: () async => notifier.urlTest(group.tag),
|
onPressed: () async => notifier.urlTest(group.tag),
|
||||||
tooltip: t.proxies.delayTestTooltip,
|
tooltip: t.proxies.delayTestTooltip,
|
||||||
child: const Icon(FluentIcons.flash_24_filled),
|
child: const Icon(FluentIcons.arrow_clockwise_24_filled),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ class AdvancedSettingTiles extends HookConsumerWidget {
|
|||||||
final t = ref.watch(translationsProvider);
|
final t = ref.watch(translationsProvider);
|
||||||
|
|
||||||
final debug = ref.watch(debugModeNotifierProvider);
|
final debug = ref.watch(debugModeNotifierProvider);
|
||||||
final perAppProxy = ref.watch(Preferences.perAppProxyMode).enabled;
|
|
||||||
final disableMemoryLimit = ref.watch(Preferences.disableMemoryLimit);
|
final disableMemoryLimit = ref.watch(Preferences.disableMemoryLimit);
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
@@ -33,28 +32,6 @@ class AdvancedSettingTiles extends HookConsumerWidget {
|
|||||||
// // await const GeoAssetsRoute().push(context);
|
// // await const GeoAssetsRoute().push(context);
|
||||||
// },
|
// },
|
||||||
// ),
|
// ),
|
||||||
if (Platform.isAndroid) ...[
|
|
||||||
ListTile(
|
|
||||||
title: Text(t.settings.network.perAppProxyPageTitle),
|
|
||||||
leading: const Icon(FluentIcons.apps_list_detail_24_regular),
|
|
||||||
trailing: Switch(
|
|
||||||
value: perAppProxy,
|
|
||||||
onChanged: (value) async {
|
|
||||||
final newMode = perAppProxy ? PerAppProxyMode.off : PerAppProxyMode.exclude;
|
|
||||||
await ref.read(Preferences.perAppProxyMode.notifier).update(newMode);
|
|
||||||
if (!perAppProxy && context.mounted) {
|
|
||||||
await const PerAppProxyRoute().push(context);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
onTap: () async {
|
|
||||||
if (!perAppProxy) {
|
|
||||||
await ref.read(Preferences.perAppProxyMode.notifier).update(PerAppProxyMode.exclude);
|
|
||||||
}
|
|
||||||
if (context.mounted) await const PerAppProxyRoute().push(context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
title: Text(t.settings.advanced.memoryLimit),
|
title: Text(t.settings.advanced.memoryLimit),
|
||||||
subtitle: Text(t.settings.advanced.memoryLimitMsg),
|
subtitle: Text(t.settings.advanced.memoryLimitMsg),
|
||||||
|
|||||||
492
pubspec.lock
492
pubspec.lock
File diff suppressed because it is too large
Load Diff
@@ -88,8 +88,8 @@ dependencies:
|
|||||||
json_path: ^0.7.1
|
json_path: ^0.7.1
|
||||||
# permission_handler: ^11.3.0 # is not compatible with windows
|
# permission_handler: ^11.3.0 # is not compatible with windows
|
||||||
#flutter_easy_permission: ^1.1.2
|
#flutter_easy_permission: ^1.1.2
|
||||||
flutter_easy_permission:
|
#flutter_easy_permission:
|
||||||
git: https://github.com/unger1984/flutter_easy_permission.git
|
# git: https://github.com/unger1984/flutter_easy_permission.git
|
||||||
in_app_review: ^2.0.9
|
in_app_review: ^2.0.9
|
||||||
# circle_flags: ^4.0.2
|
# circle_flags: ^4.0.2
|
||||||
circle_flags:
|
circle_flags:
|
||||||
|
|||||||
Reference in New Issue
Block a user