backup: before proxies page modernization

This commit is contained in:
Hiddify User
2025-12-26 02:39:35 +03:00
parent 6e73e53fb6
commit 063f2464ee
25 changed files with 1395 additions and 609 deletions

3
.fvmrc Normal file
View File

@@ -0,0 +1,3 @@
{
"flutter": "3.24.0"
}

4
.gitignore vendored
View File

@@ -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/

View File

@@ -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
View 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
**Статус сборки**: ✅ Успешно

View File

@@ -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()

View File

@@ -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

View File

@@ -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"

View File

@@ -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",

View File

@@ -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": "Активы маршрутизации",

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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,

View File

@@ -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);
} }
} }

View File

@@ -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),
),
);
}
}

View File

@@ -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();
}, },

View File

@@ -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());

View File

@@ -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,
); );
} }
}, },

View File

@@ -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(),
], ],
), ),

View File

@@ -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),
),
],
),
],
),
),
), ),
); );
} }

View File

@@ -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(

View File

@@ -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 {
), ),
], ],
), ),
),
); );
} }
} }

View File

@@ -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),
), ),
); );

View File

@@ -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),

File diff suppressed because it is too large Load Diff

View File

@@ -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: