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/release
|
||||
|
||||
/data
|
||||
|
||||
/data
|
||||
# FVM Version Cache
|
||||
.fvm/
|
||||
37
.vscode/settings.json
vendored
37
.vscode/settings.json
vendored
@@ -1,21 +1,20 @@
|
||||
{
|
||||
"dart.lineLength": 250,
|
||||
"[dart]": {
|
||||
"editor.defaultFormatter": "Dart-Code.dart-code",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnType": true,
|
||||
"editor.tabSize": 2,
|
||||
"editor.rulers": [
|
||||
250
|
||||
],
|
||||
"editor.detectIndentation": false,
|
||||
"editor.selectionHighlight": false,
|
||||
"editor.suggest.snippetsPreventQuickSuggestions": false,
|
||||
"editor.suggestSelection": "first",
|
||||
"editor.tabCompletion": "onlySnippets",
|
||||
"editor.wordBasedSuggestions": "off"
|
||||
},
|
||||
|
||||
"html.format.wrapLineLength": 250,
|
||||
|
||||
"dart.lineLength": 250,
|
||||
"[dart]": {
|
||||
"editor.defaultFormatter": "Dart-Code.dart-code",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnType": true,
|
||||
"editor.tabSize": 2,
|
||||
"editor.rulers": [
|
||||
250
|
||||
],
|
||||
"editor.detectIndentation": false,
|
||||
"editor.selectionHighlight": false,
|
||||
"editor.suggest.snippetsPreventQuickSuggestions": false,
|
||||
"editor.suggestSelection": "first",
|
||||
"editor.tabCompletion": "onlySnippets",
|
||||
"editor.wordBasedSuggestions": "off"
|
||||
},
|
||||
"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 {
|
||||
applicationId "app.hiddify.com"
|
||||
applicationId "com.hiddify.app.test"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 34
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
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
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -18,8 +18,8 @@ pluginManagement {
|
||||
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "7.4.2" apply false
|
||||
id "org.jetbrains.kotlin.android" version "1.8.21" apply false
|
||||
id "com.android.application" version "8.2.0" apply false
|
||||
id "org.jetbrains.kotlin.android" version "1.9.22" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
||||
|
||||
@@ -220,14 +220,32 @@
|
||||
"perAppProxyModes": {
|
||||
"off": "All",
|
||||
"offMsg": "Proxy All Apps",
|
||||
"include": "Proxy",
|
||||
"include": "Include",
|
||||
"includeMsg": "Proxy Only Selected Apps",
|
||||
"exclude": "Bypass",
|
||||
"exclude": "Exclude",
|
||||
"excludeMsg": "Do Not Proxy Selected Apps"
|
||||
},
|
||||
"showSystemApps": "Show 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": {
|
||||
"pageTitle": "Routing Assets",
|
||||
|
||||
@@ -220,14 +220,32 @@
|
||||
"perAppProxyModes": {
|
||||
"off": "Все",
|
||||
"offMsg": "Проксировать все приложения",
|
||||
"include": "Прокси",
|
||||
"include": "Включить",
|
||||
"includeMsg": "Проксировать выбранные приложения",
|
||||
"exclude": "Обход",
|
||||
"exclude": "Исключить",
|
||||
"excludeMsg": "Не проксировать выбранные приложения"
|
||||
},
|
||||
"showSystemApps": "Показать системные приложения",
|
||||
"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": {
|
||||
"pageTitle": "Активы маршрутизации",
|
||||
|
||||
@@ -113,3 +113,20 @@ class PerAppProxyList extends _$PerAppProxyList {
|
||||
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;
|
||||
|
||||
final useMobileRouter =
|
||||
!PlatformUtils.isDesktop || (kDebugMode && _debugMobileRouter);
|
||||
final useMobileRouter = !PlatformUtils.isDesktop || (kDebugMode && _debugMobileRouter);
|
||||
final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
// TODO: test and improve handling of deep link
|
||||
@@ -53,6 +52,7 @@ GoRouter router(RouterRef ref) {
|
||||
final tabLocations = [
|
||||
const HomeRoute().location,
|
||||
const ProxiesRoute().location,
|
||||
const PerAppProxyRoute().location,
|
||||
const ConfigOptionsRoute().location,
|
||||
const SettingsRoute().location,
|
||||
const LogsOverviewRoute().location,
|
||||
@@ -77,9 +77,7 @@ void switchTab(int index, BuildContext context) {
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class RouterListenable extends _$RouterListenable
|
||||
with AppLogger
|
||||
implements Listenable {
|
||||
class RouterListenable extends _$RouterListenable with AppLogger implements Listenable {
|
||||
VoidCallback? _routerListener;
|
||||
bool _introCompleted = false;
|
||||
|
||||
|
||||
@@ -8,8 +8,7 @@ class AppTheme {
|
||||
final String fontFamily;
|
||||
|
||||
ThemeData lightTheme(ColorScheme? lightColorScheme) {
|
||||
final ColorScheme scheme = lightColorScheme ??
|
||||
ColorScheme.fromSeed(seedColor: const Color(0xFF293CA0));
|
||||
final ColorScheme scheme = lightColorScheme ?? ColorScheme.fromSeed(seedColor: const Color(0xFF293CA0));
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: scheme,
|
||||
@@ -29,8 +28,7 @@ class AppTheme {
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: scheme,
|
||||
scaffoldBackgroundColor:
|
||||
mode.trueBlack ? Colors.black : scheme.background,
|
||||
scaffoldBackgroundColor: mode.trueBlack ? Colors.black : scheme.background,
|
||||
fontFamily: fontFamily,
|
||||
extensions: const <ThemeExtension<dynamic>>{
|
||||
ConnectionButtonTheme.light,
|
||||
|
||||
@@ -8,19 +8,13 @@ part 'theme_preferences.g.dart';
|
||||
class ThemePreferences extends _$ThemePreferences {
|
||||
@override
|
||||
AppThemeMode build() {
|
||||
final persisted = ref
|
||||
.watch(sharedPreferencesProvider)
|
||||
.requireValue
|
||||
.getString("theme_mode");
|
||||
final persisted = ref.watch(sharedPreferencesProvider).requireValue.getString("theme_mode");
|
||||
if (persisted == null) return AppThemeMode.system;
|
||||
return AppThemeMode.values.byName(persisted);
|
||||
}
|
||||
|
||||
Future<void> changeThemeMode(AppThemeMode value) async {
|
||||
state = value;
|
||||
await ref
|
||||
.read(sharedPreferencesProvider)
|
||||
.requireValue
|
||||
.setString("theme_mode", value.name);
|
||||
await ref.read(sharedPreferencesProvider).requireValue.setString("theme_mode", value.name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
abstract interface class RootScaffold {
|
||||
static final stateKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
static bool canShowDrawer(BuildContext context) =>
|
||||
Breakpoints.small.isActive(context);
|
||||
static bool canShowDrawer(BuildContext context) => Breakpoints.small.isActive(context);
|
||||
}
|
||||
|
||||
class AdaptiveRootScaffold extends HookConsumerWidget {
|
||||
@@ -26,13 +25,20 @@ class AdaptiveRootScaffold extends HookConsumerWidget {
|
||||
|
||||
final destinations = [
|
||||
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,
|
||||
),
|
||||
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,
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: const Icon(FluentIcons.more_vertical_20_regular),
|
||||
selectedIcon: const Icon(FluentIcons.more_vertical_20_filled),
|
||||
label: t.settings.network.excludedDomains.pageTitle,
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: const Icon(FluentIcons.box_edit_20_filled),
|
||||
label: t.config.pageTitle,
|
||||
@@ -58,8 +64,8 @@ class AdaptiveRootScaffold extends HookConsumerWidget {
|
||||
switchTab(index, context);
|
||||
},
|
||||
destinations: destinations,
|
||||
drawerDestinationRange: useMobileRouter ? (2, null) : (0, null),
|
||||
bottomDestinationRange: (0, 2),
|
||||
drawerDestinationRange: useMobileRouter ? (3, null) : (0, null),
|
||||
bottomDestinationRange: (0, 3),
|
||||
useBottomSheet: useMobileRouter,
|
||||
sidebarTrailing: const Expanded(
|
||||
child: Align(
|
||||
@@ -93,18 +99,14 @@ class _CustomAdaptiveScaffold extends HookConsumerWidget {
|
||||
final Widget? sidebarTrailing;
|
||||
final Widget body;
|
||||
|
||||
List<NavigationDestination> destinationsSlice((int, int?) range) =>
|
||||
destinations.sublist(range.$1, range.$2);
|
||||
List<NavigationDestination> destinationsSlice((int, int?) range) => destinations.sublist(range.$1, range.$2);
|
||||
|
||||
int? selectedWithOffset((int, int?) range) {
|
||||
final index = selectedIndex - range.$1;
|
||||
return index < 0 || (range.$2 != null && index > (range.$2! - 1))
|
||||
? null
|
||||
: index;
|
||||
return index < 0 || (range.$2 != null && index > (range.$2! - 1)) ? null : index;
|
||||
}
|
||||
|
||||
void selectWithOffset(int index, (int, int?) range) =>
|
||||
onSelectedIndexChange(index + range.$1);
|
||||
void selectWithOffset(int index, (int, int?) range) => onSelectedIndexChange(index + range.$1);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -113,14 +115,67 @@ class _CustomAdaptiveScaffold extends HookConsumerWidget {
|
||||
drawer: Breakpoints.small.isActive(context)
|
||||
? Drawer(
|
||||
width: (MediaQuery.sizeOf(context).width * 0.88).clamp(1, 304),
|
||||
child: NavigationRail(
|
||||
extended: true,
|
||||
selectedIndex: selectedWithOffset(drawerDestinationRange),
|
||||
destinations: destinationsSlice(drawerDestinationRange)
|
||||
.map((dest) => AdaptiveScaffold.toRailDestination(dest))
|
||||
.toList(),
|
||||
onDestinationSelected: (index) =>
|
||||
selectWithOffset(index, drawerDestinationRange),
|
||||
child: Column(
|
||||
children: [
|
||||
// Логотип и название приложения
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 32),
|
||||
child: Column(
|
||||
children: [
|
||||
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,
|
||||
@@ -131,9 +186,7 @@ class _CustomAdaptiveScaffold extends HookConsumerWidget {
|
||||
key: const Key('primaryNavigation'),
|
||||
builder: (_) => AdaptiveScaffold.standardNavigationRail(
|
||||
selectedIndex: selectedIndex,
|
||||
destinations: destinations
|
||||
.map((dest) => AdaptiveScaffold.toRailDestination(dest))
|
||||
.toList(),
|
||||
destinations: destinations.map((dest) => AdaptiveScaffold.toRailDestination(dest)).toList(),
|
||||
onDestinationSelected: onSelectedIndexChange,
|
||||
),
|
||||
),
|
||||
@@ -142,9 +195,7 @@ class _CustomAdaptiveScaffold extends HookConsumerWidget {
|
||||
builder: (_) => AdaptiveScaffold.standardNavigationRail(
|
||||
extended: true,
|
||||
selectedIndex: selectedIndex,
|
||||
destinations: destinations
|
||||
.map((dest) => AdaptiveScaffold.toRailDestination(dest))
|
||||
.toList(),
|
||||
destinations: destinations.map((dest) => AdaptiveScaffold.toRailDestination(dest)).toList(),
|
||||
onDestinationSelected: onSelectedIndexChange,
|
||||
trailing: sidebarTrailing,
|
||||
),
|
||||
@@ -167,10 +218,52 @@ class _CustomAdaptiveScaffold extends HookConsumerWidget {
|
||||
? NavigationBar(
|
||||
selectedIndex: selectedWithOffset(bottomDestinationRange) ?? 0,
|
||||
destinations: destinationsSlice(bottomDestinationRange),
|
||||
onDestinationSelected: (index) =>
|
||||
selectWithOffset(index, bottomDestinationRange),
|
||||
onDestinationSelected: (index) => selectWithOffset(index, bottomDestinationRange),
|
||||
)
|
||||
: 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;
|
||||
if (location == const HomeRoute().location || location == const ProfilesOverviewRoute().location) return true;
|
||||
if (location.startsWith(const ProxiesRoute().location)) return true;
|
||||
if (location.startsWith(const PerAppProxyRoute().location)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -31,11 +32,13 @@ class NestedAppBar extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
RootScaffold.canShowDrawer(context);
|
||||
final hasDrawer = RootScaffold.stateKey.currentState?.hasDrawer ?? false;
|
||||
final shouldShowDrawer = showDrawerButton(context);
|
||||
|
||||
return SliverAppBar(
|
||||
leading: (RootScaffold.stateKey.currentState?.hasDrawer ?? false) && showDrawerButton(context)
|
||||
? DrawerButton(
|
||||
leading: hasDrawer && shouldShowDrawer
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.menu),
|
||||
onPressed: () {
|
||||
RootScaffold.stateKey.currentState?.openDrawer();
|
||||
},
|
||||
|
||||
@@ -4,15 +4,15 @@ import 'dart:developer';
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.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/utils/utils.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||
// import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
const permissions = [Permissions.CAMERA];
|
||||
const permissionGroup = [PermissionGroup.Camera];
|
||||
// const permissions = [Permissions.CAMERA];
|
||||
// const permissionGroup = [PermissionGroup.Camera];
|
||||
|
||||
class QRCodeScannerScreen extends StatefulHookConsumerWidget {
|
||||
const QRCodeScannerScreen({super.key});
|
||||
@@ -62,6 +62,11 @@ class _QRCodeScannerScreenState extends ConsumerState<QRCodeScannerScreen> with
|
||||
}
|
||||
|
||||
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(
|
||||
perms: permissions,
|
||||
permsGroup: permissionGroup,
|
||||
@@ -95,6 +100,7 @@ class _QRCodeScannerScreenState extends ConsumerState<QRCodeScannerScreen> with
|
||||
);
|
||||
|
||||
return completer.future;
|
||||
*/
|
||||
}
|
||||
|
||||
Future<void> _initializeScanner() async {
|
||||
@@ -110,7 +116,7 @@ class _QRCodeScannerScreenState extends ConsumerState<QRCodeScannerScreen> with
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
// _easyPermission.dispose();
|
||||
FlutterEasyPermission().dispose();
|
||||
// FlutterEasyPermission().dispose();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
@@ -124,10 +130,14 @@ class _QRCodeScannerScreenState extends ConsumerState<QRCodeScannerScreen> with
|
||||
}
|
||||
|
||||
Future<void> _checkPermissionAndStartScanner() async {
|
||||
// Simplified: assuming permission is granted
|
||||
final hasPermission = true;
|
||||
/* Original:
|
||||
final hasPermission = await FlutterEasyPermission.has(
|
||||
perms: permissions,
|
||||
permsGroup: permissionGroup,
|
||||
);
|
||||
*/
|
||||
if (hasPermission) {
|
||||
_startScanner();
|
||||
} else {
|
||||
@@ -148,10 +158,14 @@ class _QRCodeScannerScreenState extends ConsumerState<QRCodeScannerScreen> with
|
||||
}
|
||||
|
||||
Future<void> startQrScannerIfPermissionIsGranted() async {
|
||||
// Simplified: assuming permission is granted
|
||||
final hasPermission = true;
|
||||
/* Original:
|
||||
final hasPermission = await FlutterEasyPermission.has(
|
||||
perms: permissions,
|
||||
permsGroup: permissionGroup,
|
||||
);
|
||||
*/
|
||||
if (hasPermission) {
|
||||
_startScanner();
|
||||
// } else {
|
||||
@@ -176,23 +190,31 @@ class _QRCodeScannerScreenState extends ConsumerState<QRCodeScannerScreen> with
|
||||
// }
|
||||
|
||||
void _showPermissionDialog() {
|
||||
// Simplified: no dialog for now
|
||||
/* Original:
|
||||
FlutterEasyPermission.showAppSettingsDialog(
|
||||
title: "Camera Access Required",
|
||||
rationale: "Permission to camera to scan QR Code",
|
||||
positiveButtonText: "Settings",
|
||||
negativeButtonText: "Cancel",
|
||||
);
|
||||
*/
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Translations t = ref.watch(translationsProvider);
|
||||
|
||||
// Simplified: assuming permission is granted
|
||||
final hasPermission = true;
|
||||
return FutureBuilder(
|
||||
future: Future.value(hasPermission),
|
||||
/* Original:
|
||||
future: FlutterEasyPermission.has(
|
||||
perms: permissions,
|
||||
permsGroup: permissionGroup,
|
||||
),
|
||||
*/
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
|
||||
@@ -95,6 +95,10 @@ class ConnectionButton extends HookConsumerWidget {
|
||||
_ => Assets.images.disconnectNorouz,
|
||||
},
|
||||
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.image,
|
||||
required this.useImage,
|
||||
required this.isConnected,
|
||||
});
|
||||
|
||||
final VoidCallback onTap;
|
||||
@@ -115,6 +120,7 @@ class _ConnectionButton extends StatelessWidget {
|
||||
final Color buttonColor;
|
||||
final AssetGenImage image;
|
||||
final bool useImage;
|
||||
final bool isConnected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -136,8 +142,8 @@ class _ConnectionButton extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
width: 148,
|
||||
height: 148,
|
||||
width: 120,
|
||||
height: 120,
|
||||
child: Material(
|
||||
key: const ValueKey("home_connection_button"),
|
||||
shape: const CircleBorder(),
|
||||
@@ -145,7 +151,7 @@ class _ConnectionButton extends StatelessWidget {
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(36),
|
||||
padding: const EdgeInsets.all(30),
|
||||
child: TweenAnimationBuilder(
|
||||
tween: ColorTween(end: buttonColor),
|
||||
duration: const Duration(milliseconds: 250),
|
||||
@@ -153,11 +159,11 @@ class _ConnectionButton extends StatelessWidget {
|
||||
if (useImage) {
|
||||
return image.image(filterQuality: FilterQuality.medium);
|
||||
} else {
|
||||
return Assets.images.logo.svg(
|
||||
colorFilter: ColorFilter.mode(
|
||||
value!,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
// Определяем какую иконку показывать: play для отключенного, stop для подключенного
|
||||
return Icon(
|
||||
isConnected ? Icons.stop_rounded : Icons.play_arrow_rounded,
|
||||
color: value,
|
||||
size: 60,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -68,13 +68,16 @@ class HomePage extends HookConsumerWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ConnectionButton(),
|
||||
ActiveProxyDelayIndicator(),
|
||||
],
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 160),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ConnectionButton(),
|
||||
ActiveProxyDelayIndicator(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
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:hiddify/core/localization/translations.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/per_app_proxy_mode.dart';
|
||||
import 'package:hiddify/features/per_app_proxy/overview/per_app_proxy_notifier.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:sliver_tools/sliver_tools.dart';
|
||||
|
||||
class PerAppProxyPage extends HookConsumerWidget with PresLogger {
|
||||
const PerAppProxyPage({super.key});
|
||||
@@ -28,6 +28,9 @@ class PerAppProxyPage extends HookConsumerWidget with PresLogger {
|
||||
final showSystemApps = useState(true);
|
||||
final isSearching = useState(false);
|
||||
final searchQuery = useState("");
|
||||
final currentTab = useState(0);
|
||||
final domainInputController = useTextEditingController();
|
||||
final tabController = useTabController(initialLength: 2);
|
||||
|
||||
final filteredPackages = useMemoized(
|
||||
() {
|
||||
@@ -42,9 +45,7 @@ class PerAppProxyPage extends HookConsumerWidget with PresLogger {
|
||||
}
|
||||
if (!searchQuery.value.isBlank) {
|
||||
result = result.filter(
|
||||
(e) => e.name
|
||||
.toLowerCase()
|
||||
.contains(searchQuery.value.toLowerCase()),
|
||||
(e) => e.name.toLowerCase().contains(searchQuery.value.toLowerCase()),
|
||||
);
|
||||
}
|
||||
return result.toList();
|
||||
@@ -54,152 +55,458 @@ class PerAppProxyPage extends HookConsumerWidget with PresLogger {
|
||||
[asyncPackages, showSystemApps.value, searchQuery.value],
|
||||
);
|
||||
|
||||
final appBar = NestedAppBar(
|
||||
title: Text(t.settings.network.excludedDomains.pageTitle),
|
||||
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(
|
||||
onChanged: (value) => searchQuery.value = value,
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(
|
||||
hintText: "${localizations.searchFieldLabel}...",
|
||||
isDense: true,
|
||||
filled: false,
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
errorBorder: InputBorder.none,
|
||||
focusedErrorBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
),
|
||||
),
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
searchQuery.value = "";
|
||||
isSearching.value = false;
|
||||
},
|
||||
icon: const Icon(Icons.close),
|
||||
tooltip: localizations.cancelButtonLabel,
|
||||
),
|
||||
bottom: TabBar(
|
||||
controller: tabController,
|
||||
onTap: (index) => currentTab.value = index,
|
||||
tabs: [
|
||||
Tab(text: t.settings.network.excludedDomains.domainsTab),
|
||||
Tab(text: t.settings.network.excludedDomains.appsTab),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: isSearching.value
|
||||
? AppBar(
|
||||
title: TextFormField(
|
||||
onChanged: (value) => searchQuery.value = value,
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(
|
||||
hintText: "${localizations.searchFieldLabel}...",
|
||||
isDense: true,
|
||||
filled: false,
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
errorBorder: InputBorder.none,
|
||||
focusedErrorBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
),
|
||||
),
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
searchQuery.value = "";
|
||||
isSearching.value = false;
|
||||
},
|
||||
icon: const Icon(Icons.close),
|
||||
tooltip: localizations.cancelButtonLabel,
|
||||
),
|
||||
)
|
||||
: AppBar(
|
||||
title: Text(t.settings.network.perAppProxyPageTitle),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(FluentIcons.search_24_regular),
|
||||
onPressed: () => isSearching.value = true,
|
||||
tooltip: localizations.searchFieldLabel,
|
||||
),
|
||||
PopupMenuButton(
|
||||
icon: Icon(AdaptiveIcon(context).more),
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
child: Text(
|
||||
showSystemApps.value
|
||||
? t.settings.network.hideSystemApps
|
||||
: t.settings.network.showSystemApps,
|
||||
),
|
||||
onTap: () =>
|
||||
showSystemApps.value = !showSystemApps.value,
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Text(t.settings.network.clearSelection),
|
||||
onTap: () => ref
|
||||
.read(perAppProxyListProvider.notifier)
|
||||
.update([]),
|
||||
),
|
||||
];
|
||||
},
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
isSearching.value ? searchAppBar : appBar,
|
||||
SliverFillRemaining(
|
||||
child: TabBarView(
|
||||
controller: tabController,
|
||||
children: [
|
||||
_buildDomainsTab(context, t, ref, domainInputController),
|
||||
_buildAppsTab(
|
||||
context,
|
||||
ref,
|
||||
t,
|
||||
perAppProxyMode,
|
||||
filteredPackages,
|
||||
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: [
|
||||
if (currentTab.value == 1)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
...PerAppProxyMode.values.map(
|
||||
(e) => RadioListTile<PerAppProxyMode>(
|
||||
title: Text(e.present(t).message),
|
||||
dense: true,
|
||||
value: e,
|
||||
groupValue: perAppProxyMode,
|
||||
onChanged: (value) async {
|
||||
await ref
|
||||
.read(Preferences.perAppProxyMode.notifier)
|
||||
.update(e);
|
||||
if (e == PerAppProxyMode.off && context.mounted) {
|
||||
context.pop();
|
||||
}
|
||||
},
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: perAppProxyMode == PerAppProxyMode.include
|
||||
? null
|
||||
: () async {
|
||||
await ref.read(Preferences.perAppProxyMode.notifier).update(PerAppProxyMode.include);
|
||||
},
|
||||
child: Text(t.settings.network.perAppProxyModes.include),
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
switch (filteredPackages) {
|
||||
AsyncData(value: final packages) => SliverList.builder(
|
||||
itemBuilder: (context, index) {
|
||||
final package = packages[index];
|
||||
final selected =
|
||||
perAppProxyList.contains(package.packageName);
|
||||
return CheckboxListTile(
|
||||
title: Text(
|
||||
package.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(
|
||||
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 {
|
||||
newSelection = [
|
||||
...perAppProxyList,
|
||||
package.packageName,
|
||||
];
|
||||
}
|
||||
await ref
|
||||
.read(perAppProxyListProvider.notifier)
|
||||
.update(newSelection);
|
||||
},
|
||||
secondary: SizedBox(
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: ref
|
||||
.watch(packageIconProvider(package.packageName))
|
||||
.when(
|
||||
data: (data) => Image(image: data),
|
||||
error: (error, _) =>
|
||||
const Icon(FluentIcons.error_circle_24_regular),
|
||||
loading: () => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: packages.length,
|
||||
NavigationBar(
|
||||
selectedIndex: 2,
|
||||
destinations: [
|
||||
NavigationDestination(
|
||||
icon: const Icon(FluentIcons.home_20_regular),
|
||||
selectedIcon: const Icon(FluentIcons.home_20_filled),
|
||||
label: t.home.pageTitle,
|
||||
),
|
||||
AsyncLoading() => const SliverLoadingBodyPlaceholder(),
|
||||
AsyncError(:final error) =>
|
||||
SliverErrorBodyPlaceholder(error.toString()),
|
||||
_ => const SliverToBoxAdapter(),
|
||||
},
|
||||
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);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
AsyncData(value: final packages) => packages.isEmpty
|
||||
? SliverFillRemaining(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('No packages found'),
|
||||
],
|
||||
),
|
||||
)
|
||||
: SliverList.builder(
|
||||
itemBuilder: (_, index) {
|
||||
final package = packages[index];
|
||||
final isSelected = perAppProxyList.contains(package.packageName);
|
||||
return CheckboxListTile(
|
||||
value: isSelected,
|
||||
onChanged: (_) async {
|
||||
final newList = List<String>.from(perAppProxyList);
|
||||
if (isSelected) {
|
||||
newList.remove(package.packageName);
|
||||
} else {
|
||||
newList.add(package.packageName);
|
||||
}
|
||||
await ref.read(perAppProxyListProvider.notifier).update(newList);
|
||||
},
|
||||
title: Text(package.name),
|
||||
subtitle: Text(package.packageName),
|
||||
secondary: SizedBox(
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: ref.watch(packageIconProvider(package.packageName)).when(
|
||||
data: (data) => Image(image: data),
|
||||
error: (error, _) => const Icon(FluentIcons.error_circle_24_regular),
|
||||
loading: () => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: packages.length,
|
||||
),
|
||||
AsyncError() => SliverFillRemaining(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
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,
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
// temporary solution, aspect ratio widget relies on height and in a row there no height!
|
||||
final buttonWidth = constraints.maxWidth / 2 - (buttonsPadding + (buttonsGap / 2));
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
// Fixed button width instead of using LayoutBuilder
|
||||
final buttonWidth = (MediaQuery.of(context).size.width / 2) - (buttonsPadding + (buttonsGap / 2));
|
||||
|
||||
return AnimatedCrossFade(
|
||||
firstChild: SizedBox(
|
||||
|
||||
@@ -62,111 +62,109 @@ class ProfileTile extends HookConsumerWidget {
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
shadowColor: Colors.transparent,
|
||||
child: IntrinsicHeight(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (profile is RemoteProfileEntity || !isMain) ...[
|
||||
SizedBox(
|
||||
width: 48,
|
||||
child: Semantics(
|
||||
sortKey: const OrdinalSortKey(1),
|
||||
child: ProfileActionButton(profile, !isMain),
|
||||
),
|
||||
),
|
||||
VerticalDivider(
|
||||
width: 1,
|
||||
color: effectiveOutlineColor,
|
||||
),
|
||||
],
|
||||
Expanded(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (profile is RemoteProfileEntity || !isMain) ...[
|
||||
SizedBox(
|
||||
width: 48,
|
||||
child: Semantics(
|
||||
button: true,
|
||||
sortKey: isMain ? const OrdinalSortKey(0) : null,
|
||||
focused: isMain,
|
||||
liveRegion: isMain,
|
||||
namesRoute: isMain,
|
||||
label: isMain ? t.profile.activeProfileBtnSemanticLabel : null,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (isMain) {
|
||||
const ProfilesOverviewRoute().go(context);
|
||||
} else {
|
||||
if (selectActiveMutation.state.isInProgress) return;
|
||||
if (profile.active) return;
|
||||
selectActiveMutation.setFuture(
|
||||
ref.read(profilesOverviewNotifierProvider.notifier).selectActiveProfile(profile.id),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 4,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (isMain)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Material(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Colors.transparent,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
profile.name,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontFamily: FontFamily.emoji,
|
||||
),
|
||||
semanticsLabel: t.profile.activeProfileNameSemanticLabel(
|
||||
name: profile.name,
|
||||
),
|
||||
sortKey: const OrdinalSortKey(1),
|
||||
child: ProfileActionButton(profile, !isMain),
|
||||
),
|
||||
),
|
||||
VerticalDivider(
|
||||
width: 1,
|
||||
color: effectiveOutlineColor,
|
||||
),
|
||||
],
|
||||
Expanded(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
sortKey: isMain ? const OrdinalSortKey(0) : null,
|
||||
focused: isMain,
|
||||
liveRegion: isMain,
|
||||
namesRoute: isMain,
|
||||
label: isMain ? t.profile.activeProfileBtnSemanticLabel : null,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (isMain) {
|
||||
const ProfilesOverviewRoute().go(context);
|
||||
} else {
|
||||
if (selectActiveMutation.state.isInProgress) return;
|
||||
if (profile.active) return;
|
||||
selectActiveMutation.setFuture(
|
||||
ref.read(profilesOverviewNotifierProvider.notifier).selectActiveProfile(profile.id),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 4,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (isMain)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Material(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Colors.transparent,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
profile.name,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontFamily: FontFamily.emoji,
|
||||
),
|
||||
semanticsLabel: t.profile.activeProfileNameSemanticLabel(
|
||||
name: profile.name,
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
FluentIcons.caret_down_16_filled,
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
FluentIcons.caret_down_16_filled,
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
Text(
|
||||
profile.name,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleMedium,
|
||||
semanticsLabel: profile.active
|
||||
? t.profile.activeProfileNameSemanticLabel(
|
||||
name: profile.name,
|
||||
)
|
||||
: t.profile.nonActiveProfileBtnSemanticLabel(
|
||||
name: profile.name,
|
||||
),
|
||||
),
|
||||
if (subInfo != null) ...[
|
||||
const Gap(4),
|
||||
RemainingTrafficIndicator(subInfo.ratio),
|
||||
const Gap(4),
|
||||
ProfileSubscriptionInfo(subInfo),
|
||||
const Gap(4),
|
||||
],
|
||||
)
|
||||
else
|
||||
Text(
|
||||
profile.name,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleMedium,
|
||||
semanticsLabel: profile.active
|
||||
? t.profile.activeProfileNameSemanticLabel(
|
||||
name: profile.name,
|
||||
)
|
||||
: t.profile.nonActiveProfileBtnSemanticLabel(
|
||||
name: profile.name,
|
||||
),
|
||||
),
|
||||
if (subInfo != null) ...[
|
||||
const Gap(4),
|
||||
RemainingTrafficIndicator(subInfo.ratio),
|
||||
const Gap(4),
|
||||
ProfileSubscriptionInfo(subInfo),
|
||||
const Gap(4),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,8 +20,7 @@ class ProxiesOverviewPage extends HookConsumerWidget with PresLogger {
|
||||
final sortBy = ref.watch(proxiesSortNotifierProvider);
|
||||
|
||||
final selectActiveProxyMutation = useMutation(
|
||||
initialOnFailure: (error) =>
|
||||
CustomToast.error(t.presentShortError(error)).show(context),
|
||||
initialOnFailure: (error) => CustomToast.error(t.presentShortError(error)).show(context),
|
||||
);
|
||||
|
||||
final appBar = NestedAppBar(
|
||||
@@ -85,8 +84,7 @@ class ProxiesOverviewPage extends HookConsumerWidget with PresLogger {
|
||||
proxy,
|
||||
selected: group.selected == proxy.tag,
|
||||
onSelect: () async {
|
||||
if (selectActiveProxyMutation
|
||||
.state.isInProgress) {
|
||||
if (selectActiveProxyMutation.state.isInProgress) {
|
||||
return;
|
||||
}
|
||||
selectActiveProxyMutation.setFuture(
|
||||
@@ -132,7 +130,7 @@ class ProxiesOverviewPage extends HookConsumerWidget with PresLogger {
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () async => notifier.urlTest(group.tag),
|
||||
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 debug = ref.watch(debugModeNotifierProvider);
|
||||
final perAppProxy = ref.watch(Preferences.perAppProxyMode).enabled;
|
||||
final disableMemoryLimit = ref.watch(Preferences.disableMemoryLimit);
|
||||
|
||||
return Column(
|
||||
@@ -33,28 +32,6 @@ class AdvancedSettingTiles extends HookConsumerWidget {
|
||||
// // 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(
|
||||
title: Text(t.settings.advanced.memoryLimit),
|
||||
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
|
||||
# permission_handler: ^11.3.0 # is not compatible with windows
|
||||
#flutter_easy_permission: ^1.1.2
|
||||
flutter_easy_permission:
|
||||
git: https://github.com/unger1984/flutter_easy_permission.git
|
||||
#flutter_easy_permission:
|
||||
# git: https://github.com/unger1984/flutter_easy_permission.git
|
||||
in_app_review: ^2.0.9
|
||||
# circle_flags: ^4.0.2
|
||||
circle_flags:
|
||||
|
||||
Reference in New Issue
Block a user