diff --git a/.fvm/flutter_sdk b/.fvm/flutter_sdk new file mode 120000 index 00000000..1fde000f --- /dev/null +++ b/.fvm/flutter_sdk @@ -0,0 +1 @@ +/home/vodorod/fvm/versions/3.24.0 \ No newline at end of file diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json new file mode 100644 index 00000000..7bfad02d --- /dev/null +++ b/.fvm/fvm_config.json @@ -0,0 +1,3 @@ +{ + "flutterSdkVersion": "3.24.0" +} \ No newline at end of file diff --git a/.fvm/release b/.fvm/release new file mode 100644 index 00000000..b7bc73b8 --- /dev/null +++ b/.fvm/release @@ -0,0 +1 @@ +3.24.0 \ No newline at end of file diff --git a/.fvm/version b/.fvm/version new file mode 100644 index 00000000..e69de29b diff --git a/.fvm/versions/3.24.0 b/.fvm/versions/3.24.0 new file mode 120000 index 00000000..1fde000f --- /dev/null +++ b/.fvm/versions/3.24.0 @@ -0,0 +1 @@ +/home/vodorod/fvm/versions/3.24.0 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5d73110d..d4a40fe5 100644 --- a/.gitignore +++ b/.gitignore @@ -59,4 +59,4 @@ app.*.map.json /data # FVM Version Cache -.fvm/ \ No newline at end of file +.fvm/lib/core/telegram_config.dart diff --git a/AUDIT_REPORT.md b/AUDIT_REPORT.md new file mode 100644 index 00000000..552f744e --- /dev/null +++ b/AUDIT_REPORT.md @@ -0,0 +1,306 @@ +# 📋 Аудит проекта Umbrix v0.1.0 (build 100) + +**Дата:** 29 декабря 2025 г. +**Flutter:** 3.24.0 (Stable) +**Платформа:** Android SDK 36.1.0 + +--- + +## ✅ Статус сборки + +### 🎯 Успешно собрано +- **APK:** `build/app/outputs/flutter-apk/app-debug.apk` +- **Размер:** 223 MB (debug-режим с символами отладки) +- **Время сборки:** 11.4s (Gradle) +- **Bootstrap:** 2358ms +- **Статус:** ✅ Приложение запущено и работает на эмуляторе + +--- + +## 📊 Статический анализ кода (Flutter Analyze) + +### Общая статистика +- **Всего проблем:** 265 +- **Категории:** + - ❌ **Errors:** 0 (критических ошибок нет) + - ⚠️ **Warnings:** ~30 (неиспользуемые импорты, мёртвый код) + - ℹ️ **Info/Hints:** ~235 (стилистика, рекомендации) + +### Основные проблемы + +#### ⚠️ Warnings (требуют внимания): +1. **Неиспользуемые импорты** (20+ файлов): + - `lib/features/per_app_proxy/overview/per_app_proxy_page.dart:5` - `go_router` + - `lib/features/home/widget/home_page.dart:15` - `active_proxy_notifier.dart` + - `lib/features/settings/widgets/advanced_setting_tiles.dart` - 3 импорта + - И другие... + +2. **Неиспользуемые переменные**: + - `connection_button.dart:31` - `today` + - `system_tray_notifier.dart:64` - `destinations` + - `logs_overview_page.dart:31` - `debug` + - `about_page.dart:28` - `appUpdate` + - И другие... + +3. **Мёртвый код (Dead code)**: + - `profile_notifier.dart:117` - `false && ...` (всегда false) + - `qr_code_scanner_screen.dart:143` - недостижимый код + - `system_tray_notifier.dart:184` - недостижимый код + +4. **Go lang warning**: + - `libcore/config/server.go:46` - impossible condition: `nil != nil` + +#### ℹ️ Info (рекомендации по стилю): +- Отсутствие `const` конструкторов (~100 мест) +- Отсутствие trailing commas (~50 мест) +- Использование deprecated API (~10 мест) +- Generated код (protobuf) - игнорируется + +--- + +## 📦 Зависимости + +### Устаревшие пакеты (требующие обновления): + +#### Критичные (серьёзно устарели): +- `flutter_adaptive_scaffold`: 0.1.12 → **0.3.3+1** (discontinued! ⚠️) +- `go_router`: 13.2.5 → **17.0.1** (+3 мажорные версии) +- `grpc`: 3.2.4 → **5.1.0** (+2 мажорные версии) +- `hooks_riverpod`: 2.6.1 → **3.1.0** +- `freezed_annotation`: 2.4.4 → **3.1.0** +- `riverpod_annotation`: 2.6.1 → **4.0.0** +- `protobuf`: 3.1.0 → **6.0.0** (+3 мажорные версии) + +#### Рекомендуемые к обновлению: +- `mobile_scanner`: 5.2.3 → 7.1.4 +- `package_info_plus`: 5.0.1 → 9.0.0 +- `sentry_flutter`: 7.20.2 → 9.9.1 +- `share_plus`: 7.2.2 → 12.0.1 +- `slang`: 3.32.0 → 4.11.1 +- `upgrader`: 9.0.0 → 12.3.0 +- `window_manager`: 0.3.9 → 0.5.1 +- `wolt_modal_sheet`: 0.4.1 → 0.11.0 + +#### Dev dependencies: +- `build_runner`: 2.4.13 → 2.10.4 +- `dependency_validator`: 3.2.3 → 5.0.3 + +--- + +## 🔧 Конфигурация линтера + +**Файл:** `analysis_options.yaml` + +### Используемые плагины: +- ✅ `package:lint/strict.yaml` (строгие правила) +- ✅ `custom_lint` с `provider_parameters` + +### Исключения: +- `libcore/**` (Go код) +- `**.g.dart` (сгенерированный код) +- `lib/gen/**` (переводы) + +### Отключенные правила: +- `sort_pub_dependencies: false` +- `sort_unnamed_constructors_first: false` +- `avoid_classes_with_only_static_members: false` + +--- + +## 🌍 Переводы (i18n) + +### Статус: ✅ Полная локализация + +**Поддерживаемые языки:** 11 +- 🇸🇦 ar (арабский) +- 🇬🇧 en (английский - базовый) +- 🇪🇸 es (испанский) +- 🇮🇷 fa (персидский) +- 🇫🇷 fr (французский) +- 🇮🇩 id (индонезийский) +- 🇧🇷 pt-BR (португальский) +- 🇷🇺 ru (русский) ✨ +- 🇹🇷 tr (турецкий) +- 🇨🇳 zh-CN (китайский упрощенный) +- 🇹🇼 zh-TW (китайский традиционный) + +**Генератор:** slang v3.32.0 (можно обновить до 4.11.1) +**Время генерации:** 0.229s +**Статус:** Все ключи переведены, включая: +- ✅ `excludedDomains` (добавлены для id, pt-BR, zh-TW в этой сессии) +- ✅ `proxies.pageTitle` изменено с "Proxies" на "Locations" во всех языках + +--- + +## 🛠️ Flutter Doctor + +### ✅ Работающие компоненты: +- Flutter SDK 3.24.0 (stable) +- Android toolchain SDK 36.1.0 +- Android Studio 2025.1.3 +- VS Code 1.106.3 / 1.108.0-insider +- Connected devices (2) +- Network resources + +### ❌ Отсутствующие (для полной поддержки): +- Chrome (для web-разработки) +- Linux toolchain (clang++, CMake, ninja) + +--- + +## 📝 Git Status + +### Изменённые файлы: +- `.gitignore` +- `android/app/build.gradle` +- `android/app/src/main/AndroidManifest.xml` + +### Удалённые файлы (ребрендинг): +- Старый пакет: `com.hiddify.hiddify` +- Новый пакет: `com.umbrix.app` +- Удалено ~50 Kotlin файлов из старого namespace + +--- + +## 🎨 UI Изменения (текущая сессия) + +1. **Размер шрифта:** + - Description: 12px (было: default) + - Buttons: 13px (было: default) + - Цель: уместить русский текст в одну строку + +2. **Переводы:** + - Кнопка "Proxies" → "Локации" (все языки) + - Добавлен раздел "Exclusions" для id, pt-BR, zh-TW + +--- + +## 🚀 Рекомендации + +### 🔴 Критично (сделать в ближайшее время): + +1. **Удалить неиспользуемые импорты:** + ```bash + dart fix --apply + ``` + +2. **Обновить discontinued пакет:** + - Заменить `flutter_adaptive_scaffold` на актуальную альтернативу + +3. **Исправить мёртвый код:** + - `profile_notifier.dart:117` - убрать `false &&` + - Другие dead code warnings + +### 🟡 Важно (планировать): + +4. **Major updates пакетов:** + - Обновить Riverpod 2.x → 3.x (breaking changes!) + - Обновить go_router 13.x → 17.x + - Обновить protobuf 3.x → 6.x + - Обновить grpc 3.x → 5.x + +5. **Размер APK (223 MB):** + - Создать release build для оптимизации + - Проверить, нет ли лишних ресурсов + - Использовать app bundle вместо APK + +### 🟢 Желательно (улучшения): + +6. **Добавить const конструкторы** (производительность) +7. **Добавить trailing commas** (читаемость) +8. **Обновить deprecated API:** + - `AutoDisposeRef` → `Ref` + - `SingboxServiceRef` → `Ref` + - `package:drift_dev/api/migrations.dart` + +9. **Linux support:** + ```bash + sudo apt install clang cmake ninja-build + ``` + +--- + +## 🧪 Плагины для проверки + +### Уже используются: +- ✅ `flutter analyze` (встроенный статический анализатор) +- ✅ `custom_lint` (кастомные правила для Riverpod) +- ✅ `package:lint/strict.yaml` (строгие правила) + +### Рекомендуемые дополнительно: + +1. **dart_code_metrics** (DCM) + ```yaml + dev_dependencies: + dart_code_metrics: ^5.7.6 + ``` + - Циклическая сложность + - Метрики кода + - Anti-patterns + +2. **flutter_lints** + ```yaml + dev_dependencies: + flutter_lints: ^4.0.0 + ``` + - Более современная альтернатива lint + +3. **very_good_analysis** + ```yaml + dev_dependencies: + very_good_analysis: ^5.1.0 + ``` + - Еще более строгие правила от Very Good Ventures + +4. **Dependency checkers:** + ```bash + flutter pub run dependency_validator + ``` + - Проверка неиспользуемых зависимостей + +5. **Security audit:** + ```bash + flutter pub run pubspec_dependency_analyzer + ``` + +--- + +## 📈 Итоговая оценка + +| Критерий | Оценка | Комментарий | +|----------|--------|-------------| +| **Сборка** | ✅ 10/10 | Собирается без ошибок | +| **Код-стиль** | ⚠️ 6/10 | 265 замечаний, но нет критичных | +| **Зависимости** | ⚠️ 5/10 | Много устаревших пакетов | +| **Локализация** | ✅ 10/10 | 11 языков, все ключи переведены | +| **Размер APK** | ⚠️ 6/10 | 223 MB (debug), нужен release | +| **Тесты** | ❓ N/A | Требует проверки | +| **Документация** | ✅ 8/10 | README есть, можно улучшить | + +### Общая оценка: ⚠️ 7.5/10 + +**Вывод:** Проект в хорошем состоянии, основные проблемы - устаревшие зависимости и стилистические замечания линтера. Критических ошибок нет, приложение работает стабильно. + +--- + +## 🎯 План действий + +### Этап 1 (Немедленно): +- [ ] Запустить `dart fix --apply` +- [ ] Удалить неиспользуемые импорты вручную +- [ ] Исправить dead code + +### Этап 2 (На этой неделе): +- [ ] Собрать release APK +- [ ] Проверить размер release build +- [ ] Обновить minor версии пакетов + +### Этап 3 (Планирование): +- [ ] Протестировать major updates +- [ ] Заменить discontinued пакеты +- [ ] Добавить DCM метрики + +--- + +**Отчёт сгенерирован:** GitHub Copilot +**Версия приложения:** Umbrix v0.1.0 (100) diff --git a/DOMAIN_ZONES_AUTO_SELECTION.md b/DOMAIN_ZONES_AUTO_SELECTION.md new file mode 100644 index 00000000..dcd235cf --- /dev/null +++ b/DOMAIN_ZONES_AUTO_SELECTION.md @@ -0,0 +1,172 @@ +# 🌍 Автоматический выбор доменных зон по региону + +## ✅ Что добавлено + +### 1. Расширенный список доменных зон + +**По регионам** (автоматически показываются первыми): + +#### 🇷🇺 Россия и СНГ (Region.ru): +- `.ru`, `.рф`, `.su` - Россия +- `.by` - Беларусь +- `.kz` - Казахстан +- `.ua` - Украина +- `.am` - Армения +- `.ge` - Грузия +- `.md` - Молдова +- `.kg` - Киргизия +- `.uz` - Узбекистан +- `.tm` - Туркменистан +- `.az` - Азербайджан + +#### 🇮🇷 Иран и окружение (Region.ir): +- `.ir`, `.ایران` - Иран +- `.af` - Афганистан +- `.tj`, `.تاجیکستان` - Таджикистан +- `.pk` - Пакистан +- `.iq` - Ирак + +#### 🇨🇳 Китай и Восточная Азия (Region.cn): +- `.cn`, `.中国` - Китай +- `.hk` - Гонконг +- `.tw` - Тайвань +- `.mo` - Макао +- `.sg` - Сингапур +- `.kr` - Южная Корея +- `.jp` - Япония + +#### 🇮🇩 Индонезия и Юго-Восточная Азия (Region.id): +- `.id` - Индонезия +- `.my` - Малайзия +- `.ph` - Филиппины +- `.vn` - Вьетнам +- `.th` - Таиланд +- `.la` - Лаос +- `.mm` - Мьянма +- `.kh` - Камбоджа +- `.bn` - Бруней +- `.tl` - Восточный Тимор + +#### 🇹🇷 Турция и Тюркский мир (Region.tr): +- `.tr` - Турция +- `.az` - Азербайджан +- `.tm` - Туркменистан +- `.uz` - Узбекистан +- `.kg` - Киргизия +- `.kz` - Казахстан + +#### 🇦🇫 Афганистан и окружение (Region.af): +- `.af` - Афганистан +- `.pk` - Пакистан +- `.tj`, `.تاجیکستان` - Таджикистан +- `.ir`, `.ایران` - Иран + +#### 🇧🇷 Бразилия и Латинская Америка (Region.br): +- `.br` - Бразилия +- `.pt` - Португалия +- `.ao` - Ангола +- `.mz` - Мозамбик +- `.mx` - Мексика +- `.ar` - Аргентина +- `.cl` - Чили +- `.co` - Колумбия +- `.ve` - Венесуэла +- `.pe` - Перу + +#### 🇮🇳 Индия и Южная Азия (Region.in_) **[НОВЫЙ РЕГИОН]**: +- `.in`, `.भारत` - Индия +- `.pk` - Пакистан +- `.bd` - Бангладеш +- `.lk` - Шри-Ланка +- `.np` - Непал +- `.bt` - Бутан +- `.mv` - Мальдивы + +### 2. Глобальные популярные зоны (топ-20): + +Всегда показываются после региональных: +- `.com`, `.org`, `.net`, `.info`, `.biz` +- `.co`, `.io`, `.ai`, `.app`, `.dev` +- `.xyz`, `.online`, `.site`, `.tech`, `.store` +- `.me`, `.cc`, `.tv`, `.pro`, `.us` + +## 🤖 Как работает автоматический выбор + +1. **При открытии "Исключения"** → **"+ Добавить домены"**: + - Система определяет регион пользователя (Region.ru, Region.cn, и т.д.) + - Автоматически показывает релевантные зоны **в начале списка** + - Затем добавляет популярные глобальные зоны + +2. **Пример для русского пользователя** (Region.ru): + ``` + [✓] .ru + [✓] .рф + [ ] .su + [ ] .by + [ ] .kz + [ ] .ua + ... (11 региональных зон) + [ ] .com + [ ] .org + [ ] .net + ... (20 глобальных зон) + ``` + +3. **Пример для китайского пользователя** (Region.cn): + ``` + [ ] .cn + [ ] .中国 + [ ] .hk + [ ] .tw + ... (8 региональных зон) + [ ] .com + [ ] .org + ... (глобальные зоны) + ``` + +## 📂 Измененные файлы + +### 1. `lib/core/model/region.dart` +- ✅ Добавлен новый регион `in_` (Индия) +- Теперь всего **9 регионов** (было 8) + +### 2. `lib/features/per_app_proxy/overview/per_app_proxy_page.dart` +- ✅ Добавлены импорты: `Region`, `ConfigOptions` +- ✅ Реализована логика автоматического выбора зон по региону +- ✅ Расширен список с 6 до **80+ доменных зон** +- ✅ Зоны группируются: региональные (по текущему региону) + глобальные + +## 🎯 Преимущества + +1. **Удобство для пользователей**: + - Не нужно искать нужные зоны вручную + - Релевантные зоны показываются первыми + - Поддержка национальных доменов (кириллица, арабский, китайский) + +2. **Полнота покрытия**: + - Россия: 13 зон (было 6) + - Китай: 8 зон (было 6) + - Иран: 7 зон (было 5) + - Индонезия: 10 зон (было 6) + - **Новый регион**: Индия (7 зон) + +3. **Умное поведение**: + - Автоматически определяет регион при первом запуске + - Можно вручную изменить регион в настройках + - Список обновляется при смене региона + +## 📱 Как протестировать + +1. Запусти приложение +2. Открой: **Настройки** → **Сеть** → **Исключения** → **Домены** +3. Нажми **"+ Добавить домены"** +4. Увидишь: + - Для России: `.ru`, `.рф`, `.su`, `.by`, `.kz`, `.ua`, ... (13 зон) + - Затем глобальные: `.com`, `.org`, `.net`, `.io`, `.ai`, ... (20 зон) + +## 🔮 Будущие улучшения + +- [ ] Добавить автоматическую подстановку зон при первом запуске +- [ ] Сделать "быструю кнопку" для добавления всех региональных зон +- [ ] Показывать флаги стран рядом с зонами +- [ ] Добавить поиск по доменным зонам diff --git a/SECURITY_AUDIT_REPORT.md b/SECURITY_AUDIT_REPORT.md new file mode 100644 index 00000000..c2a0ca6e --- /dev/null +++ b/SECURITY_AUDIT_REPORT.md @@ -0,0 +1,354 @@ +# 🔒 ОТЧЁТ ПО БЕЗОПАСНОСТИ И УТЕЧКАМ ДАННЫХ +## Hiddify App v2.5.7 - Полный Аудит + +--- + +## 📧 КОНТАКТЫ ОРИГИНАЛЬНОГО ПРОЕКТА + +### Email адреса разработчиков: +- **contribute@hiddify.com** - основной контакт для контрибьюторов +- **linux@hiddify.com** - для Linux пакетов (deb/rpm) +- **wrt@hiddify.com** - для OpenWRT роутеров + +### Ссылки на проект: +- **GitHub**: https://github.com/hiddify/hiddify-next +- **Telegram**: https://t.me/hiddify +- **API релизов**: https://api.github.com/repos/hiddify/hiddify-next/releases +- **Сайт**: https://hiddify.com/ + +--- + +## 🚨 КРИТИЧЕСКИЕ НАХОДКИ + +### 1. ⚠️ SENTRY - АВТОМАТИЧЕСКАЯ ОТПРАВКА КРАШЕЙ + +**Файл**: `lib/core/analytics/analytics_controller.dart` + +**Что делает**: +- Собирает информацию о крашах приложения +- Отправляет стек-трейсы ошибок на сервера Sentry +- Включает логи, информацию об окружении, версию приложения + +**Куда отправляет**: +```dart +final dsn = !kDebugMode || _testCrashReport ? Environment.sentryDSN : ""; +``` +DSN (Data Source Name) берётся из переменной окружения `sentry_dsn` при сборке. + +**Какие данные собираются**: +```dart +SentryFlutter.init( + (options) { + options.dsn = dsn; + options.environment = env.name; // prod/dev + options.dist = appInfo.release.name; // версия релиза + options.enableNativeCrashHandling = true; // крэши нативного кода + options.enableNdkScopeSync = true; // Android NDK + options.serverName = ""; // имя сервера (пустое) + options.attachThreads = true; // информация о потоках + options.tracesSampleRate = 0.20; // 20% трассировка производительности + options.enableUserInteractionTracing = true; // отслеживание действий пользователя + }, +); +``` + +**⚠️ ВАЖНО**: +- Пользовательские данные **АНОНИМИЗИРОВАНЫ**: +```dart +event.copyWith( + user: SentryUser(email: "", username: "", ipAddress: "0.0.0.0"), +); +``` +- Но всё равно отправляются: стек-трейсы, версия приложения, операционная система, действия перед крашем + +**✅ ХОРОШАЯ НОВОСТЬ**: +- Можно отключить в настройках! +- По умолчанию **ВКЛЮЧЕНО** (`true`) +- Файл: `lib/core/analytics/analytics_controller.dart:23` + +--- + +### 2. 🌍 АВТОМАТИЧЕСКОЕ ОПРЕДЕЛЕНИЕ СТРАНЫ + +**Файл**: `lib/features/intro/widget/intro_page.dart:275` + +**Что делает**: +При первом запуске приложение **АВТОМАТИЧЕСКИ** отправляет запрос к внешнему сервису: + +```dart +final response = await client.get>('https://api.ip.sb/geoip/'); +``` + +**Какие данные отправляются**: +- Ваш **IP адрес** (автоматически виден серверу) +- User-Agent: `Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0` + +**Что получает приложение**: +```json +{ + "country_code": "RU", + "country": "Russia", + ... +} +``` + +**Цель**: Автоматически выбрать язык интерфейса и регион настроек. + +**⚠️ РИСК**: +- Ваш IP адрес становится известен стороннему сервису **api.ip.sb** +- Происходит **БЕЗ СОГЛАСИЯ** пользователя при первом запуске +- Сервис может логировать IP адреса + +--- + +### 3. 🔄 АВТОМАТИЧЕСКАЯ ПРОВЕРКА ОБНОВЛЕНИЙ + +**Файл**: `lib/features/app_update/notifier/app_update_notifier.dart` + +**Что делает**: +Приложение периодически проверяет наличие обновлений, обращаясь к: + +``` +https://raw.githubusercontent.com/hiddify/hiddify-next/main/appcast.xml +``` + +**Файл appcast.xml**: +```xml + + + Hiddify + https://github.com/hiddify/hiddify-next/releases + ... + + +``` + +**Куда отправляется запрос**: +- GitHub Raw (через CDN) +- Может быть виден ваш IP адрес в логах GitHub/CDN + +**Частота**: Раз в 12 часов (`durationUntilAlertAgain: Duration(hours: 12)`) + +**⚠️ ПРИМЕЧАНИЕ**: +- Это стандартная практика для проверки обновлений +- Можно отключить для Google Play релиза (`allowCustomUpdateChecker`) + +--- + +### 4. 🌐 ПРОВЕРКА IP ИНФОРМАЦИИ ПРОКСИ + +**Файл**: `lib/features/proxy/data/proxy_repository.dart:127-129` + +Приложение использует **3 сервиса** для проверки текущего IP при подключении к VPN: + +```dart +"https://api.ip.sb/geoip/": IpInfo.fromIpSbJson, +"https://ipapi.co/json/": IpInfo.fromIpApiCoJson, +"https://ipinfo.io/json/": IpInfo.fromIpInfoIoJson, +``` + +**Что отправляется**: Ваш IP адрес (через VPN, если подключен) + +**Цель**: Показать пользователю текущее местоположение и провайдера + +--- + +## 🛡️ ЧТО НЕ СОБИРАЕТСЯ (ХОРОШИЕ НОВОСТИ) + +✅ **НЕТ Firebase Analytics** (закомментировано в истории) +✅ **НЕТ Google Analytics** +✅ **НЕТ рекламных трекеров** +✅ **НЕТ сбора списка приложений** (кроме Per-App Proxy функции) +✅ **НЕТ доступа к контактам, SMS, звонкам** +✅ **НЕТ передачи VPN трафика третьим лицам** +✅ **НЕТ сбора истории браузера** + +--- + +## 📊 НАСТРОЙКИ ПРИВАТНОСТИ В ПРИЛОЖЕНИИ + +### Где найти: + +**Экран приветствия** (`lib/features/intro/widget/intro_page.dart`): +``` +"Сбор аналитики" +"Сбор данных аналитики и отправка отчетов о сбоях для улучшения приложения" +``` + +**Переводы** (`assets/translations/strings_ru.i18n.json`): +```json +"enableAnalytics": "Включить аналитику", +"enableAnalyticsMsg": "Разрешить сбор аналитики и отправку отчетов о сбоях" +``` + +### Как работает: + +1. **Первый запуск**: Пользователю предлагается включить/отключить аналитику +2. **Сохраняется в**: SharedPreferences (`enable_analytics` ключ) +3. **По умолчанию**: `true` (ВКЛЮЧЕНО) +4. **Можно изменить**: В настройках приложения + +--- + +## 🔐 РЕКОМЕНДАЦИИ ПО БЕЗОПАСНОСТИ + +### Для обычных пользователей: + +1. **Отключите аналитику** при первом запуске +2. Понимайте, что ваш IP виден при первом запросе к `api.ip.sb` +3. Используйте VPN сразу после установки, чтобы скрыть реальный IP + +### Для параноиков: + +1. **Заблокируйте** в файерволе: + - `api.ip.sb` + - `ipapi.co` + - `ipinfo.io` + - `sentry.io` (если отключили аналитику) + - `raw.githubusercontent.com` (отключит проверку обновлений) + +2. **Измените код** перед сборкой: + - Удалите автоматический запрос к `api.ip.sb` (строка 275 в `intro_page.dart`) + - Установите `enableAnalyticsPrefKey` по умолчанию в `false` + +3. **Соберите приложение без Sentry**: + ```bash + flutter build apk --dart-define sentry_dsn="" + ``` + +### Для разработчиков форка (Umbrix): + +1. **Удалите все контакты Hiddify**: + - `contribute@hiddify.com` → замените на свои + - Telegram канал → замените на свой + - GitHub ссылки → замените на свой репозиторий + +2. **Измените URLs**: + ```dart + // lib/core/model/constants.dart + static const githubUrl = "https://github.com/YOUR_ACCOUNT/umbrix"; + static const appCastUrl = "https://raw.githubusercontent.com/YOUR_ACCOUNT/umbrix/main/appcast.xml"; + static const telegramChannelUrl = "https://t.me/YOUR_CHANNEL"; + ``` + +3. **Отключите Sentry** или используйте свой DSN: + ```bash + # В Makefile замените или удалите: + SENTRY_DSN=your_sentry_dsn_here + ``` + +4. **Замените IP определение** на приватное решение: + - Используйте только локальные методы (timezone, locale системы) + - Или предлагайте пользователю выбрать страну вручную + +--- + +## 🔍 БЭКДОРЫ? + +**НЕ ОБНАРУЖЕНО**. Код открытый, проверяется community на GitHub. + +### Проверенные векторы: + +- ✅ Нет скрытых серверов для коммуникации +- ✅ Нет зашифрованных payload +- ✅ Нет подозрительных native библиотек +- ✅ VPN трафик не перенаправляется на третьи сервера +- ✅ Нет обфусцированного кода + +### Единственные внешние соединения: + +1. **Sentry** (опционально, можно отключить) +2. **api.ip.sb** (при первом запуске) +3. **GitHub** (проверка обновлений) +4. **IP check сервисы** (при подключении к VPN, показывают текущий IP) + +--- + +## 📝 ИТОГОВАЯ ОЦЕНКА + +### Уровень приватности: **7/10** ⭐⭐⭐⭐⭐⭐⭐☆☆☆ + +**Плюсы**: +- ✅ Открытый исходный код +- ✅ Можно отключить аналитику +- ✅ Данные анонимизированы в Sentry +- ✅ Нет рекламы и трекеров +- ✅ Нет сбора личных данных + +**Минусы**: +- ⚠️ IP адрес утекает к `api.ip.sb` при первом запуске (БЕЗ СОГЛАСИЯ) +- ⚠️ Sentry включён по умолчанию +- ⚠️ Проверка обновлений с GitHub (виден IP) + +**Вердикт**: Приложение относительно безопасное, но требует настройки для максимальной приватности. + +--- + +## 🛠️ КАК УДАЛИТЬ ВСЕ УТЕЧКИ (ДЛЯ UMBRIX) + +### 1. Удалите автоматическое определение страны: + +**Файл**: `lib/features/intro/widget/intro_page.dart` + +Замените функцию `autoSelectRegion` на: + +```dart +Future autoSelectRegion(WidgetRef ref) async { + // НЕ ДЕЛАЕМ НИЧЕГО - пусть пользователь выберет сам + loggy.debug("Auto region selection disabled for privacy"); +} +``` + +### 2. Отключите Sentry по умолчанию: + +**Файл**: `lib/core/analytics/analytics_controller.dart:23` + +```dart +@override +Future build() async { + return _preferences.getBool(enableAnalyticsPrefKey) ?? false; // ← БЫЛО true +} +``` + +### 3. Удалите проверку обновлений: + +**Файл**: `lib/features/app/widget/app.dart` + +Закомментируйте или удалите: +```dart +// final upgrader = ref.watch(upgraderProvider); +// upgrader: upgrader, +``` + +### 4. Замените все URLs: + +**Файл**: `lib/core/model/constants.dart` + +```dart +abstract class Constants { + static const appName = "Umbrix"; + static const githubUrl = "https://github.com/YOUR_ACCOUNT/umbrix"; + static const githubReleasesApiUrl = + "https://api.github.com/repos/YOUR_ACCOUNT/umbrix/releases"; + static const githubLatestReleaseUrl = + "https://github.com/YOUR_ACCOUNT/umbrix/releases/latest"; + static const appCastUrl = + "https://raw.githubusercontent.com/YOUR_ACCOUNT/umbrix/main/appcast.xml"; + static const telegramChannelUrl = "https://t.me/YOUR_CHANNEL"; + static const privacyPolicyUrl = "https://umbrix.com/privacy-policy/"; + static const termsAndConditionsUrl = "https://umbrix.com/terms/"; + // ... +} +``` + +### 5. Соберите без Sentry: + +```bash +make TARGET=android SENTRY_DSN="" +``` + +--- + +**Дата аудита**: 27 декабря 2025 г. +**Версия**: Hiddify v2.5.7 +**Аудитор**: GitHub Copilot AI Assistant diff --git a/TELEGRAM_BOT_SETUP.md b/TELEGRAM_BOT_SETUP.md new file mode 100644 index 00000000..d2ce28e4 --- /dev/null +++ b/TELEGRAM_BOT_SETUP.md @@ -0,0 +1,161 @@ +# 🤖 Настройка Telegram Bot для логов Umbrix + +## 📝 Пошаговая инструкция + +### 1️⃣ Создайте бота + +1. Откройте **Telegram** +2. Найдите бота **@BotFather** +3. Отправьте команду: `/newbot` +4. Придумайте **имя** для бота (например: `Umbrix Logs Bot`) +5. Придумайте **username** (например: `umbrix_logs_bot`) +6. Скопируйте полученный **токен** (выглядит как `1234567890:ABCdefGHIjklMNOpqrsTUVwxyz`) + +### 2️⃣ Настройте приватность + +1. Отправьте @BotFather команду: `/mybots` +2. Выберите своего бота +3. Нажмите `Bot Settings` +4. Нажмите `Group Privacy` +5. Нажмите `Turn OFF` (чтобы бот мог читать сообщения в группах) + +### 3️⃣ Создайте канал/группу для логов + +**Вариант A: Приватный канал (РЕКОМЕНДУЮ)** +1. Создайте новый канал в Telegram +2. Назовите его (например, "Umbrix Logs") +3. Сделайте канал **приватным** +4. Добавьте бота в администраторы канала + +**Вариант B: Приватная группа** +1. Создайте новую группу +2. Добавьте бота в участники +3. Сделайте бота администратором + +**Вариант C: Личные сообщения** +1. Найдите своего бота в Telegram +2. Нажмите `/start` + +### 4️⃣ Получите Chat ID + +**Способ 1: Через API (для каналов/групп)** + +1. Отправьте любое сообщение в канал/группу +2. Откройте в браузере: + ``` + https://api.telegram.org/bot<ВАШ_ТОКЕН>/getUpdates + ``` + Замените `<ВАШ_ТОКЕН>` на токен от BotFather + +3. Найдите в JSON ответе: + ```json + "chat": { + "id": -1001234567890, ← ЭТО ВАШ CHAT ID + "title": "Umbrix Logs", + "type": "channel" + } + ``` + +4. Скопируйте этот ID (с минусом!) + +**Способ 2: Через бота (для личных сообщений)** + +1. Найдите бота **@userinfobot** в Telegram +2. Отправьте ему `/start` +3. Он пришлёт ваш Chat ID + +### 5️⃣ Вставьте токены в код + +Откройте файл: `lib/core/model/secrets.dart` + +```dart +abstract class Secrets { + static const String telegramBotToken = "1234567890:ABCdefGHIjklMNOpqrsTUVwxyz"; // ← ВАШ ТОКЕН + static const String telegramChatId = "-1001234567890"; // ← ВАШ CHAT ID + + static bool get isConfigured => + telegramBotToken.isNotEmpty && telegramChatId.isNotEmpty; +} +``` + +### 6️⃣ Добавьте в .gitignore + +**ОБЯЗАТЕЛЬНО!** Чтобы токен не попал в GitHub: + +Откройте файл: `.gitignore` + +Добавьте строку: +``` +lib/core/model/secrets.dart +``` + +Или создайте secrets.dart с шаблоном: +```bash +cp lib/core/model/secrets.dart lib/core/model/secrets.example.dart +``` + +В secrets.example.dart оставьте пустые строки, а secrets.dart добавьте в .gitignore. + +### 7️⃣ Проверьте что работает + +Запустите приложение и попробуйте отправить тестовый лог через настройки. + +Если логи не приходят, проверьте: +- ✅ Токен правильный (скопирован полностью) +- ✅ Chat ID правильный (с минусом для каналов) +- ✅ Бот добавлен в администраторы канала +- ✅ Group Privacy выключен у бота + +--- + +## 🔒 Безопасность + +### ❌ НЕ ДЕЛАЙТЕ ТАК: + +```dart +// ❌ НЕ хардкодьте токен напрямую! +const token = "1234567890:ABCdef..."; + +// ❌ НЕ коммитьте secrets.dart в Git! +git add lib/core/model/secrets.dart // НЕТ! +``` + +### ✅ ПРАВИЛЬНО: + +1. Храните токен в `secrets.dart` +2. Добавьте `secrets.dart` в `.gitignore` +3. Создайте `secrets.example.dart` с пустыми значениями для других разработчиков +4. В production сборке: используйте environment variables + +--- + +## 📤 Формат логов + +Бот будет отправлять сообщения в таком формате: + +``` +🐛 Отчёт об ошибке Umbrix + +📱 Версия: 2.5.7 +🤖 Android: 13 (API 33) +📦 Device: Samsung Galaxy S21 +🆔 ID: a3f5c8d1 (анонимный) + +📋 Логи: +[ERROR] Connection timeout +[WARN] Retry attempt 3/5 +[INFO] Connecting to server... +``` + +--- + +## 💡 Советы + +1. **Используйте приватный канал** - логи могут содержать технические детали +2. **Настройте уведомления** - чтобы сразу видеть новые логи +3. **Создайте отдельного бота для каждого проекта** - не смешивайте логи разных приложений +4. **Ограничьте размер логов** - отправляйте только последние 50-100 строк + +--- + +**Готово!** Теперь пользователи смогут отправлять анонимные логи, а вы - исправлять баги быстрее! 🚀 diff --git a/TELEGRAM_SETUP.md b/TELEGRAM_SETUP.md new file mode 100644 index 00000000..bdc52ae1 --- /dev/null +++ b/TELEGRAM_SETUP.md @@ -0,0 +1,187 @@ +# 📱 Настройка Telegram Бота для Логов + +## 🎯 Цель +Пользователи смогут **добровольно** отправлять логи ошибок через Telegram, и вы будете получать их мгновенно в вашу группу/канал. + +--- + +## 📋 Шаг 1: Создайте Telegram Бота + +1. Откройте Telegram и найдите **@BotFather** +2. Отправьте команду: `/newbot` +3. Введите имя бота: **Umbrix Log Bot** +4. Введите username: **@umbrix_logs_bot** (или любой свободный) +5. **СКОПИРУЙТЕ TOKEN** который выдаст BotFather (будет примерно таким): + ``` + 1234567890:ABCdefGHIjklMNOpqrsTUVwxyz1234567890 + ``` + +--- + +## 📋 Шаг 2: Создайте Приватную Группу + +1. Создайте **приватную группу** в Telegram (или канал) +2. Назовите её например: **Umbrix Logs** +3. **Добавьте бота** в эту группу: + - Нажмите на группу → Info → Add members + - Найдите вашего бота по username (@umbrix_logs_bot) + - Добавьте его + +--- + +## 📋 Шаг 3: Получите CHAT_ID + +### Вариант A: Через веб-запрос + +1. Отправьте **любое сообщение** в созданную группу +2. Откройте в браузере (замените ``): + ``` + https://api.telegram.org/bot/getUpdates + ``` + Пример: + ``` + https://api.telegram.org/bot1234567890:ABCdefGHIjkl/getUpdates + ``` + +3. Найдите в ответе: + ```json + { + "message": { + "chat": { + "id": -1001234567890, ← ЭТО ВАШ CHAT_ID + "title": "Umbrix Logs" + } + } + } + ``` + +### Вариант B: Через бота @userinfobot + +1. Добавьте **@userinfobot** в вашу группу +2. Он автоматически напишет CHAT_ID группы + +--- + +## 📋 Шаг 4: Настройка в Коде + +1. **Скопируйте файл-образец**: + ```bash + cd /home/vodorod/dorod/hiddify-original-v2.5.7 + cp lib/core/telegram_config.dart.example lib/core/telegram_config.dart + ``` + +2. **Откройте** `lib/core/telegram_config.dart` + +3. **Вставьте свои данные**: + ```dart + class TelegramConfig { + static const String botToken = '1234567890:ABCdefGHIjklMNOpqrsTUVwxyz1234567890'; + static const String chatId = '-1001234567890'; // С минусом для групп! + + // Остальное не трогайте + static const String apiUrl = 'https://api.telegram.org'; + static const int maxMessageLength = 4000; + } + ``` + +4. **ВАЖНО**: Добавьте в `.gitignore`: + ```bash + echo "lib/core/telegram_config.dart" >> .gitignore + ``` + +--- + +## 📋 Шаг 5: Проверка + +1. Соберите приложение: + ```bash + flutter build apk --debug + ``` + +2. Установите на эмулятор/телефон + +3. Зайдите в **Настройки → О приложении** + +4. Нажмите **"Отправить логи разработчику"** + +5. Проверьте вашу Telegram группу - должно прийти сообщение! 🎉 + +--- + +## 🔐 Безопасность + +### ✅ ЧТО БЕЗОПАСНО: +- Токен бота хранится в `telegram_config.dart` (не в Git) +- Группа приватная (только вы видите логи) +- Логи анонимны (нет IP, email, паролей) + +### ⚠️ ЧТО ОТПРАВЛЯЕТСЯ: +- Версия приложения +- Версия Android +- Модель устройства (Samsung, Xiaomi) +- Последние 50 строк логов +- Анонимный ID (хэш от device_id) + +### 🚫 ЧТО НЕ ОТПРАВЛЯЕТСЯ: +- IP адрес +- Email пользователя +- VPN конфигурация +- Серверы/ключи +- История сайтов +- Трафик + +--- + +## 📊 Пример Сообщения в Telegram + +``` +🐛 Отчёт об ошибке Umbrix + +📱 Версия: 1.0.0 +🤖 Android: 13 +📲 Устройство: Samsung SM-G991B +🆔 ID: a3f7c9d2 (анонимный) +🕐 Время: 2024-12-27 15:30:22 + +📋 Логи: +[ERROR] Connection failed: Timeout +[INFO] Retrying connection... +[ERROR] Failed after 3 attempts +... +``` + +--- + +## 🛠️ Альтернативы + +Если Telegram не подходит, можете использовать: + +1. **Email** (через SMTP или веб-сервис) +2. **GitHub Issues** (автоматически через API) +3. **Discord Webhook** +4. **Slack Webhook** + +Но Telegram - самый простой и удобный вариант для начала! 👍 + +--- + +## ❓ FAQ + +**Q: Бот не отвечает** +A: Проверьте что бот добавлен в группу и вы скопировали правильный токен + +**Q: Не могу найти CHAT_ID** +A: Убедитесь что отправили сообщение в группу ПОСЛЕ добавления бота + +**Q: Лимит сообщений?** +A: Telegram Bot API позволяет 30 сообщений в секунду (более чем достаточно) + +**Q: Можно ли использовать личный чат вместо группы?** +A: Да! Напишите боту /start, получите свой chat_id через getUpdates + +**Q: Безопасно ли хранить токен в коде?** +A: Да, если файл в .gitignore. Но для production лучше использовать environment variables + +--- + +**Дата создания**: 27 декабря 2025 г. diff --git a/TEST_APK_INSTRUCTIONS.md b/TEST_APK_INSTRUCTIONS.md new file mode 100644 index 00000000..8aa8deee --- /dev/null +++ b/TEST_APK_INSTRUCTIONS.md @@ -0,0 +1,116 @@ +# 📱 Инструкция по тестированию APK на реальном телефоне + +## 1️⃣ Подключи телефон к компьютеру + +```bash +# Включи на телефоне: Настройки → О телефоне → Нажми 7 раз на "Номер сборки" +# Затем: Настройки → Для разработчиков → Включи "Отладка по USB" + +# Проверь подключение: +adb devices -l +``` + +## 2️⃣ Удали старую версию (если есть) + +```bash +adb uninstall com.umbrix.app +adb uninstall com.hiddify.app.test +``` + +## 3️⃣ Установи новый APK + +```bash +# Универсальный APK (работает на всех телефонах): +adb install -r /home/vodorod/dorod/hiddify-original-v2.5.7/build/app/outputs/flutter-apk/app-debug.apk +``` + +## 4️⃣ Запусти приложение и собери логи + +```bash +# Очисти логи: +adb logcat -c + +# Запусти приложение вручную на телефоне + +# Собери логи (если крашится): +adb logcat -d > ~/umbrix_crash.log + +# Или сразу смотри ошибки: +adb logcat | grep -E "FATAL|AndroidRuntime|crash|umbrix|Umbrix" +``` + +## 5️⃣ Если появляется ошибка INSTALL_FAILED_NO_MATCHING_ABIS + +Это значит неправильная архитектура. Проверь: + +```bash +# Узнай архитектуру телефона: +adb shell getprop ro.product.cpu.abi + +# Если телефон ARM64 (arm64-v8a): +adb install -r build/app/outputs/flutter-apk/app-arm64-v8a-debug.apk + +# Если старый ARM (armeabi-v7a): +adb install -r build/app/outputs/flutter-apk/app-armeabi-v7a-debug.apk + +# Универсальный (тяжелее, но работает везде): +adb install -r build/app/outputs/flutter-apk/app-debug.apk +``` + +## 6️⃣ Частые проблемы + +### Краш при запуске: + +**Причина**: Package name изменился, но нативные библиотеки не пересобрались + +**Решение**: +```bash +cd /home/vodorod/dorod/hiddify-original-v2.5.7 +flutter clean +cd android && ./gradlew clean && cd .. +flutter pub get +flutter build apk --debug +``` + +### Ошибка "No matching ABIS": + +**Причина**: APK собран для другой архитектуры (x86 для эмулятора, ARM для телефона) + +**Решение**: Используй `app-debug.apk` (универсальный, 193 MB) + +### Приложение установилось, но не запускается: + +**Решение**: Собери логи: +```bash +adb logcat -c +# Запусти приложение на телефоне +adb logcat -d | grep -E "FATAL|Exception" > ~/crash.log +``` + +## 📍 Где лежат APK файлы: + +``` +/home/vodorod/dorod/hiddify-original-v2.5.7/build/app/outputs/flutter-apk/ +├── app-debug.apk (193 MB) ← ИСПОЛЬЗУЙ ЭТОТ (универсальный) +├── app-arm64-v8a-debug.apk (119 MB) ← Для современных телефонов +├── app-armeabi-v7a-debug.apk (113 MB) ← Для старых телефонов +└── app-x86_64-debug.apk (118 MB) ← Только для эмуляторов +``` + +## 🔍 Как понять почему крашится: + +1. Установи APK +2. Запусти приложение +3. Собери логи: `adb logcat -d > crash.log` +4. Найди строки с `FATAL EXCEPTION` или `AndroidRuntime` +5. Покажи мне эти строки + +## ✅ Что изменилось в Umbrix: + +- ✅ Package ID: `com.hiddify.app.test` → `com.umbrix.app` +- ✅ App name: "Hiddify" → "Umbrix" +- ✅ Icons: новые иконки Umbrix (зелёно-синий щит с черепахой) +- ✅ URL scheme: `hiddify://` → `umbrix://` +- ✅ Все URL изменены на umbrix.net +- ✅ Вкладка "Прокси" → "Локации" +- ✅ Privacy policy и Terms созданы diff --git a/android/app/build.gradle b/android/app/build.gradle index ebf23bbc..6d817f74 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -35,9 +35,9 @@ def flutterVersionCode = localProperties.getProperty('flutter.versionCode')?: '1 def flutterVersionName = localProperties.getProperty('flutter.versionName') ?: '1.0' android { - namespace 'com.hiddify.hiddify' - testNamespace "test.com.hiddify.hiddify" - compileSdkVersion 34 + namespace 'com.umbrix.app' + testNamespace "test.com.umbrix.app" + compileSdkVersion 35 ndkVersion "26.1.10909125" compileOptions { @@ -54,7 +54,7 @@ android { } defaultConfig { - applicationId "com.hiddify.app.test" + applicationId "com.umbrix.app" minSdkVersion 21 targetSdkVersion 34 versionCode flutterVersionCode.toInteger() diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e06a3133..b0889d9a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -12,6 +12,7 @@ android:required="false" /> + @@ -26,10 +27,8 @@ - - - - - @@ -74,9 +68,9 @@ - + - + @@ -85,18 +79,6 @@ - - - - - - + + + + + + + + + + + + + + + + + + + + - get() { - val stringValue = if (perAppProxyMode == PerAppProxyMode.INCLUDE) { - preferences.getString(SettingsKey.PER_APP_PROXY_INCLUDE_LIST, "")!! - } else { - preferences.getString(SettingsKey.PER_APP_PROXY_EXCLUDE_LIST, "")!! - } - if (!stringValue.startsWith(LIST_IDENTIFIER)) { - return emptyList() - } - return decodeListString(stringValue.substring(LIST_IDENTIFIER.length)) - } - - private fun decodeListString(listString: String): List { - val stream = ObjectInputStream(ByteArrayInputStream(Base64.decode(listString, 0))) - return stream.readObject() as List - } - - var activeConfigPath: String - get() = preferences.getString(SettingsKey.ACTIVE_CONFIG_PATH, "")!! - set(value) = preferences.edit().putString(SettingsKey.ACTIVE_CONFIG_PATH, value).apply() - - var activeProfileName: String - get() = preferences.getString(SettingsKey.ACTIVE_PROFILE_NAME, "")!! - set(value) = preferences.edit().putString(SettingsKey.ACTIVE_PROFILE_NAME, value).apply() - - var serviceMode: String - get() = preferences.getString(SettingsKey.SERVICE_MODE, ServiceMode.VPN)!! - set(value) = preferences.edit().putString(SettingsKey.SERVICE_MODE, value).apply() - - var configOptions: String - get() = preferences.getString(SettingsKey.CONFIG_OPTIONS, "")!! - set(value) = preferences.edit().putString(SettingsKey.CONFIG_OPTIONS, value).apply() - - var debugMode: Boolean - get() = preferences.getBoolean(SettingsKey.DEBUG_MODE, false) - set(value) = preferences.edit().putBoolean(SettingsKey.DEBUG_MODE, value).apply() - - var disableMemoryLimit: Boolean - get() = preferences.getBoolean(SettingsKey.DISABLE_MEMORY_LIMIT, false) - set(value) = - preferences.edit().putBoolean(SettingsKey.DISABLE_MEMORY_LIMIT, value).apply() - - var dynamicNotification: Boolean - get() = preferences.getBoolean(SettingsKey.DYNAMIC_NOTIFICATION, true) - set(value) = - preferences.edit().putBoolean(SettingsKey.DYNAMIC_NOTIFICATION, value).apply() - - var systemProxyEnabled: Boolean - get() = preferences.getBoolean(SettingsKey.SYSTEM_PROXY_ENABLED, true) - set(value) = - preferences.edit().putBoolean(SettingsKey.SYSTEM_PROXY_ENABLED, value).apply() - - var startedByUser: Boolean - get() = preferences.getBoolean(SettingsKey.STARTED_BY_USER, false) - set(value) = preferences.edit().putBoolean(SettingsKey.STARTED_BY_USER, value).apply() - - fun serviceClass(): Class<*> { - return when (serviceMode) { - ServiceMode.VPN -> VPNService::class.java - else -> ProxyService::class.java - } - } - - private var currentServiceMode : String? = null - - suspend fun rebuildServiceMode(): Boolean { - var newMode = ServiceMode.NORMAL - try { - if (serviceMode == ServiceMode.VPN) { - newMode = ServiceMode.VPN - } - } catch (_: Exception) { - } - if (currentServiceMode == newMode) { - return false - } - currentServiceMode = newMode - return true - } - - private suspend fun needVPNService(): Boolean { - val filePath = activeConfigPath - if (filePath.isBlank()) return false - val content = JSONObject(File(filePath).readText()) - val inbounds = content.getJSONArray("inbounds") - for (index in 0 until inbounds.length()) { - val inbound = inbounds.getJSONObject(index) - if (inbound.getString("type") == "tun") { - return true - } - } - return false - } -} - diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/ShortcutActivity.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/ShortcutActivity.kt deleted file mode 100644 index 07742690..00000000 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/ShortcutActivity.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.hiddify.hiddify - -import android.app.Activity -import android.content.Intent -import android.content.pm.ShortcutManager -import android.os.Build -import android.os.Bundle -import androidx.core.content.getSystemService -import androidx.core.content.pm.ShortcutInfoCompat -import androidx.core.content.pm.ShortcutManagerCompat -import androidx.core.graphics.drawable.IconCompat -import com.hiddify.hiddify.bg.BoxService -import com.hiddify.hiddify.bg.ServiceConnection -import com.hiddify.hiddify.constant.Status - -class ShortcutActivity : Activity(), ServiceConnection.Callback { - - private val connection = ServiceConnection(this, this, false) - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - if (intent.action == Intent.ACTION_CREATE_SHORTCUT) { - setResult( - RESULT_OK, ShortcutManagerCompat.createShortcutResultIntent( - this, - ShortcutInfoCompat.Builder(this, "toggle") - .setIntent( - Intent( - this, - ShortcutActivity::class.java - ).setAction(Intent.ACTION_MAIN) - ) - .setIcon( - IconCompat.createWithResource( - this, - R.mipmap.ic_launcher - ) - ) - .setShortLabel(getString(R.string.quick_toggle)) - .build() - ) - ) - finish() - } else { - connection.connect() - if (Build.VERSION.SDK_INT >= 25) { - getSystemService()?.reportShortcutUsed("toggle") - } - } - moveTaskToBack(true) - } - - override fun onServiceStatusChanged(status: Status) { - when (status) { - Status.Started -> BoxService.stop() - Status.Stopped -> BoxService.start() - else -> {} - } - finish() - } - - override fun onDestroy() { - connection.disconnect() - super.onDestroy() - } - -} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/ActiveGroupsChannel.kt b/android/app/src/main/kotlin/com/umbrix/app/ActiveGroupsChannel.kt similarity index 93% rename from android/app/src/main/kotlin/com/hiddify/hiddify/ActiveGroupsChannel.kt rename to android/app/src/main/kotlin/com/umbrix/app/ActiveGroupsChannel.kt index 34c5d547..9e7bde28 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/ActiveGroupsChannel.kt +++ b/android/app/src/main/kotlin/com/umbrix/app/ActiveGroupsChannel.kt @@ -1,9 +1,9 @@ -package com.hiddify.hiddify +package com.umbrix.app import android.util.Log import com.google.gson.Gson -import com.hiddify.hiddify.utils.CommandClient -import com.hiddify.hiddify.utils.ParsedOutboundGroup +import com.umbrix.app.utils.CommandClient +import com.umbrix.app.utils.ParsedOutboundGroup import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.EventChannel import io.nekohasekai.libbox.OutboundGroup diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/Application.kt b/android/app/src/main/kotlin/com/umbrix/app/Application.kt similarity index 90% rename from android/app/src/main/kotlin/com/hiddify/hiddify/Application.kt rename to android/app/src/main/kotlin/com/umbrix/app/Application.kt index 939c25f1..574c0aea 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/Application.kt +++ b/android/app/src/main/kotlin/com/umbrix/app/Application.kt @@ -1,4 +1,4 @@ -package com.hiddify.hiddify +package com.umbrix.app import android.app.Application import android.app.NotificationManager @@ -8,9 +8,9 @@ import android.content.IntentFilter import android.net.ConnectivityManager import android.os.PowerManager import androidx.core.content.getSystemService -import com.hiddify.hiddify.bg.AppChangeReceiver +import com.umbrix.app.bg.AppChangeReceiver import go.Seq -import com.hiddify.hiddify.Application as BoxApplication +import com.umbrix.app.Application as BoxApplication class Application : Application() { diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/EventHandler.kt b/android/app/src/main/kotlin/com/umbrix/app/EventHandler.kt similarity index 96% rename from android/app/src/main/kotlin/com/hiddify/hiddify/EventHandler.kt rename to android/app/src/main/kotlin/com/umbrix/app/EventHandler.kt index 95a58b42..5ad3a4f4 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/EventHandler.kt +++ b/android/app/src/main/kotlin/com/umbrix/app/EventHandler.kt @@ -1,9 +1,9 @@ -package com.hiddify.hiddify +package com.umbrix.app import android.util.Log import androidx.lifecycle.Observer -import com.hiddify.hiddify.constant.Alert -import com.hiddify.hiddify.constant.Status +import com.umbrix.app.constant.Alert +import com.umbrix.app.constant.Status import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.JSONMethodCodec diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/GroupsChannel.kt b/android/app/src/main/kotlin/com/umbrix/app/GroupsChannel.kt similarity index 93% rename from android/app/src/main/kotlin/com/hiddify/hiddify/GroupsChannel.kt rename to android/app/src/main/kotlin/com/umbrix/app/GroupsChannel.kt index 7f2b907b..d3c154db 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/GroupsChannel.kt +++ b/android/app/src/main/kotlin/com/umbrix/app/GroupsChannel.kt @@ -1,9 +1,9 @@ -package com.hiddify.hiddify +package com.umbrix.app import android.util.Log import com.google.gson.Gson -import com.hiddify.hiddify.utils.CommandClient -import com.hiddify.hiddify.utils.ParsedOutboundGroup +import com.umbrix.app.utils.CommandClient +import com.umbrix.app.utils.ParsedOutboundGroup import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.EventChannel import io.nekohasekai.libbox.OutboundGroup diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/LogHandler.kt b/android/app/src/main/kotlin/com/umbrix/app/LogHandler.kt similarity index 97% rename from android/app/src/main/kotlin/com/hiddify/hiddify/LogHandler.kt rename to android/app/src/main/kotlin/com/umbrix/app/LogHandler.kt index 3fc91860..0cb1cac4 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/LogHandler.kt +++ b/android/app/src/main/kotlin/com/umbrix/app/LogHandler.kt @@ -1,4 +1,4 @@ -package com.hiddify.hiddify +package com.umbrix.app import android.util.Log import io.flutter.embedding.engine.plugins.FlutterPlugin diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/MainActivity.kt b/android/app/src/main/kotlin/com/umbrix/app/MainActivity.kt similarity index 95% rename from android/app/src/main/kotlin/com/hiddify/hiddify/MainActivity.kt rename to android/app/src/main/kotlin/com/umbrix/app/MainActivity.kt index f0767315..872d7290 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/MainActivity.kt +++ b/android/app/src/main/kotlin/com/umbrix/app/MainActivity.kt @@ -1,4 +1,4 @@ -package com.hiddify.hiddify +package com.umbrix.app import android.annotation.SuppressLint import android.content.Intent @@ -11,11 +11,11 @@ import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.lifecycle.MutableLiveData import androidx.lifecycle.lifecycleScope -import com.hiddify.hiddify.bg.ServiceConnection -import com.hiddify.hiddify.bg.ServiceNotification -import com.hiddify.hiddify.constant.Alert -import com.hiddify.hiddify.constant.ServiceMode -import com.hiddify.hiddify.constant.Status +import com.umbrix.app.bg.ServiceConnection +import com.umbrix.app.bg.ServiceNotification +import com.umbrix.app.constant.Alert +import com.umbrix.app.constant.ServiceMode +import com.umbrix.app.constant.Status import io.flutter.embedding.android.FlutterFragmentActivity import io.flutter.embedding.engine.FlutterEngine import kotlinx.coroutines.Dispatchers diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/MethodHandler.kt b/android/app/src/main/kotlin/com/umbrix/app/MethodHandler.kt similarity index 98% rename from android/app/src/main/kotlin/com/hiddify/hiddify/MethodHandler.kt rename to android/app/src/main/kotlin/com/umbrix/app/MethodHandler.kt index f47c5c53..5f8e172a 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/MethodHandler.kt +++ b/android/app/src/main/kotlin/com/umbrix/app/MethodHandler.kt @@ -1,8 +1,8 @@ -package com.hiddify.hiddify +package com.umbrix.app import android.util.Log -import com.hiddify.hiddify.bg.BoxService -import com.hiddify.hiddify.constant.Status +import com.umbrix.app.bg.BoxService +import com.umbrix.app.constant.Status import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/PlatformSettingsHandler.kt b/android/app/src/main/kotlin/com/umbrix/app/PlatformSettingsHandler.kt similarity index 95% rename from android/app/src/main/kotlin/com/hiddify/hiddify/PlatformSettingsHandler.kt rename to android/app/src/main/kotlin/com/umbrix/app/PlatformSettingsHandler.kt index 9ef9bd27..ce93a241 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/PlatformSettingsHandler.kt +++ b/android/app/src/main/kotlin/com/umbrix/app/PlatformSettingsHandler.kt @@ -1,4 +1,4 @@ -package com.hiddify.hiddify +package com.umbrix.app import android.Manifest import android.annotation.SuppressLint @@ -13,7 +13,7 @@ import android.os.Build import android.util.Base64 import com.google.gson.Gson import com.google.gson.annotations.SerializedName -import com.hiddify.hiddify.Application.Companion.packageManager +import com.umbrix.app.Application.Companion.packageManager import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding @@ -141,7 +141,8 @@ class PlatformSettingsHandler : FlutterPlugin, MethodChannel.MethodCallHandler, packageManager.getInstalledPackages(flag) } val list = mutableListOf() - installedPackages.forEach { + for (it in installedPackages) { + val appInfo = it.applicationInfo ?: continue if (it.packageName != Application.application.packageName && (it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true || it.packageName == "android") @@ -149,8 +150,8 @@ class PlatformSettingsHandler : FlutterPlugin, MethodChannel.MethodCallHandler, list.add( AppItem( it.packageName, - it.applicationInfo.loadLabel(packageManager).toString(), - it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM == 1 + appInfo.loadLabel(packageManager).toString(), + appInfo.flags and ApplicationInfo.FLAG_SYSTEM == 1 ) ) } diff --git a/android/app/src/main/kotlin/com/umbrix/app/Settings.kt b/android/app/src/main/kotlin/com/umbrix/app/Settings.kt index 049747f4..627d4234 100644 --- a/android/app/src/main/kotlin/com/umbrix/app/Settings.kt +++ b/android/app/src/main/kotlin/com/umbrix/app/Settings.kt @@ -17,7 +17,7 @@ object Settings { } var perAppProxyMode: String - get() = preferences.getString(SettingsKey.PER_APP_PROXY_MODE, PerAppProxyMode.OFF)!! + get() = preferences.getString(SettingsKey.PER_APP_PROXY_MODE, PerAppProxyMode.EXCLUDE)!! set(value) = preferences.edit().putString(SettingsKey.PER_APP_PROXY_MODE, value).apply() val perAppProxyEnabled: Boolean @@ -25,14 +25,59 @@ object Settings { val perAppProxyList: List get() { - val key = if (perAppProxyMode == PerAppProxyMode.INCLUDE) { + // Принудительно перечитываем preferences на случай если данные только что изменились + val freshPrefs = Application.application.applicationContext + .getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE) + + android.util.Log.d("Settings", "=== perAppProxyList called ===") + val currentMode = freshPrefs.getString(SettingsKey.PER_APP_PROXY_MODE, PerAppProxyMode.OFF) + android.util.Log.d("Settings", "perAppProxyMode (fresh read) = $currentMode") + + val key = if (currentMode == PerAppProxyMode.INCLUDE) { SettingsKey.PER_APP_PROXY_INCLUDE_LIST } else { SettingsKey.PER_APP_PROXY_EXCLUDE_LIST } - // Flutter SharedPreferences plugin сохраняет List как StringSet - // Читаем напрямую без дополнительной сериализации - return preferences.getStringSet(key, emptySet())?.toList() ?: emptyList() + android.util.Log.d("Settings", "Using key: $key") + + // Flutter SharedPreferences сохраняет List в специальном формате: + // "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu" (base64: "This is the prefix for a list.") + JSON + val stringValue = freshPrefs.getString(key, "") ?: "" + android.util.Log.d("Settings", "Raw value: $stringValue") + + if (stringValue.isEmpty()) { + android.util.Log.d("Settings", "Empty value, returning emptyList()") + return emptyList() + } + + // Проверяем наличие префикса Flutter + val prefix = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu" + if (!stringValue.startsWith(prefix)) { + android.util.Log.e("Settings", "Missing Flutter prefix!") + return emptyList() + } + + // Убираем префикс и парсим JSON массив + val jsonPart = stringValue.substring(prefix.length) + android.util.Log.d("Settings", "JSON part: $jsonPart") + + // Flutter добавляет "!" перед JSON массивом, убираем его + val cleanJsonPart = if (jsonPart.startsWith("!")) { + jsonPart.substring(1) + } else { + jsonPart + } + android.util.Log.d("Settings", "Clean JSON: $cleanJsonPart") + + return try { + val jsonArray = JSONObject("{\"list\":$cleanJsonPart}").getJSONArray("list") + val result = List(jsonArray.length()) { jsonArray.getString(it) } + android.util.Log.d("Settings", "Parsed list: $result") + result + } catch (e: Exception) { + android.util.Log.e("Settings", "Parse error: ${e.message}", e) + emptyList() + } } var activeConfigPath: String diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/StatsChannel.kt b/android/app/src/main/kotlin/com/umbrix/app/StatsChannel.kt similarity index 96% rename from android/app/src/main/kotlin/com/hiddify/hiddify/StatsChannel.kt rename to android/app/src/main/kotlin/com/umbrix/app/StatsChannel.kt index 1ee59299..0ef353cc 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/StatsChannel.kt +++ b/android/app/src/main/kotlin/com/umbrix/app/StatsChannel.kt @@ -1,7 +1,7 @@ -package com.hiddify.hiddify +package com.umbrix.app import android.util.Log -import com.hiddify.hiddify.utils.CommandClient +import com.umbrix.app.utils.CommandClient import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.JSONMethodCodec diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/AppChangeReceiver.kt b/android/app/src/main/kotlin/com/umbrix/app/bg/AppChangeReceiver.kt similarity index 94% rename from android/app/src/main/kotlin/com/hiddify/hiddify/bg/AppChangeReceiver.kt rename to android/app/src/main/kotlin/com/umbrix/app/bg/AppChangeReceiver.kt index fd028f6c..fea04692 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/AppChangeReceiver.kt +++ b/android/app/src/main/kotlin/com/umbrix/app/bg/AppChangeReceiver.kt @@ -1,9 +1,9 @@ -package com.hiddify.hiddify.bg +package com.umbrix.app.bg import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import com.hiddify.hiddify.Settings +import com.umbrix.app.Settings class AppChangeReceiver : BroadcastReceiver() { diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/BootReceiver.kt b/android/app/src/main/kotlin/com/umbrix/app/bg/BootReceiver.kt similarity index 91% rename from android/app/src/main/kotlin/com/hiddify/hiddify/bg/BootReceiver.kt rename to android/app/src/main/kotlin/com/umbrix/app/bg/BootReceiver.kt index 028d16a9..6eecf812 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/BootReceiver.kt +++ b/android/app/src/main/kotlin/com/umbrix/app/bg/BootReceiver.kt @@ -1,9 +1,9 @@ -package com.hiddify.hiddify.bg +package com.umbrix.app.bg import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import com.hiddify.hiddify.Settings +import com.umbrix.app.Settings import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/BoxService.kt b/android/app/src/main/kotlin/com/umbrix/app/bg/BoxService.kt similarity index 85% rename from android/app/src/main/kotlin/com/hiddify/hiddify/bg/BoxService.kt rename to android/app/src/main/kotlin/com/umbrix/app/bg/BoxService.kt index b322e248..5dc12cc7 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/BoxService.kt +++ b/android/app/src/main/kotlin/com/umbrix/app/bg/BoxService.kt @@ -1,4 +1,4 @@ -package com.hiddify.hiddify.bg +package com.umbrix.app.bg import android.app.Service import android.content.BroadcastReceiver @@ -13,12 +13,12 @@ import android.util.Log import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat import androidx.lifecycle.MutableLiveData -import com.hiddify.hiddify.Application -import com.hiddify.hiddify.R -import com.hiddify.hiddify.Settings -import com.hiddify.hiddify.constant.Action -import com.hiddify.hiddify.constant.Alert -import com.hiddify.hiddify.constant.Status +import com.umbrix.app.Application +import com.umbrix.app.R +import com.umbrix.app.Settings +import com.umbrix.app.constant.Action +import com.umbrix.app.constant.Alert +import com.umbrix.app.constant.Status import go.Seq import io.nekohasekai.libbox.BoxService import io.nekohasekai.libbox.CommandServer @@ -45,6 +45,14 @@ class BoxService( private var initializeOnce = false private lateinit var workingDir: File + + @Volatile + private var currentStatus: Status = Status.Stopped + + fun isConnected(): Boolean { + return currentStatus == Status.Started + } + private fun initialize() { if (initializeOnce) return val baseDir = Application.application.filesDir @@ -112,6 +120,17 @@ class BoxService( private var boxService: BoxService? = null private var commandServer: CommandServer? = null private var receiverRegistered = false + + private fun updateStatus(newStatus: Status) { + currentStatus = newStatus + status.value = newStatus + } + + private fun postStatus(newStatus: Status) { + currentStatus = newStatus + status.postValue(newStatus) + } + private val receiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { when (intent.action) { @@ -198,12 +217,15 @@ class BoxService( newService.start() boxService = newService commandServer?.setService(boxService) - status.postValue(Status.Started) + postStatus(Status.Started) withContext(Dispatchers.Main) { notification.show(activeProfileName, R.string.status_started) } notification.start() + + // Уведомляем виджеты о изменении состояния + notifyWidgets(true) } catch (e: Exception) { stopAndAlert(Alert.StartService, e.message) return @@ -212,7 +234,7 @@ class BoxService( override fun serviceReload() { notification.close() - status.postValue(Status.Starting) + postStatus(Status.Starting) val pfd = fileDescriptor if (pfd != null) { pfd.close() @@ -257,7 +279,7 @@ class BoxService( private fun stopService() { if (status.value != Status.Started) return - status.value = Status.Stopping + updateStatus(Status.Stopping) if (receiverRegistered) { service.unregisterReceiver(receiver) receiverRegistered = false @@ -289,9 +311,12 @@ class BoxService( commandServer = null Settings.startedByUser = false withContext(Dispatchers.Main) { - status.value = Status.Stopped + updateStatus(Status.Stopped) service.stopSelf() } + + // Уведомляем виджеты о изменении состояния + notifyWidgets(false) } } override fun postServiceClose() { @@ -309,13 +334,28 @@ class BoxService( binder.broadcast { callback -> callback.onServiceAlert(type.ordinal, message) } - status.value = Status.Stopped + updateStatus(Status.Stopped) + + // Уведомляем виджеты о изменении состояния + notifyWidgets(false) + } + } + + private fun notifyWidgets(isConnected: Boolean) { + try { + val intent = Intent("com.umbrix.app.SERVICE_STATE_CHANGED").apply { + putExtra("isConnected", isConnected) + setPackage(Application.application.packageName) + } + Application.application.sendBroadcast(intent) + } catch (e: Exception) { + Log.w(TAG, "Failed to notify widgets: ${e.message}") } } fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (status.value != Status.Stopped) return Service.START_NOT_STICKY - status.value = Status.Starting + updateStatus(Status.Starting) if (!receiverRegistered) { ContextCompat.registerReceiver(service, receiver, IntentFilter().apply { diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/DefaultNetworkListener.kt b/android/app/src/main/kotlin/com/umbrix/app/bg/DefaultNetworkListener.kt similarity index 98% rename from android/app/src/main/kotlin/com/hiddify/hiddify/bg/DefaultNetworkListener.kt rename to android/app/src/main/kotlin/com/umbrix/app/bg/DefaultNetworkListener.kt index c47d1c3f..5b0975bf 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/DefaultNetworkListener.kt +++ b/android/app/src/main/kotlin/com/umbrix/app/bg/DefaultNetworkListener.kt @@ -1,4 +1,4 @@ -package com.hiddify.hiddify.bg +package com.umbrix.app.bg import android.annotation.TargetApi import android.net.ConnectivityManager @@ -8,7 +8,7 @@ import android.net.NetworkRequest import android.os.Build import android.os.Handler import android.os.Looper -import com.hiddify.hiddify.Application +import com.umbrix.app.Application import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/DefaultNetworkMonitor.kt b/android/app/src/main/kotlin/com/umbrix/app/bg/DefaultNetworkMonitor.kt similarity index 96% rename from android/app/src/main/kotlin/com/hiddify/hiddify/bg/DefaultNetworkMonitor.kt rename to android/app/src/main/kotlin/com/umbrix/app/bg/DefaultNetworkMonitor.kt index 65e385fe..e5650195 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/DefaultNetworkMonitor.kt +++ b/android/app/src/main/kotlin/com/umbrix/app/bg/DefaultNetworkMonitor.kt @@ -1,8 +1,8 @@ -package com.hiddify.hiddify.bg +package com.umbrix.app.bg import android.net.Network import android.os.Build -import com.hiddify.hiddify.Application +import com.umbrix.app.Application import io.nekohasekai.libbox.InterfaceUpdateListener import java.net.NetworkInterface diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/LocalResolver.kt b/android/app/src/main/kotlin/com/umbrix/app/bg/LocalResolver.kt similarity index 98% rename from android/app/src/main/kotlin/com/hiddify/hiddify/bg/LocalResolver.kt rename to android/app/src/main/kotlin/com/umbrix/app/bg/LocalResolver.kt index a5d95f23..f67b95c5 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/LocalResolver.kt +++ b/android/app/src/main/kotlin/com/umbrix/app/bg/LocalResolver.kt @@ -1,11 +1,11 @@ -package com.hiddify.hiddify.bg +package com.umbrix.app.bg import android.net.DnsResolver import android.os.Build import android.os.CancellationSignal import android.system.ErrnoException import androidx.annotation.RequiresApi -import com.hiddify.hiddify.ktx.tryResumeWithException +import com.umbrix.app.ktx.tryResumeWithException import io.nekohasekai.libbox.ExchangeContext import io.nekohasekai.libbox.LocalDNSTransport import kotlinx.coroutines.Dispatchers diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/PlatformInterfaceWrapper.kt b/android/app/src/main/kotlin/com/umbrix/app/bg/PlatformInterfaceWrapper.kt similarity index 98% rename from android/app/src/main/kotlin/com/hiddify/hiddify/bg/PlatformInterfaceWrapper.kt rename to android/app/src/main/kotlin/com/umbrix/app/bg/PlatformInterfaceWrapper.kt index 8c35188f..09474d0b 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/PlatformInterfaceWrapper.kt +++ b/android/app/src/main/kotlin/com/umbrix/app/bg/PlatformInterfaceWrapper.kt @@ -1,10 +1,10 @@ -package com.hiddify.hiddify.bg +package com.umbrix.app.bg import android.content.pm.PackageManager import android.os.Build import android.os.Process import androidx.annotation.RequiresApi -import com.hiddify.hiddify.Application +import com.umbrix.app.Application import io.nekohasekai.libbox.InterfaceUpdateListener import io.nekohasekai.libbox.NetworkInterfaceIterator import io.nekohasekai.libbox.PlatformInterface diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/ProxyService.kt b/android/app/src/main/kotlin/com/umbrix/app/bg/ProxyService.kt similarity index 94% rename from android/app/src/main/kotlin/com/hiddify/hiddify/bg/ProxyService.kt rename to android/app/src/main/kotlin/com/umbrix/app/bg/ProxyService.kt index 5d65029e..70975ba7 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/ProxyService.kt +++ b/android/app/src/main/kotlin/com/umbrix/app/bg/ProxyService.kt @@ -1,4 +1,4 @@ -package com.hiddify.hiddify.bg +package com.umbrix.app.bg import android.app.Service import android.content.Intent diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/ServiceBinder.kt b/android/app/src/main/kotlin/com/umbrix/app/bg/ServiceBinder.kt similarity index 91% rename from android/app/src/main/kotlin/com/hiddify/hiddify/bg/ServiceBinder.kt rename to android/app/src/main/kotlin/com/umbrix/app/bg/ServiceBinder.kt index dd4b748f..117eb9ae 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/ServiceBinder.kt +++ b/android/app/src/main/kotlin/com/umbrix/app/bg/ServiceBinder.kt @@ -1,10 +1,10 @@ -package com.hiddify.hiddify.bg +package com.umbrix.app.bg import android.os.RemoteCallbackList import androidx.lifecycle.MutableLiveData -import com.hiddify.hiddify.IService -import com.hiddify.hiddify.IServiceCallback -import com.hiddify.hiddify.constant.Status +import com.umbrix.app.IService +import com.umbrix.app.IServiceCallback +import com.umbrix.app.constant.Status import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/ServiceConnection.kt b/android/app/src/main/kotlin/com/umbrix/app/bg/ServiceConnection.kt similarity index 92% rename from android/app/src/main/kotlin/com/hiddify/hiddify/bg/ServiceConnection.kt rename to android/app/src/main/kotlin/com/umbrix/app/bg/ServiceConnection.kt index 8d215962..754c749d 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/ServiceConnection.kt +++ b/android/app/src/main/kotlin/com/umbrix/app/bg/ServiceConnection.kt @@ -1,11 +1,11 @@ -package com.hiddify.hiddify.bg +package com.umbrix.app.bg -import com.hiddify.hiddify.IService -import com.hiddify.hiddify.IServiceCallback -import com.hiddify.hiddify.Settings -import com.hiddify.hiddify.constant.Action -import com.hiddify.hiddify.constant.Alert -import com.hiddify.hiddify.constant.Status +import com.umbrix.app.IService +import com.umbrix.app.IServiceCallback +import com.umbrix.app.Settings +import com.umbrix.app.constant.Action +import com.umbrix.app.constant.Alert +import com.umbrix.app.constant.Status import android.content.ComponentName import android.content.Context import android.content.Intent diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/ServiceNotification.kt b/android/app/src/main/kotlin/com/umbrix/app/bg/ServiceNotification.kt similarity index 94% rename from android/app/src/main/kotlin/com/hiddify/hiddify/bg/ServiceNotification.kt rename to android/app/src/main/kotlin/com/umbrix/app/bg/ServiceNotification.kt index 3be3178a..4b7bf924 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/ServiceNotification.kt +++ b/android/app/src/main/kotlin/com/umbrix/app/bg/ServiceNotification.kt @@ -1,4 +1,4 @@ -package com.hiddify.hiddify.bg +package com.umbrix.app.bg import android.app.NotificationChannel import android.app.NotificationManager @@ -13,13 +13,13 @@ import androidx.annotation.StringRes import androidx.core.app.NotificationCompat import androidx.core.app.ServiceCompat import androidx.lifecycle.MutableLiveData -import com.hiddify.hiddify.Application -import com.hiddify.hiddify.MainActivity -import com.hiddify.hiddify.R -import com.hiddify.hiddify.Settings -import com.hiddify.hiddify.constant.Action -import com.hiddify.hiddify.constant.Status -import com.hiddify.hiddify.utils.CommandClient +import com.umbrix.app.Application +import com.umbrix.app.MainActivity +import com.umbrix.app.R +import com.umbrix.app.Settings +import com.umbrix.app.constant.Action +import com.umbrix.app.constant.Status +import com.umbrix.app.utils.CommandClient import io.nekohasekai.libbox.Libbox import io.nekohasekai.libbox.StatusMessage import kotlinx.coroutines.Dispatchers diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/TileService.kt b/android/app/src/main/kotlin/com/umbrix/app/bg/TileService.kt similarity index 93% rename from android/app/src/main/kotlin/com/hiddify/hiddify/bg/TileService.kt rename to android/app/src/main/kotlin/com/umbrix/app/bg/TileService.kt index d2d02383..2c01d8be 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/TileService.kt +++ b/android/app/src/main/kotlin/com/umbrix/app/bg/TileService.kt @@ -1,9 +1,9 @@ -package com.hiddify.hiddify.bg +package com.umbrix.app.bg import android.service.quicksettings.Tile import android.service.quicksettings.TileService import androidx.annotation.RequiresApi -import com.hiddify.hiddify.constant.Status +import com.umbrix.app.constant.Status @RequiresApi(24) class TileService : TileService(), ServiceConnection.Callback { diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/VPNService.kt b/android/app/src/main/kotlin/com/umbrix/app/bg/VPNService.kt similarity index 87% rename from android/app/src/main/kotlin/com/hiddify/hiddify/bg/VPNService.kt rename to android/app/src/main/kotlin/com/umbrix/app/bg/VPNService.kt index 1d48e2fb..f81c9888 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/VPNService.kt +++ b/android/app/src/main/kotlin/com/umbrix/app/bg/VPNService.kt @@ -1,15 +1,15 @@ -package com.hiddify.hiddify.bg +package com.umbrix.app.bg import android.util.Log -import com.hiddify.hiddify.Settings +import com.umbrix.app.Settings import android.content.Intent import android.content.pm.PackageManager.NameNotFoundException import android.net.ProxyInfo import android.net.VpnService import android.os.Build import android.os.IBinder -import com.hiddify.hiddify.constant.PerAppProxyMode -import com.hiddify.hiddify.ktx.toIpPrefix +import com.umbrix.app.constant.PerAppProxyMode +import com.umbrix.app.ktx.toIpPrefix import io.nekohasekai.libbox.TunOptions import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking @@ -19,10 +19,22 @@ class VPNService : VpnService(), PlatformInterfaceWrapper { companion object { private const val TAG = "A/VPNService" + + @Volatile + private var instance: VPNService? = null + + fun isRunning(): Boolean { + return instance != null + } } private val service = BoxService(this, this) + override fun onCreate() { + super.onCreate() + instance = this + } + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int) = service.onStartCommand(intent, flags, startId) @@ -35,6 +47,7 @@ class VPNService : VpnService(), PlatformInterfaceWrapper { } override fun onDestroy() { + instance = null service.onDestroy() } @@ -145,19 +158,29 @@ class VPNService : VpnService(), PlatformInterfaceWrapper { } if (Settings.perAppProxyEnabled) { + Log.d(TAG, "=== Per-App Proxy ENABLED ===") + Log.d(TAG, "Mode: ${Settings.perAppProxyMode}") val appList = Settings.perAppProxyList + Log.d(TAG, "App list: $appList") + if (Settings.perAppProxyMode == PerAppProxyMode.INCLUDE) { + Log.d(TAG, "Using INCLUDE mode") appList.forEach { + Log.d(TAG, "Including package: $it") addIncludePackage(builder,it) } + Log.d(TAG, "Including self package: $packageName") addIncludePackage(builder,packageName) } else { + Log.d(TAG, "Using EXCLUDE mode") appList.forEach { + Log.d(TAG, "Excluding package: $it") addExcludePackage(builder,it) } //addExcludePackage(builder,packageName) } } else { + Log.d(TAG, "=== Per-App Proxy DISABLED ===") val includePackage = options.includePackage if (includePackage.hasNext()) { while (includePackage.hasNext()) { diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/constant/Action.kt b/android/app/src/main/kotlin/com/umbrix/app/constant/Action.kt similarity index 84% rename from android/app/src/main/kotlin/com/hiddify/hiddify/constant/Action.kt rename to android/app/src/main/kotlin/com/umbrix/app/constant/Action.kt index 96565d02..77fd8b1b 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/constant/Action.kt +++ b/android/app/src/main/kotlin/com/umbrix/app/constant/Action.kt @@ -1,4 +1,4 @@ -package com.hiddify.hiddify.constant +package com.umbrix.app.constant object Action { const val SERVICE = "com.hiddify.app.SERVICE" diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/constant/Alert.kt b/android/app/src/main/kotlin/com/umbrix/app/constant/Alert.kt similarity index 81% rename from android/app/src/main/kotlin/com/hiddify/hiddify/constant/Alert.kt rename to android/app/src/main/kotlin/com/umbrix/app/constant/Alert.kt index afbb72a4..28add07c 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/constant/Alert.kt +++ b/android/app/src/main/kotlin/com/umbrix/app/constant/Alert.kt @@ -1,4 +1,4 @@ -package com.hiddify.hiddify.constant +package com.umbrix.app.constant enum class Alert { RequestVPNPermission, diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/constant/PerAppProxyMode.kt b/android/app/src/main/kotlin/com/umbrix/app/constant/PerAppProxyMode.kt similarity index 76% rename from android/app/src/main/kotlin/com/hiddify/hiddify/constant/PerAppProxyMode.kt rename to android/app/src/main/kotlin/com/umbrix/app/constant/PerAppProxyMode.kt index aafe920a..0d8d4481 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/constant/PerAppProxyMode.kt +++ b/android/app/src/main/kotlin/com/umbrix/app/constant/PerAppProxyMode.kt @@ -1,4 +1,4 @@ -package com.hiddify.hiddify.constant +package com.umbrix.app.constant object PerAppProxyMode { const val OFF = "off" diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/constant/ServiceMode.kt b/android/app/src/main/kotlin/com/umbrix/app/constant/ServiceMode.kt similarity index 68% rename from android/app/src/main/kotlin/com/hiddify/hiddify/constant/ServiceMode.kt rename to android/app/src/main/kotlin/com/umbrix/app/constant/ServiceMode.kt index f86de8a1..035f7b0a 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/constant/ServiceMode.kt +++ b/android/app/src/main/kotlin/com/umbrix/app/constant/ServiceMode.kt @@ -1,4 +1,4 @@ -package com.hiddify.hiddify.constant +package com.umbrix.app.constant object ServiceMode { const val NORMAL = "proxy" diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/constant/SettingsKey.kt b/android/app/src/main/kotlin/com/umbrix/app/constant/SettingsKey.kt similarity index 96% rename from android/app/src/main/kotlin/com/hiddify/hiddify/constant/SettingsKey.kt rename to android/app/src/main/kotlin/com/umbrix/app/constant/SettingsKey.kt index c331b5da..bc1f88fb 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/constant/SettingsKey.kt +++ b/android/app/src/main/kotlin/com/umbrix/app/constant/SettingsKey.kt @@ -1,4 +1,4 @@ -package com.hiddify.hiddify.constant +package com.umbrix.app.constant object SettingsKey { private const val KEY_PREFIX = "flutter." diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/constant/Status.kt b/android/app/src/main/kotlin/com/umbrix/app/constant/Status.kt similarity index 67% rename from android/app/src/main/kotlin/com/hiddify/hiddify/constant/Status.kt rename to android/app/src/main/kotlin/com/umbrix/app/constant/Status.kt index f3537cf8..194c02e9 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/constant/Status.kt +++ b/android/app/src/main/kotlin/com/umbrix/app/constant/Status.kt @@ -1,4 +1,4 @@ -package com.hiddify.hiddify.constant +package com.umbrix.app.constant enum class Status { Stopped, diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/ktx/Continuations.kt b/android/app/src/main/kotlin/com/umbrix/app/ktx/Continuations.kt similarity index 92% rename from android/app/src/main/kotlin/com/hiddify/hiddify/ktx/Continuations.kt rename to android/app/src/main/kotlin/com/umbrix/app/ktx/Continuations.kt index 244dd328..9216386e 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/ktx/Continuations.kt +++ b/android/app/src/main/kotlin/com/umbrix/app/ktx/Continuations.kt @@ -1,4 +1,4 @@ -package com.hiddify.hiddify.ktx +package com.umbrix.app.ktx import kotlin.coroutines.Continuation diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/ktx/Wrappers.kt b/android/app/src/main/kotlin/com/umbrix/app/ktx/Wrappers.kt similarity index 93% rename from android/app/src/main/kotlin/com/hiddify/hiddify/ktx/Wrappers.kt rename to android/app/src/main/kotlin/com/umbrix/app/ktx/Wrappers.kt index c74f8af1..13e24dc4 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/ktx/Wrappers.kt +++ b/android/app/src/main/kotlin/com/umbrix/app/ktx/Wrappers.kt @@ -1,4 +1,4 @@ -package com.hiddify.hiddify.ktx +package com.umbrix.app.ktx import android.net.IpPrefix import android.os.Build diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/utils/CommandClient.kt b/android/app/src/main/kotlin/com/umbrix/app/utils/CommandClient.kt similarity index 98% rename from android/app/src/main/kotlin/com/hiddify/hiddify/utils/CommandClient.kt rename to android/app/src/main/kotlin/com/umbrix/app/utils/CommandClient.kt index 852a0439..ccd00610 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/utils/CommandClient.kt +++ b/android/app/src/main/kotlin/com/umbrix/app/utils/CommandClient.kt @@ -1,4 +1,4 @@ -package com.hiddify.hiddify.utils +package com.umbrix.app.utils import go.Seq import io.nekohasekai.libbox.CommandClient @@ -9,7 +9,7 @@ import io.nekohasekai.libbox.OutboundGroup import io.nekohasekai.libbox.OutboundGroupIterator import io.nekohasekai.libbox.StatusMessage import io.nekohasekai.libbox.StringIterator -import com.hiddify.hiddify.ktx.toList +import com.umbrix.app.ktx.toList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/utils/OutboundMapper.kt b/android/app/src/main/kotlin/com/umbrix/app/utils/OutboundMapper.kt similarity index 97% rename from android/app/src/main/kotlin/com/hiddify/hiddify/utils/OutboundMapper.kt rename to android/app/src/main/kotlin/com/umbrix/app/utils/OutboundMapper.kt index 27937e83..62f69310 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/utils/OutboundMapper.kt +++ b/android/app/src/main/kotlin/com/umbrix/app/utils/OutboundMapper.kt @@ -1,4 +1,4 @@ -package com.hiddify.hiddify.utils +package com.umbrix.app.utils import com.google.gson.annotations.SerializedName import io.nekohasekai.libbox.OutboundGroup diff --git a/android/app/src/main/kotlin/com/umbrix/app/widget/ConnectionWidget1x1.kt b/android/app/src/main/kotlin/com/umbrix/app/widget/ConnectionWidget1x1.kt new file mode 100644 index 00000000..a30f1b37 --- /dev/null +++ b/android/app/src/main/kotlin/com/umbrix/app/widget/ConnectionWidget1x1.kt @@ -0,0 +1,19 @@ +package com.umbrix.app.widget + +import android.content.Context +import android.widget.RemoteViews +import com.umbrix.app.R + +class ConnectionWidget1x1 : ConnectionWidgetProvider() { + override fun getLayout(): Int = R.layout.widget_connection_1x1 + + override fun updateWidgetUI(context: Context, views: RemoteViews, isConnected: Boolean) { + if (isConnected) { + views.setImageViewResource(R.id.widget_icon, R.drawable.ic_pause_circle_24) + views.setInt(R.id.widget_background, "setBackgroundResource", R.drawable.widget_bg_active) + } else { + views.setImageViewResource(R.id.widget_icon, R.drawable.ic_power_24) + views.setInt(R.id.widget_background, "setBackgroundResource", R.drawable.widget_bg_inactive) + } + } +} diff --git a/android/app/src/main/kotlin/com/umbrix/app/widget/ConnectionWidget2x2.kt b/android/app/src/main/kotlin/com/umbrix/app/widget/ConnectionWidget2x2.kt new file mode 100644 index 00000000..c09c8fad --- /dev/null +++ b/android/app/src/main/kotlin/com/umbrix/app/widget/ConnectionWidget2x2.kt @@ -0,0 +1,27 @@ +package com.umbrix.app.widget + +import android.content.Context +import android.graphics.Color +import android.widget.RemoteViews +import com.umbrix.app.R + +class ConnectionWidget2x2 : ConnectionWidgetProvider() { + override fun getLayout(): Int = R.layout.widget_connection_2x2 + + override fun updateWidgetUI(context: Context, views: RemoteViews, isConnected: Boolean) { + if (isConnected) { + views.setImageViewResource(R.id.widget_icon, R.drawable.ic_pause_circle_24) + views.setInt(R.id.widget_background, "setBackgroundResource", R.drawable.widget_bg_active) + } else { + views.setImageViewResource(R.id.widget_icon, R.drawable.ic_power_24) + views.setInt(R.id.widget_background, "setBackgroundResource", R.drawable.widget_bg_inactive) + } + + // Обновляем текст статуса + val statusText = if (isConnected) "Connected" else "Tap to Connect" + val statusColor = if (isConnected) Color.parseColor("#00BFA5") else Color.parseColor("#9E9E9E") + + views.setTextViewText(R.id.widget_status, statusText) + views.setTextColor(R.id.widget_status, statusColor) + } +} diff --git a/android/app/src/main/kotlin/com/umbrix/app/widget/ConnectionWidgetProvider.kt b/android/app/src/main/kotlin/com/umbrix/app/widget/ConnectionWidgetProvider.kt new file mode 100644 index 00000000..9e6b5da6 --- /dev/null +++ b/android/app/src/main/kotlin/com/umbrix/app/widget/ConnectionWidgetProvider.kt @@ -0,0 +1,100 @@ +package com.umbrix.app.widget + +import android.app.PendingIntent +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.widget.RemoteViews +import com.umbrix.app.R +import com.umbrix.app.bg.BoxService + +abstract class ConnectionWidgetProvider : AppWidgetProvider() { + + companion object { + private const val ACTION_TOGGLE = "com.umbrix.app.widget.TOGGLE" + + fun updateAllWidgets(context: Context, isConnected: Boolean) { + // Обновляем все виджеты 1x1 + updateWidgets(context, ConnectionWidget1x1::class.java, isConnected) + // Обновляем все виджеты 2x2 + updateWidgets(context, ConnectionWidget2x2::class.java, isConnected) + } + + private fun updateWidgets(context: Context, widgetClass: Class, isConnected: Boolean) { + val appWidgetManager = AppWidgetManager.getInstance(context) + val componentName = ComponentName(context, widgetClass) + val appWidgetIds = appWidgetManager.getAppWidgetIds(componentName) + + appWidgetIds.forEach { appWidgetId -> + val instance = widgetClass.getDeclaredConstructor().newInstance() + instance.updateAppWidget(context, appWidgetManager, appWidgetId, isConnected) + } + } + } + + protected abstract fun getLayout(): Int + + protected open fun updateWidgetUI(context: Context, views: RemoteViews, isConnected: Boolean) { + // Переопределяется в подклассах + } + + protected fun updateAppWidget( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetId: Int, + isConnected: Boolean + ) { + val views = RemoteViews(context.packageName, getLayout()) + + // Обновляем UI + updateWidgetUI(context, views, isConnected) + + // Настраиваем клик на весь виджет + val toggleIntent = Intent(context, this::class.java).apply { + action = ACTION_TOGGLE + putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) + } + val togglePendingIntent = PendingIntent.getBroadcast( + context, + appWidgetId, + toggleIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + // Устанавливаем клик на фон виджета + views.setOnClickPendingIntent(R.id.widget_background, togglePendingIntent) + + appWidgetManager.updateAppWidget(appWidgetId, views) + } + + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray + ) { + val isConnected = com.umbrix.app.bg.BoxService.isConnected() + appWidgetIds.forEach { appWidgetId -> + updateAppWidget(context, appWidgetManager, appWidgetId, isConnected) + } + } + + override fun onReceive(context: Context, intent: Intent) { + super.onReceive(context, intent) + + when (intent.action) { + ACTION_TOGGLE -> { + if (BoxService.isConnected()) { + BoxService.stop() + } else { + BoxService.start() + } + } + "com.umbrix.app.SERVICE_STATE_CHANGED" -> { + val isConnected = intent.getBooleanExtra("isConnected", false) + updateAllWidgets(context, isConnected) + } + } + } +} diff --git a/android/app/src/main/res/drawable-hdpi/ic_stat_logo.png b/android/app/src/main/res/drawable-hdpi/ic_stat_logo.png index 75cbdc7d..c84865ce 100644 Binary files a/android/app/src/main/res/drawable-hdpi/ic_stat_logo.png and b/android/app/src/main/res/drawable-hdpi/ic_stat_logo.png differ diff --git a/android/app/src/main/res/drawable-mdpi/ic_stat_logo.png b/android/app/src/main/res/drawable-mdpi/ic_stat_logo.png index efe228a1..ff99e320 100644 Binary files a/android/app/src/main/res/drawable-mdpi/ic_stat_logo.png and b/android/app/src/main/res/drawable-mdpi/ic_stat_logo.png differ diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml index 3cc4948a..07a53911 100644 --- a/android/app/src/main/res/drawable-v21/launch_background.xml +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -3,7 +3,5 @@ - - - + diff --git a/android/app/src/main/res/drawable-xhdpi/ic_stat_logo.png b/android/app/src/main/res/drawable-xhdpi/ic_stat_logo.png new file mode 100644 index 00000000..ddba2e14 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_stat_logo.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_stat_logo.png b/android/app/src/main/res/drawable-xxhdpi/ic_stat_logo.png new file mode 100644 index 00000000..727068e7 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_stat_logo.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/android12splash.png b/android/app/src/main/res/drawable-xxxhdpi/android12splash.png new file mode 100644 index 00000000..a139c199 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_stat_logo.png b/android/app/src/main/res/drawable-xxxhdpi/ic_stat_logo.png new file mode 100644 index 00000000..039e94b4 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_stat_logo.png differ diff --git a/android/app/src/main/res/drawable/ic_launcher_foreground.xml b/android/app/src/main/res/drawable/ic_launcher_foreground.xml index 50314d13..350a7218 100644 --- a/android/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/android/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,29 +1,14 @@ - - - - - - - - + android:viewportWidth="108" + android:viewportHeight="108"> + + diff --git a/android/app/src/main/res/drawable/ic_pause_circle_24.xml b/android/app/src/main/res/drawable/ic_pause_circle_24.xml new file mode 100644 index 00000000..5ee26734 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_pause_circle_24.xml @@ -0,0 +1,5 @@ + + + + diff --git a/android/app/src/main/res/drawable/ic_play_widget.xml b/android/app/src/main/res/drawable/ic_play_widget.xml new file mode 100644 index 00000000..5e6ee265 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_play_widget.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_power_24.xml b/android/app/src/main/res/drawable/ic_power_24.xml new file mode 100644 index 00000000..caf8f5df --- /dev/null +++ b/android/app/src/main/res/drawable/ic_power_24.xml @@ -0,0 +1,5 @@ + + + + diff --git a/android/app/src/main/res/drawable/ic_power_widget.xml b/android/app/src/main/res/drawable/ic_power_widget.xml new file mode 100644 index 00000000..8ee28ddc --- /dev/null +++ b/android/app/src/main/res/drawable/ic_power_widget.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_stop_widget.xml b/android/app/src/main/res/drawable/ic_stop_widget.xml new file mode 100644 index 00000000..911abcfa --- /dev/null +++ b/android/app/src/main/res/drawable/ic_stop_widget.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml index 3cc4948a..07a53911 100644 --- a/android/app/src/main/res/drawable/launch_background.xml +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -3,7 +3,5 @@ - - - + diff --git a/android/app/src/main/res/drawable/widget_bg_active.xml b/android/app/src/main/res/drawable/widget_bg_active.xml new file mode 100644 index 00000000..96b0fb84 --- /dev/null +++ b/android/app/src/main/res/drawable/widget_bg_active.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/widget_bg_inactive.xml b/android/app/src/main/res/drawable/widget_bg_inactive.xml new file mode 100644 index 00000000..d6b15145 --- /dev/null +++ b/android/app/src/main/res/drawable/widget_bg_inactive.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/widget_preview_1x1.xml b/android/app/src/main/res/drawable/widget_preview_1x1.xml new file mode 100644 index 00000000..333ae9c1 --- /dev/null +++ b/android/app/src/main/res/drawable/widget_preview_1x1.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/widget_preview_2x2.xml b/android/app/src/main/res/drawable/widget_preview_2x2.xml new file mode 100644 index 00000000..d657b8c3 --- /dev/null +++ b/android/app/src/main/res/drawable/widget_preview_2x2.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/widget_connection_1x1.xml b/android/app/src/main/res/layout/widget_connection_1x1.xml new file mode 100644 index 00000000..b229ce2a --- /dev/null +++ b/android/app/src/main/res/layout/widget_connection_1x1.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/android/app/src/main/res/layout/widget_connection_2x2.xml b/android/app/src/main/res/layout/widget_connection_2x2.xml new file mode 100644 index 00000000..92b8d43c --- /dev/null +++ b/android/app/src/main/res/layout/widget_connection_2x2.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_banner.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_banner.xml deleted file mode 100644 index a0a0dece..00000000 --- a/android/app/src/main/res/mipmap-anydpi-v26/ic_banner.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index 7353dbd1..00000000 --- a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index 7353dbd1..00000000 --- a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..0427c95c Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index 753e2691..00000000 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index adc3d493..00000000 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ diff --git a/android/app/src/main/res/mipmap-ldpi/ic_launcher.png b/android/app/src/main/res/mipmap-ldpi/ic_launcher.png new file mode 100644 index 00000000..0427c95c Binary files /dev/null and b/android/app/src/main/res/mipmap-ldpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..0427c95c Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index b30ec90c..00000000 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp deleted file mode 100644 index 3a4ac086..00000000 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_banner.png b/android/app/src/main/res/mipmap-xhdpi/ic_banner.png deleted file mode 100644 index f97689ac..00000000 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_banner.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..0427c95c Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index ebb8bb0a..00000000 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index 018dbbe2..00000000 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..0427c95c Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index bf57ad31..00000000 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9dce965d..00000000 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..0427c95c Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index 903d2b4d..00000000 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp deleted file mode 100644 index 18346956..00000000 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/android/app/src/main/res/values/ic_launcher_background.xml b/android/app/src/main/res/values/ic_launcher_background.xml index a766f4c7..9e744084 100644 --- a/android/app/src/main/res/values/ic_launcher_background.xml +++ b/android/app/src/main/res/values/ic_launcher_background.xml @@ -1,4 +1,4 @@ - #F0F3FA + #2D3748 \ No newline at end of file diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 608ae199..5ac31d64 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -1,7 +1,9 @@ + Umbrix Stop - Toggle Service starting… Service started + Connection Button + Connection Widget \ No newline at end of file diff --git a/android/app/src/main/res/xml/shortcuts.xml b/android/app/src/main/res/xml/shortcuts.xml index 668ed275..dfb7d8fa 100644 --- a/android/app/src/main/res/xml/shortcuts.xml +++ b/android/app/src/main/res/xml/shortcuts.xml @@ -1,13 +1,3 @@ - - - \ No newline at end of file diff --git a/android/app/src/main/res/xml/widget_info_1x1.xml b/android/app/src/main/res/xml/widget_info_1x1.xml new file mode 100644 index 00000000..117311ad --- /dev/null +++ b/android/app/src/main/res/xml/widget_info_1x1.xml @@ -0,0 +1,13 @@ + + + diff --git a/android/app/src/main/res/xml/widget_info_2x2.xml b/android/app/src/main/res/xml/widget_info_2x2.xml new file mode 100644 index 00000000..13e076fa --- /dev/null +++ b/android/app/src/main/res/xml/widget_info_2x2.xml @@ -0,0 +1,15 @@ + + + diff --git a/appcast.xml b/appcast.xml index 130c64ad..7057b1de 100644 --- a/appcast.xml +++ b/appcast.xml @@ -1,34 +1,7 @@ - Release - - Version 0.13.6 - Sun, 7 Jan 2024 22:00:00 +0000 - - - - Version 0.13.6 - Sun, 7 Jan 2024 22:00:00 +0000 - - - - Version 0.13.6 - Sun, 7 Jan 2024 22:00:00 +0000 - - - - Version 0.13.6 - Sun, 7 Jan 2024 22:00:00 +0000 - - + Umbrix Updates + - \ No newline at end of file + diff --git a/assets/images/logo.png b/assets/images/logo.png new file mode 100644 index 00000000..727068e7 Binary files /dev/null and b/assets/images/logo.png differ diff --git a/assets/images/logo_splash.png b/assets/images/logo_splash.png new file mode 100644 index 00000000..ddba2e14 Binary files /dev/null and b/assets/images/logo_splash.png differ diff --git a/assets/images/umbrix_logo.png b/assets/images/umbrix_logo.png index 039e94b4..727068e7 100644 Binary files a/assets/images/umbrix_logo.png and b/assets/images/umbrix_logo.png differ diff --git a/assets/translations/strings_ar.i18n.json b/assets/translations/strings_ar.i18n.json index 49bfbb2b..d4b477a6 100644 --- a/assets/translations/strings_ar.i18n.json +++ b/assets/translations/strings_ar.i18n.json @@ -26,7 +26,9 @@ }, "intro": { "termsAndPolicyCaution(rich)": "بمواصلة استخدامك، فإنك توافق على ${tap(@:about.termsAndConditions)}", - "start": "ابدأ" + "start": "ابدأ", + "welcomeTitle": "مرحبا بك في Umbrix", + "subtitle": "بسيط. سريع. موثوق." }, "home": { "pageTitle": "الصفحة الرئيسية", @@ -127,12 +129,14 @@ } }, "proxies": { - "pageTitle": "الخوادم الوكيلية", + "pageTitle": "المواقع", "emptyProxiesMsg": "لا توجد خوادم وكيلية متاحة", - "delayTestTooltip": "اختبار التأخير", + "delayTestTooltip": "المواقع", "sortTooltip": "فرز الخوادم الوكيلية", "checkIp": "تحقق من عنوان IP", "unknownIp": "عنوان IP غير معروف", + "globalAuto": "تلقائي", + "globalAutoDesc": "اختيار تلقائي من جميع المواقع", "sortOptions": { "unsorted": "افتراضي", "name": "أبجديًا", @@ -186,8 +190,7 @@ "themeModes": { "system": "اتباع سمة النظام", "dark": "الوضع الداكن", - "light": "الوضع الفاتح", - "black": "الوضع الأسود" + "light": "الوضع الفاتح" }, "enableAnalytics": "تمكين التحليلات", "enableAnalyticsMsg": "منح الإذن بجمع التحليلات وإرسال تقارير الأعطال لتحسين التطبيق", @@ -220,7 +223,26 @@ }, "showSystemApps": "عرض تطبيقات النظام", "hideSystemApps": "إخفاء تطبيقات النظام", - "clearSelection": "مسح الاختيار" + "clearSelection": "مسح الاختيار", + "excludedDomains": { + "pageTitle": "الاستثناءات", + "domainsTab": "النطاقات", + "appsTab": "التطبيقات", + "addButton": "إضافة نطاقات أو مناطق", + "addModalTitle": "+ إضافة نطاقات", + "addOwnDomain": "إضافة للاستثناء", + "domainInputHint": "site.com أو .com", + "domainInputDescription": "أو منطقة نطاق كاملة", + "selectReadyZones": "أو اختر مناطق جاهزة:", + "cancel": "إلغاء", + "ok": "موافق", + "helpTitle": "النطاقات المستثناة", + "helpDescription": "النطاقات ومناطق النطاق من هذه القائمة ستتجاوز VPN وتستخدم اتصالاً مباشراً.", + "helpButton": "فهمت", + "emptyState": "لا توجد نطاقات مستثناة", + "emptyStateDescription": "أضف نطاقات يجب أن تتجاوز VPN", + "fabButton": "إضافة" + } }, "geoAssets": { "pageTitle": "أصول التوجيه", @@ -310,9 +332,9 @@ } }, "play": { - "title": "Hiddify (معاينة)", + "title": "Umbrix (معاينة)", "short_description": "Auto, SSH, VLESS, VMess, Trojan, Reality, Sing-Box, Clash, XRay, Shadowsocks", - "full_description": "الهدف الرئيسي لـ Hiddify هو توفير عميل نفق آمن وسهل الاستخدام وكفاءة. يمكّنك من توجيه جميع حركة المرور أو حركة المرور من التطبيق المحدد إلى خادم بعيد من اختيارك، باستخدام إذن VPN-Service. \n\nملاحظة: لا نوفر أي خادم، ويتعين على المستخدمين ضمان بقاء أنشطتهم عبر الإنترنت خاصة باستخدام خادمهم المخصص أو الخوادم الموثوقة. \n \nندعم الخوادم مع:\n- رابط اشتراك V2Ray/XRay عادي \n- رابط اشتراك Clash \n- رابط اشتراك Sing-Box \n\nما هي ميزاتنا الفريدة؟\n - سهل الاستخدام \n - مُحسّن وسريع \n - اختيار أدنى Ping تلقائيًا \n - عرض معلومات استخدام المستخدم \n - استيراد sublink بسهولة بنقرة واحدة باستخدام deeplinking \n - مجاني وخالي من الإعلانات \n - تبديل sublinks بسهولة \n - المزيد والمزيد \n\nالدعم:\n- جميع البروتوكولات التي تدعمها Sing-Box \n- VLESS + XTLS Reality, Vision \n- VMess \n- Trojan \n- ShoadowSocks \n- Reality \n- WireGuard \n- V2Ray \n- Hysteria2 \n- TUICv5 \n- SSH \n- ShadowTLS \n\n\nرمز المصدر موجود في https://github.com/hiddify/Hiddify-Next \nتعتمد نواة التطبيق على Sing-Box مفتوحة المصدر.\n\nوصف الإذن:\n- VPN Service: نظرًا لأن هدف هذا التطبيق هو توفير عميل نفق آمن وسهل الاستخدام وكفاءة، نحتاج إلى هذا الإذن لنتمكن من توجيه حركة المرور عبر النفق إلى الخادم البعيد. \n- QUERY ALL PACKAGES: يستخدم هذا الإذن للسماح للمستخدمين بتضمين أو استبعاد تطبيقات محددة للأنفاق. \n- RECEIVE BOOT COMPLETED: يمكن تمكين أو تعطيل هذا الإذن من إعدادات التطبيق لتنشيط هذا التطبيق عند تشغيل الجهاز. \n- POST NOTIFICATIONS: هذا الإذن ضروري لأننا نستخدم خدمة المقدمة لضمان تشغيل خدمة VPN بشكل مستمر. \n- هذا التطبيق خالي من الإعلانات. يتم جمع التحليلات وبيانات الأعطال فقط بموافقة صريحة من المستخدم في أول استخدام للتطبيق." + "full_description": "الهدف الرئيسي لـ Umbrix هو توفير عميل نفق آمن وسهل الاستخدام وكفاءة. يمكّنك من توجيه جميع حركة المرور أو حركة المرور من التطبيق المحدد إلى خادم بعيد من اختيارك، باستخدام إذن VPN-Service. \n\nملاحظة: لا نوفر أي خادم، ويتعين على المستخدمين ضمان بقاء أنشطتهم عبر الإنترنت خاصة باستخدام خادمهم المخصص أو الخوادم الموثوقة. \n \nندعم الخوادم مع:\n- رابط اشتراك V2Ray/XRay عادي \n- رابط اشتراك Clash \n- رابط اشتراك Sing-Box \n\nما هي ميزاتنا الفريدة؟\n - سهل الاستخدام \n - مُحسّن وسريع \n - اختيار أدنى Ping تلقائيًا \n - عرض معلومات استخدام المستخدم \n - استيراد sublink بسهولة بنقرة واحدة باستخدام deeplinking \n - مجاني وخالي من الإعلانات \n - تبديل sublinks بسهولة \n - المزيد والمزيد \n\nالدعم:\n- جميع البروتوكولات التي تدعمها Sing-Box \n- VLESS + XTLS Reality, Vision \n- VMess \n- Trojan \n- ShoadowSocks \n- Reality \n- WireGuard \n- V2Ray \n- Hysteria2 \n- TUICv5 \n- SSH \n- ShadowTLS \n\n\nرمز المصدر موجود في https://github.com/hiddify/Hiddify-Next \nتعتمد نواة التطبيق على Sing-Box مفتوحة المصدر.\n\nوصف الإذن:\n- VPN Service: نظرًا لأن هدف هذا التطبيق هو توفير عميل نفق آمن وسهل الاستخدام وكفاءة، نحتاج إلى هذا الإذن لنتمكن من توجيه حركة المرور عبر النفق إلى الخادم البعيد. \n- QUERY ALL PACKAGES: يستخدم هذا الإذن للسماح للمستخدمين بتضمين أو استبعاد تطبيقات محددة للأنفاق. \n- RECEIVE BOOT COMPLETED: يمكن تمكين أو تعطيل هذا الإذن من إعدادات التطبيق لتنشيط هذا التطبيق عند تشغيل الجهاز. \n- POST NOTIFICATIONS: هذا الإذن ضروري لأننا نستخدم خدمة المقدمة لضمان تشغيل خدمة VPN بشكل مستمر. \n- هذا التطبيق خالي من الإعلانات. يتم جمع التحليلات وبيانات الأعطال فقط بموافقة صريحة من المستخدم في أول استخدام للتطبيق." }, "connection": { "tapToConnect": "انقر للاتصال", diff --git a/assets/translations/strings_ckb-KUR.i18n.json b/assets/translations/strings_ckb-KUR.i18n.json index afd051ab..08937ce9 100644 --- a/assets/translations/strings_ckb-KUR.i18n.json +++ b/assets/translations/strings_ckb-KUR.i18n.json @@ -25,8 +25,10 @@ "grantPermission": "مۆڵەتدان" }, "intro": { - "termsAndPolicyCaution(rich)": "بە بەردەوام بوون تۆ ڕازیت لەگەڵ ${tap(@:about.termsAndConditions)}", - "start": "دەستپێک" + "termsAndPolicyCaution(rich)": "بە بەردەوام بوون تۆ ڕازی دەبیت بە ${tap(@:about.termsAndConditions)}", + "start": "دەستپێکردن", + "welcomeTitle": "بەخێربێیت بۆ Umbrix", + "subtitle": "سادە. خێرا. متمانەپێکراو." }, "home": { "pageTitle": "سەرەتا", @@ -129,10 +131,12 @@ "proxies": { "pageTitle": "پرۆکسیەکان", "emptyProxiesMsg": "هیچ پرۆکسییەک بەردەست نییە", - "delayTestTooltip": "تاقیکردنەوەی دواکەوتن", + "delayTestTooltip": "شوێنەکان", "sortTooltip": "ڕیزکردنی پرۆکسیەکان", "checkIp": "IP پشکنینی", "unknownIp": "ئایپی نەناسراو", + "globalAuto": "خۆکار", + "globalAutoDesc": "هەڵبژاردنی خۆکار لە هەموو شوێنەکان", "sortOptions": { "unsorted": "بنەڕەتی", "name": "بە پێی ئەلفوبێ", @@ -186,8 +190,7 @@ "themeModes": { "system": "خۆگونجاندن لەگەڵ تێمی سیستەم", "dark": "تاریک", - "light": "ڕووناک", - "black": "ڕەش" + "light": "ڕووناک" }, "enableAnalytics": "چالاک کردنی شیکاری", "enableAnalyticsMsg": "مۆڵەت بدە بە کۆکردنەوەی شیکاری و ناردنی ڕاپۆرتی تێکچوون بۆ باشترکردنی ئەپەکە", diff --git a/assets/translations/strings_en.i18n.json b/assets/translations/strings_en.i18n.json index eead16e3..c508c764 100644 --- a/assets/translations/strings_en.i18n.json +++ b/assets/translations/strings_en.i18n.json @@ -1,6 +1,6 @@ { "general": { - "appTitle": "Hiddify", + "appTitle": "Umbrix", "reset": "Reset", "toggle": { "enabled": "Enabled", @@ -26,7 +26,9 @@ }, "intro": { "termsAndPolicyCaution(rich)": "By Continuing You Agree With ${tap(@:about.termsAndConditions)}", - "start": "Start" + "start": "Start", + "welcomeTitle": "Welcome to Umbrix", + "subtitle": "Simple. Fast. Reliable." }, "home": { "pageTitle": "Home", @@ -127,12 +129,14 @@ } }, "proxies": { - "pageTitle": "Proxies", + "pageTitle": "Locations", "emptyProxiesMsg": "No Proxies Available", - "delayTestTooltip": "Test Delay", + "delayTestTooltip": "Locations", "sortTooltip": "Sort Proxies", "checkIp": "Check IP", "unknownIp": "Unknown IP", + "globalAuto": "Auto", + "globalAutoDesc": "Automatic selection from all locations", "sortOptions": { "unsorted": "Default", "name": "Alphabetically", @@ -187,8 +191,7 @@ "themeModes": { "system": "Follow System Theme", "dark": "Dark Mode", - "light": "Light Mode", - "black": "Black Mode" + "light": "Light Mode" }, "enableAnalytics": "Enable Analytics", "enableAnalyticsMsg": "Give permission to collect analytics and send crash reports to improve the app", @@ -219,11 +222,11 @@ "perAppProxyPageTitle": "Per-App Proxy", "perAppProxyModes": { "off": "All", - "offMsg": "Proxy All Apps", - "include": "Include", - "includeMsg": "Proxy Only Selected Apps", + "offMsg": "All apps will use VPN", + "include": "Proxy", + "includeMsg": "Selected apps will use VPN", "exclude": "Exclude", - "excludeMsg": "Do Not Proxy Selected Apps" + "excludeMsg": "Selected apps will NOT use VPN" }, "showSystemApps": "Show System Apps", "hideSystemApps": "Hide System Apps", @@ -234,8 +237,9 @@ "appsTab": "Applications", "addButton": "Add Domains or Zones", "addModalTitle": "+ Add Domains", - "addOwnDomain": "Add your site:", - "domainInputHint": "site.com", + "addOwnDomain": "Add to exclusion", + "domainInputHint": "site.com or .com", + "domainInputDescription": "Or entire domain zone", "selectReadyZones": "Or select ready-made zones:", "cancel": "Cancel", "ok": "OK", @@ -268,7 +272,43 @@ "telegramChannel": "Telegram Channel", "checkForUpdate": "Check For Update", "privacyPolicy": "Privacy Policy", - "termsAndConditions": "Terms and Conditions" + "termsAndConditions": "Terms and Conditions", + "licenses": "Licenses", + "openLicenses": "Open Source Licenses" + }, + "privacyPolicy": { + "lastUpdated": "Last updated: December 27, 2025", + "section1Title": "General Provisions", + "section1Content": "Umbrix is a private proxy client created to protect your privacy. We do not collect, store, or share your personal data with third parties.", + "section2Title": "What Data We DO NOT Collect", + "section2Content": "❌ IP addresses\n❌ Browsing history\n❌ Traffic content\n❌ Personal identifiers (IMEI, MAC address, etc.)\n❌ Payment information", + "section3Title": "Analytics (Voluntary)", + "section3Content": "The app may collect anonymous analytics only if you explicitly enable this feature in settings:\n• Application crash information (for bug fixes)\n• General device information (OS version only)\n• This data is used exclusively to improve the application\n\nYou can disable analytics at any time in settings.", + "section4Title": "Logs and Debugging", + "section4Content": "Application logs are stored only locally on your device. You can voluntarily send logs to the developer through a secure channel to help solve problems.", + "section5Title": "Your Rights", + "section5Content": "• Right to complete anonymity\n• Right to know what data is processed (none)\n• Right to delete the application without traces", + "section6Title": "Policy Changes", + "section6Content": "You will be notified of policy changes when updating the application.", + "section7Title": "Contacts", + "section7Content": "Privacy questions: support@umbrix.app" + }, + "termsAndConditions": { + "lastUpdated": "Last updated: December 27, 2025", + "section1Title": "Acceptance of Terms", + "section1Content": "By using Umbrix, you agree to these terms.", + "section2Title": "Service Description", + "section2Content": "Umbrix is a proxy client for secure connection to proxy servers.", + "section3Title": "Privacy", + "section3Content": "🔒 Umbrix does not collect or store your data\n🔒 We do not track your activity\n🔒 Analytics is disabled by default (opt-in only)\n🔒 Logs are stored locally on your device only", + "section4Title": "Responsible Use", + "section4Content": "✅ Use to protect privacy\n✅ Comply with your country's laws\n❌ Do not use for illegal activities", + "section5Title": "Disclaimer", + "section5Content": "The application is provided \"as is\" without any warranties. You are fully responsible for using the application.", + "section6Title": "Limitation of Liability", + "section6Content": "Developers are not responsible for:\n• Data loss\n• Disruption of other services\n• Actions of third parties", + "section7Title": "Changes to Terms", + "section7Content": "We reserve the right to change these terms. Continued use after changes means acceptance of new terms." }, "appUpdate": { "notAvailableMsg": "Already Using The Latest Version", @@ -335,9 +375,9 @@ } }, "play": { - "title": "Hiddify (Preview)", + "title": "Umbrix (Preview)", "short_description": "Auto, SSH, VLESS, VMess, Trojan, Reality, Sing-Box, Clash, XRay, Shadowsocks", - "full_description": "The key goal of Hiddify is to provide a secure, user-friendly and efficient tunneling client. It enables you to route all traffic or selected app traffic to a remote server of your choose, utilizing VPN-Service permission.\n\nNote: We do not provide any server; users are required to ensure their online activities stay private by using use their own self-hosted server or trusted servers. \n \nWe Support Servers With:\n- Normal V2Ray/XRay Subscription Link\n- Clash Subscription Link\n- Sing-Box Subscription Link\n\nWhat is our unique features?\n - User Friendly\n - Optimized and Fast\n - Automatically select LowestPing \n - Show user usage information\n - Easily import sublink by one click using deeplinking \n - Free and No ADS\n - Easily switch user sublinks\n - More and more\n\nSupport:\n- All Protocols Supported by Sing-Box \n- VLESS + XTLS Reality, Vision\n- VMess\n- Trojan\n- ShadowSocks\n- Reality\n- WireGuard\n- V2Ray\n- Hysteria2\n- TUICv5\n- SSH\n- ShadowTLS\n\n\nThe source code exist in https://github.com/hiddify/Hiddify-Next\nThe application core is based on open-source Sing-Box.\n\nPermission Description:\n- VPN Service: As the goal of this application is to provide a secure, user-friendly and efficient tunneling client, we need this permission to be able to route the traffic via tunnel to the remote server. \n- QUERY ALL PACKAGES: This permission is used to allow users to include or exclude specific applications for tunneling.\n- RECEIVE BOOT COMPLETED: This permission can be enabled or disabled from app settings to activate this application upon device boot.\n- POST NOTIFICATIONS: This permission is essential as we employ a foreground service to ensure the continuous operation of the VPN service.\n- This application is free from advertisements. The analytics and crash data only occurs with the explicit consent of the user in the first use of application." + "full_description": "The key goal of Umbrix is to provide a secure, user-friendly and efficient tunneling client. It enables you to route all traffic or selected app traffic to a remote server of your choose, utilizing VPN-Service permission.\n\nNote: We do not provide any server; users are required to ensure their online activities stay private by using use their own self-hosted server or trusted servers. \n \nWe Support Servers With:\n- Normal V2Ray/XRay Subscription Link\n- Clash Subscription Link\n- Sing-Box Subscription Link\n\nWhat is our unique features?\n - User Friendly\n - Optimized and Fast\n - Automatically select LowestPing \n - Show user usage information\n - Easily import sublink by one click using deeplinking \n - Free and No ADS\n - Easily switch user sublinks\n - More and more\n\nSupport:\n- All Protocols Supported by Sing-Box \n- VLESS + XTLS Reality, Vision\n- VMess\n- Trojan\n- ShadowSocks\n- Reality\n- WireGuard\n- V2Ray\n- Hysteria2\n- TUICv5\n- SSH\n- ShadowTLS\n\n\nThe source code exist in https://github.com/hiddify/Hiddify-Next\nThe application core is based on open-source Sing-Box.\n\nPermission Description:\n- VPN Service: As the goal of this application is to provide a secure, user-friendly and efficient tunneling client, we need this permission to be able to route the traffic via tunnel to the remote server. \n- QUERY ALL PACKAGES: This permission is used to allow users to include or exclude specific applications for tunneling.\n- RECEIVE BOOT COMPLETED: This permission can be enabled or disabled from app settings to activate this application upon device boot.\n- POST NOTIFICATIONS: This permission is essential as we employ a foreground service to ensure the continuous operation of the VPN service.\n- This application is free from advertisements. The analytics and crash data only occurs with the explicit consent of the user in the first use of application." }, "connection": { "tapToConnect": "Tap To Connect", @@ -444,7 +484,18 @@ "warpNoise": "Noise Count", "warpNoiseSize": "Noise Size", "warpNoiseMode": "Noise Mode", - "warpNoiseDelay": "Noise Delay" + "warpNoiseDelay": "Noise Delay", + "bypassLanWarning": { + "title": "Warning", + "subtitle": "For trusted networks only (home/office)", + "message": "Enable local network bypass only in trusted networks (home or office).\n\n❌ DO NOT enable in:\n • Public WiFi (cafes, airports)\n • Hotels\n • Unknown networks\n\nIn public networks this can be unsafe.\n\nConnection will NOT be interrupted - changes will apply to new connections.", + "cancel": "Cancel", + "enable": "Enable" + }, + "blockAdsWarning": { + "subtitle": "May cause issues on some websites", + "message": "Ad blocking may cause problems on some websites:\n\n• Websites may not load completely\n• Some features may not work\n• Login issues\n\nIf you encounter problems, try disabling this option.\n\nConnection will NOT be interrupted - changes will apply to new connections." + } }, "window": { "hide": "Hide", diff --git a/assets/translations/strings_es.i18n.json b/assets/translations/strings_es.i18n.json index c16330e2..28a9a1d2 100644 --- a/assets/translations/strings_es.i18n.json +++ b/assets/translations/strings_es.i18n.json @@ -1,6 +1,6 @@ { "general": { - "appTitle": "Hiddify", + "appTitle": "Umbrix", "reset": "Renicio", "toggle": { "enabled": "Activado", @@ -25,8 +25,10 @@ "grantPermission": "Conceder permiso" }, "intro": { - "termsAndPolicyCaution(rich)": "al continuar, aceptas ${tap(@:about.termsAndConditions)}", - "start": "Comenzar" + "termsAndPolicyCaution(rich)": "Al continuar, aceptas ${tap(@:about.termsAndConditions)}", + "start": "Empezar", + "welcomeTitle": "Bienvenido a Umbrix", + "subtitle": "Simple. Rápido. Confiable." }, "home": { "pageTitle": "Hogar", @@ -127,12 +129,14 @@ } }, "proxies": { - "pageTitle": "Proxies", + "pageTitle": "Ubicaciones", "emptyProxiesMsg": "No proxies disponibles", - "delayTestTooltip": "Prueba de Restraso", + "delayTestTooltip": "Ubicaciones", "sortTooltip": "Ordenar Proxies", "checkIp": "Comprobar IP", "unknownIp": "IP desconocida", + "globalAuto": "Auto", + "globalAutoDesc": "Selección automática de todas las ubicaciones", "sortOptions": { "unsorted": "Por Defecto", "name": "Alfabéticamente", @@ -186,8 +190,7 @@ "themeModes": { "system": "Seguir el tema del sistema", "dark": "Modo Oscuro", - "light": "Modo Claro", - "black": "Modo Negro" + "light": "Modo Claro" }, "enableAnalytics": "Habilitar análisis", "enableAnalyticsMsg": "Dar permiso para recopilar análisis y enviar informes de fallos para mejorar la aplicación.", @@ -220,7 +223,26 @@ }, "showSystemApps": "Mostrar aplicaciones del sistema", "hideSystemApps": "Ocultar aplicaciones del sistema", - "clearSelection": "Selección clara" + "clearSelection": "Selección clara", + "excludedDomains": { + "pageTitle": "Exclusiones", + "domainsTab": "Dominios", + "appsTab": "Aplicaciones", + "addButton": "Agregar dominios o zonas", + "addModalTitle": "+ Agregar dominios", + "addOwnDomain": "Agregar a exclusión", + "domainInputHint": "site.com o .com", + "domainInputDescription": "O zona de dominio completa", + "selectReadyZones": "O seleccionar zonas preparadas:", + "cancel": "Cancelar", + "ok": "OK", + "helpTitle": "Dominios excluidos", + "helpDescription": "Los dominios y zonas de dominio de esta lista omitirán la VPN y usarán conexión directa.", + "helpButton": "Entendido", + "emptyState": "Sin dominios excluidos", + "emptyStateDescription": "Agregar dominios que deben omitir la VPN", + "fabButton": "Agregar" + } }, "geoAssets": { "pageTitle": "Activos de enrutamiento", @@ -310,9 +332,9 @@ } }, "play": { - "title": "Hiddify Next (vista previa)", + "title": "Umbrix (vista previa)", "short_description": "Auto, SSH, VLESS, VMess, Trojan, Reality, Sing-Box, Clash, XRay, Shadowsocks", - "full_description": "El objetivo clave de HiddifyNext es proporcionar un cliente de túnel seguro, fácil de usar y eficiente. Le permite enrutar todo el tráfico o el tráfico de aplicaciones seleccionadas a un servidor remoto de su elección, utilizando el permiso del servicio VPN.Nota: No proporcionamos ningún servidor; Los usuarios deben garantizar que sus actividades en línea permanezcan privadas mediante el uso de su propio servidor autohospedado o servidores confiables. Soportamos servidores con:- Enlace de suscripción normal a V2ray/Xray- Enlace de suscripción a Choque- Enlace de suscripción a Sing-Box¿Cuáles son nuestras características únicas? - Fácil de usar - Optimizado y Rápido - Seleccionar automáticamente LowestPing - Mostrar información de uso del usuario. - Importe fácilmente un subvínculo con un solo clic mediante enlaces profundos - Gratis y sin anuncios - Cambie fácilmente los subvínculos de usuario - más y másApoyo:- Todos los protocolos soportados por Sing-Box- VLESS + xtls realidad, visión- VMESS- troyano- Calcetines Shoadow- Realidad-V2ray-Histria2-TUIC-SSH- SombraTLSEl código fuente existe en https://github.com/hiddify/Hiddify-NextEl núcleo de la aplicación se basa en sing-box de código abierto.Descripción del permiso:- Servicio VPN: como el objetivo de esta aplicación es proporcionar un cliente de túnel seguro, fácil de usar y eficiente, necesitamos este permiso para poder enrutar el tráfico a través del túnel al servidor remoto.- CONSULTAR TODOS LOS PAQUETES: este permiso se utiliza para permitir a los usuarios incluir o excluir aplicaciones específicas para la tunelización.- RECIBIR ARRANQUE COMPLETADO: este permiso se puede habilitar o deshabilitar desde la configuración de la aplicación para activar esta aplicación al iniciar el dispositivo.- PUBLICAR NOTIFICACIONES: este permiso es esencial ya que empleamos un servicio en primer plano para garantizar el funcionamiento continuo del servicio VPN.- Esta aplicación está libre de publicidad. Los datos analíticos y de fallos solo se producen con el consentimiento explícito del usuario en el primer uso de la aplicación." + "full_description": "El objetivo clave de Umbrix es proporcionar un cliente de túnel seguro, fácil de usar y eficiente. Le permite enrutar todo el tráfico o el tráfico de aplicaciones seleccionadas a un servidor remoto de su elección, utilizando el permiso del servicio VPN.Nota: No proporcionamos ningún servidor; Los usuarios deben garantizar que sus actividades en línea permanezcan privadas mediante el uso de su propio servidor autohospedado o servidores confiables. Soportamos servidores con:- Enlace de suscripción normal a V2ray/Xray- Enlace de suscripción a Choque- Enlace de suscripción a Sing-Box¿Cuáles son nuestras características únicas? - Fácil de usar - Optimizado y Rápido - Seleccionar automáticamente LowestPing - Mostrar información de uso del usuario. - Importe fácilmente un subvínculo con un solo clic mediante enlaces profundos - Gratis y sin anuncios - Cambie fácilmente los subvínculos de usuario - más y másApoyo:- Todos los protocolos soportados por Sing-Box- VLESS + xtls realidad, visión- VMESS- troyano- Calcetines Shoadow- Realidad-V2ray-Histria2-TUIC-SSH- SombraTLSEl código fuente existe en https://github.com/hiddify/Hiddify-NextEl núcleo de la aplicación se basa en sing-box de código abierto.Descripción del permiso:- Servicio VPN: como el objetivo de esta aplicación es proporcionar un cliente de túnel seguro, fácil de usar y eficiente, necesitamos este permiso para poder enrutar el tráfico a través del túnel al servidor remoto.- CONSULTAR TODOS LOS PAQUETES: este permiso se utiliza para permitir a los usuarios incluir o excluir aplicaciones específicas para la tunelización.- RECIBIR ARRANQUE COMPLETADO: este permiso se puede habilitar o deshabilitar desde la configuración de la aplicación para activar esta aplicación al iniciar el dispositivo.- PUBLICAR NOTIFICACIONES: este permiso es esencial ya que empleamos un servicio en primer plano para garantizar el funcionamiento continuo del servicio VPN.- Esta aplicación está libre de publicidad. Los datos analíticos y de fallos solo se producen con el consentimiento explícito del usuario en el primer uso de la aplicación." }, "connection": { "tapToConnect": "Toque para conectarse", diff --git a/assets/translations/strings_fa.i18n.json b/assets/translations/strings_fa.i18n.json index a498162a..bdaa6aaa 100644 --- a/assets/translations/strings_fa.i18n.json +++ b/assets/translations/strings_fa.i18n.json @@ -26,7 +26,9 @@ }, "intro": { "termsAndPolicyCaution(rich)": "در صورت ادامه با ${tap(@:about.termsAndConditions)} موافقت می‌کنید", - "start": "آغاز" + "start": "آغاز", + "welcomeTitle": "به Umbrix خوش آمدید", + "subtitle": "ساده. سریع. قابل اعتماد." }, "home": { "pageTitle": "خانه", @@ -127,12 +129,14 @@ } }, "proxies": { - "pageTitle": "پروکسی‌ها", + "pageTitle": "مکان‌ها", "emptyProxiesMsg": "هیچ پروکسی موجود نیست", - "delayTestTooltip": "تست تأخیر", + "delayTestTooltip": "مکان‌ها", "sortTooltip": "مرتب‌سازی پروکسی‌ها", "checkIp": "بررسی آی‌پی", "unknownIp": "آی‌پی ناشناخته", + "globalAuto": "خودکار", + "globalAutoDesc": "انتخاب خودکار از همه مکان‌ها", "sortOptions": { "unsorted": "پیش‌فرض", "name": "براساس نام", @@ -186,8 +190,7 @@ "themeModes": { "system": "پیروی از پوسته‌ی دستگاه", "dark": "حالت تیره", - "light": "حالت روشن", - "black": "حالت سیاه" + "light": "حالت روشن" }, "enableAnalytics": "فعال‌سازی تجزیه و تحلیل‌ها", "enableAnalyticsMsg": "ارائه‌ی دسترسی جمع‌آوری تجزیه و تحلیل‌ها و ارسال گزارش‌های خطا برای بهبود عملکرد برنامه", @@ -218,9 +221,28 @@ "exclude": "کنار گذاشتن", "excludeMsg": "برنامه‌های انتخاب‌شده پروکسی نشوند" }, - "showSystemApps": "نمایش برنامه‌های سیستمی", - "hideSystemApps": "پنهان کردن برنامه‌های سیستمی", - "clearSelection": "پاک کردن انتخاب‌ها" + "showSystemApps": "نمایش برنامه‌های سیستم", + "hideSystemApps": "مخفی کردن برنامه‌های سیستم", + "clearSelection": "پاک کردن انتخاب", + "excludedDomains": { + "pageTitle": "استثناها", + "domainsTab": "دامنه‌ها", + "appsTab": "برنامه‌ها", + "addButton": "افزودن دامنه‌ها یا مناطق", + "addModalTitle": "+ افزودن دامنه‌ها", + "addOwnDomain": "افزودن به استثنا", + "domainInputHint": "site.com یا .com", + "domainInputDescription": "یا کل منطقه دامنه", + "selectReadyZones": "یا مناطق آماده را انتخاب کنید:", + "cancel": "لغو", + "ok": "تأیید", + "helpTitle": "دامنه‌های مستثنی", + "helpDescription": "دامنه‌ها و مناطق دامنه از این لیست VPN را دور می‌زنند و از اتصال مستقیم استفاده می‌کنند.", + "helpButton": "متوجه شدم", + "emptyState": "دامنه مستثنی وجود ندارد", + "emptyStateDescription": "دامنه‌هایی که باید VPN را دور بزنند اضافه کنید", + "fabButton": "افزودن" + } }, "geoAssets": { "pageTitle": "فایل‌های مسیریابی", diff --git a/assets/translations/strings_fr.i18n.json b/assets/translations/strings_fr.i18n.json index dd4b3763..53b6c9a6 100644 --- a/assets/translations/strings_fr.i18n.json +++ b/assets/translations/strings_fr.i18n.json @@ -1,6 +1,6 @@ { "general": { - "appTitle": "Hiddify", + "appTitle": "Umbrix", "reset": "Réinitialiser", "toggle": { "enabled": "Activé", @@ -25,8 +25,10 @@ "grantPermission": "Donner la permission" }, "intro": { - "termsAndPolicyCaution(rich)": "En continuant, vous êtes d'accord avec ${tap( @:about.termsAndConditions)}", - "start": "Commencer" + "termsAndPolicyCaution(rich)": "En continuant, vous acceptez ${tap(@:about.termsAndConditions)}", + "start": "Commencer", + "welcomeTitle": "Bienvenue sur Umbrix", + "subtitle": "Simple. Rapide. Fiable." }, "home": { "pageTitle": "Maison", @@ -127,12 +129,14 @@ } }, "proxies": { - "pageTitle": "Procurations", + "pageTitle": "Emplacements", "emptyProxiesMsg": "Aucun proxy disponible", - "delayTestTooltip": "Délai de test", + "delayTestTooltip": "Emplacements", "sortTooltip": "Trier les proxys", "checkIp": "Vérifier l'adresse IP", "unknownIp": "IP inconnue", + "globalAuto": "Auto", + "globalAutoDesc": "Sélection automatique parmi tous les emplacements", "sortOptions": { "unsorted": "Défaut", "name": "Alphabétiquement", @@ -186,8 +190,7 @@ "themeModes": { "system": "Suivre le thème du système", "dark": "Mode sombre", - "light": "Mode lumière", - "black": "Mode noir" + "light": "Mode lumière" }, "enableAnalytics": "Activer l'analyse", "enableAnalyticsMsg": "Autoriser la collecte d'analyses et l'envoi de rapports d'erreur pour améliorer l'application", @@ -220,7 +223,26 @@ }, "showSystemApps": "Afficher les applications système", "hideSystemApps": "Masquer les applications système", - "clearSelection": "Effacer la sélection" + "clearSelection": "Effacer la sélection", + "excludedDomains": { + "pageTitle": "Exclusions", + "domainsTab": "Domaines", + "appsTab": "Applications", + "addButton": "Ajouter des domaines ou zones", + "addModalTitle": "+ Ajouter des domaines", + "addOwnDomain": "Ajouter à l'exclusion", + "domainInputHint": "site.com ou .com", + "domainInputDescription": "Ou zone de domaine entière", + "selectReadyZones": "Ou sélectionner des zones prêtes:", + "cancel": "Annuler", + "ok": "OK", + "helpTitle": "Domaines exclus", + "helpDescription": "Les domaines et zones de domaine de cette liste contourneront le VPN et utiliseront une connexion directe.", + "helpButton": "Compris", + "emptyState": "Aucun domaine exclu", + "emptyStateDescription": "Ajoutez des domaines qui doivent contourner le VPN", + "fabButton": "Ajouter" + } }, "geoAssets": { "pageTitle": "Actifs de routage", @@ -310,9 +332,9 @@ } }, "play": { - "title": "Hiddify (aperçu)", + "title": "Umbrix (aperçu)", "short_description": "Auto, SSH, VLESS, VMess, cheval de Troie, Reality, Sing-Box, Clash, XRay, Shadowsocks", - "full_description": "L'objectif principal de Hiddify est de fournir un client de tunneling sécurisé, convivial et efficace. Il vous permet d'acheminer tout le trafic ou le trafic d'applications sélectionnées vers un serveur distant de votre choix, en utilisant l'autorisation du service VPN.\nRemarque : Nous ne fournissons aucun serveur ; les utilisateurs sont tenus de garantir que leurs activités en ligne restent privées en utilisant leur propre serveur auto-hébergé ou des serveurs de confiance.\nNous prenons en charge les serveurs avec :\n- Lien d'abonnement normal V2Ray/XRay\n- Lien d'abonnement Clash\n- Lien d'abonnement à Sing-Box\nQuelles sont nos caractéristiques uniques ?\n- Convivial\n- Optimisé et rapide\n- Sélectionnez automatiquement le plus bas Ping\n- Afficher les informations d'utilisation de l'utilisateur\n- Importez facilement des sous-liens en un seul clic grâce au deeplinking\n- Gratuit et sans publicité\n- Changez facilement de sous-liens utilisateur\n- De plus en plus\nSoutien:\n- Tous les protocoles pris en charge par Sing-Box\n- VLESS + XTLS Réalité, Vision\n-VMess\n- Cheval de Troie\n- Chaussettes Shadow\n- Réalité\n- WireGuard\n-V2Ray\n- Hystérie2\n-TUICv5\n-SSH\n-OmbreTLS\nLe code source existe sur https://github.com/hiddify/Hiddify-Next\nLe cœur de l'application est basé sur Sing-Box open source.\nDescription de l'autorisation :\n- Service VPN : L'objectif de cette application étant de fournir un client de tunneling sécurisé, convivial et efficace, nous avons besoin de cette autorisation pour pouvoir acheminer le trafic via un tunnel vers le serveur distant.\n- REQUÊTER TOUS LES PAQUETS : cette autorisation est utilisée pour permettre aux utilisateurs d'inclure ou d'exclure des applications spécifiques pour le tunneling.\n- RECEVOIR LE BOOT TERMINÉ : Cette autorisation peut être activée ou désactivée à partir des paramètres de l'application pour activer cette application au démarrage de l'appareil.\n- POST NOTIFICATIONS : Cette autorisation est essentielle car nous utilisons un service de premier plan pour assurer le fonctionnement continu du service VPN.\n- Cette application est exempte de publicités. Les données d'analyse et de crash n'ont lieu qu'avec le consentement explicite de l'utilisateur lors de la première utilisation de l'application." + "full_description": "L'objectif principal de Umbrix est de fournir un client de tunneling sécurisé, convivial et efficace. Il vous permet d'acheminer tout le trafic ou le trafic d'applications sélectionnées vers un serveur distant de votre choix, en utilisant l'autorisation du service VPN.\nRemarque : Nous ne fournissons aucun serveur ; les utilisateurs sont tenus de garantir que leurs activités en ligne restent privées en utilisant leur propre serveur auto-hébergé ou des serveurs de confiance.\nNous prenons en charge les serveurs avec :\n- Lien d'abonnement normal V2Ray/XRay\n- Lien d'abonnement Clash\n- Lien d'abonnement à Sing-Box\nQuelles sont nos caractéristiques uniques ?\n- Convivial\n- Optimisé et rapide\n- Sélectionnez automatiquement le plus bas Ping\n- Afficher les informations d'utilisation de l'utilisateur\n- Importez facilement des sous-liens en un seul clic grâce au deeplinking\n- Gratuit et sans publicité\n- Changez facilement de sous-liens utilisateur\n- De plus en plus\nSoutien:\n- Tous les protocoles pris en charge par Sing-Box\n- VLESS + XTLS Réalité, Vision\n-VMess\n- Cheval de Troie\n- Chaussettes Shadow\n- Réalité\n- WireGuard\n-V2Ray\n- Hystérie2\n-TUICv5\n-SSH\n-OmbreTLS\nLe code source existe sur https://github.com/hiddify/Hiddify-Next\nLe cœur de l'application est basé sur Sing-Box open source.\nDescription de l'autorisation :\n- Service VPN : L'objectif de cette application étant de fournir un client de tunneling sécurisé, convivial et efficace, nous avons besoin de cette autorisation pour pouvoir acheminer le trafic via un tunnel vers le serveur distant.\n- REQUÊTER TOUS LES PAQUETS : cette autorisation est utilisée pour permettre aux utilisateurs d'inclure ou d'exclure des applications spécifiques pour le tunneling.\n- RECEVOIR LE BOOT TERMINÉ : Cette autorisation peut être activée ou désactivée à partir des paramètres de l'application pour activer cette application au démarrage de l'appareil.\n- POST NOTIFICATIONS : Cette autorisation est essentielle car nous utilisons un service de premier plan pour assurer le fonctionnement continu du service VPN.\n- Cette application est exempte de publicités. Les données d'analyse et de crash n'ont lieu qu'avec le consentement explicite de l'utilisateur lors de la première utilisation de l'application." }, "connection": { "tapToConnect": "Appuyez pour vous connecter", diff --git a/assets/translations/strings_id.i18n.json b/assets/translations/strings_id.i18n.json index b1941395..1dbbedf5 100644 --- a/assets/translations/strings_id.i18n.json +++ b/assets/translations/strings_id.i18n.json @@ -1,6 +1,6 @@ { "general": { - "appTitle": "Hiddify", + "appTitle": "Umbrix", "reset": "Set Ulang", "toggle": { "enabled": "Mengaktifkan", @@ -26,7 +26,9 @@ }, "intro": { "termsAndPolicyCaution(rich)": "lanjut berarti setuju dengan ${tap(@:about.termsAndConditions)}", - "start": "Mulai" + "start": "Mulai", + "welcomeTitle": "Selamat datang di Umbrix", + "subtitle": "Sederhana. Cepat. Andal." }, "home": { "pageTitle": "Utama", @@ -127,12 +129,14 @@ } }, "proxies": { - "pageTitle": "Proxy", + "pageTitle": "Lokasi", "emptyProxiesMsg": "Tidak ada proxy", - "delayTestTooltip": "Test delay", + "delayTestTooltip": "Lokasi", "sortTooltip": "Urut Proxy", "checkIp": "Periksa IP", "unknownIp": "IP tidak dikenal", + "globalAuto": "Otomatis", + "globalAutoDesc": "Pilihan otomatis dari semua lokasi", "sortOptions": { "unsorted": "Awal", "name": "Alfabetikal", @@ -186,8 +190,7 @@ "themeModes": { "system": "Ikut Tema Sistem", "dark": "Tema Gelap", - "light": "Tema Cerah", - "black": "Tema Hitam" + "light": "Tema Cerah" }, "enableAnalytics": "Mengaktifkan Analitik", "enableAnalyticsMsg": "Beri izin untuk mengumpulkan analisis dan mengirim laporan kegagalan untuk meningkatkan aplikasi", @@ -220,7 +223,26 @@ }, "showSystemApps": "Tampil aplikasi sistem", "hideSystemApps": "Sembunyikan aplikasi sistem", - "clearSelection": "Bersihkan seleksi" + "clearSelection": "Bersihkan seleksi", + "excludedDomains": { + "pageTitle": "Pengecualian", + "domainsTab": "Domain", + "appsTab": "Aplikasi", + "addButton": "Tambah domain atau zona", + "addModalTitle": "+ Tambah domain", + "addOwnDomain": "Tambah untuk dikecualikan", + "domainInputHint": "site.com atau .id", + "domainInputDescription": "Atau seluruh zona domain", + "selectReadyZones": "Atau pilih zona siap pakai:", + "cancel": "Batal", + "ok": "OK", + "helpTitle": "Domain yang dikecualikan", + "helpDescription": "Domain dan zona domain dari daftar ini akan melewati VPN dan menggunakan koneksi langsung.", + "helpButton": "Mengerti", + "emptyState": "Tidak ada domain yang dikecualikan", + "emptyStateDescription": "Tambahkan domain yang harus melewati VPN", + "fabButton": "Tambah" + } }, "geoAssets": { "pageTitle": "Rute Aset", @@ -310,9 +332,9 @@ } }, "play": { - "title": "Hiddify (Preview)", + "title": "Umbrix (Preview)", "short_description": "Otomatik, SSH, VLESS, Vmess, Trojan, Reality, Sing-Box, Clash, Xray, Shadowsocks", - "full_description": "Tujuan utama Hiddify adalah memberikan keamanan, user-friendly dan client tunnel yg efisien. Hiddify mengizinkanmu untuk mengarahkan semua atau beberapa trafik data aplikasi terpilih ke server remot pilihanmu, memanfaatkan izin VPN-Service.\n\nNote: Kami tidak menyediakan server apapun; pengguna diwajibkan untuk memastikan aktivitas online mereka tetap private dengan menggunakan self-hosted server atau server yg dipercaya pilihan mereka sendiri. \n \nKami mendukung server dengan:\n- Normal V2ray/Xray Subscription Link\n- Clash Subscription Link\n- Sing-Box Subscription Link\n\nApa fitur unik kami?\n - User Friendly\n - Cepat dan teroptimasi\n - Otomatis pilihan PING terendah \n - Menampilkan informasi penggunaan user\n - Dengan mudah import sublink dengan satu klik menggunakan deeplinking \n - Bebas dan tanpa iklan\n - Dengan mudah berganti user sublink\n - dan sebagainya\n\nSupport:\n- Semua protokol di dukung oleh Sing-Box \n- VLESS + xtls reality, vision\n- VMESS\n- Trojan\n- ShadowSocks\n- Reality\n- V2ray\n- Hystria2\n- TUIC\n- SSH\n- ShadowTLS\n\n\nCode Sumber ada di https://github.com/hiddify/Hiddify-Next\nInti Aplikasi didasarkan pada Sing-Box Open Source.\n\nDeskripsi Izin:\n- VPN Servis: Tujuan aplikasi ini menyediakan keamanan, user-friendly dan tunneling client efisien, kami membutuhkan izin untuk bisa mengarahkan traffic data melalui kanal remot server. \n- QUERY ALL PACKAGES: izin ini digunakan untuk memperbolehkan pengguna masuk atau mengeluarkan aplikasi tertentu untuk tunneling.\n- RECEIVE BOOT COMPLETED: izin ini dapat diaktifkan atau dinonaktifkan dari setting aplikasi untuk mengaktikan aplikasi ini saat boot device.\n- POST NOTIFICATIONS: izin ini penting karena kami menggunakan foreground service untuk memastikan operasi berkelanjutan dari VPN Servis.\n- Aplikasi ini bebas dari iklan. Analitik dan data crash hanya terjadi dengan persetujuan eksplisit dari pengguna pada saat penggunaan pertama kali" + "full_description": "Tujuan utama Umbrix adalah memberikan keamanan, user-friendly dan client tunnel yg efisien. Umbrix mengizinkanmu untuk mengarahkan semua atau beberapa trafik data aplikasi terpilih ke server remot pilihanmu, memanfaatkan izin VPN-Service.\n\nNote: Kami tidak menyediakan server apapun; pengguna diwajibkan untuk memastikan aktivitas online mereka tetap private dengan menggunakan self-hosted server atau server yg dipercaya pilihan mereka sendiri. \n \nKami mendukung server dengan:\n- Normal V2ray/Xray Subscription Link\n- Clash Subscription Link\n- Sing-Box Subscription Link\n\nApa fitur unik kami?\n - User Friendly\n - Cepat dan teroptimasi\n - Otomatis pilihan PING terendah \n - Menampilkan informasi penggunaan user\n - Dengan mudah import sublink dengan satu klik menggunakan deeplinking \n - Bebas dan tanpa iklan\n - Dengan mudah berganti user sublink\n - dan sebagainya\n\nSupport:\n- Semua protokol di dukung oleh Sing-Box \n- VLESS + xtls reality, vision\n- VMESS\n- Trojan\n- ShadowSocks\n- Reality\n- V2ray\n- Hystria2\n- TUIC\n- SSH\n- ShadowTLS\n\n\nInti Aplikasi didasarkan pada Sing-Box Open Source.\n\nDeskripsi Izin:\n- VPN Servis: Tujuan aplikasi ini menyediakan keamanan, user-friendly dan tunneling client efisien, kami membutuhkan izin untuk bisa mengarahkan traffic data melalui kanal remot server. \n- QUERY ALL PACKAGES: izin ini digunakan untuk memperbolehkan pengguna masuk atau mengeluarkan aplikasi tertentu untuk tunneling.\n- RECEIVE BOOT COMPLETED: izin ini dapat diaktifkan atau dinonaktifkan dari setting aplikasi untuk mengaktikan aplikasi ini saat boot device.\n- POST NOTIFICATIONS: izin ini penting karena kami menggunakan foreground service untuk memastikan operasi berkelanjutan dari VPN Servis.\n- Aplikasi ini bebas dari iklan. Analitik dan data crash hanya terjadi dengan persetujuan eksplisit dari pengguna pada saat penggunaan pertama kali" }, "connection": { "tapToConnect": "Ketuk untuk Sambung", diff --git a/assets/translations/strings_pt-BR.i18n.json b/assets/translations/strings_pt-BR.i18n.json index f2b2f202..e6b6bcc1 100644 --- a/assets/translations/strings_pt-BR.i18n.json +++ b/assets/translations/strings_pt-BR.i18n.json @@ -1,6 +1,6 @@ { "general": { - "appTitle": "Hiddify", + "appTitle": "Umbrix", "reset": "Restaurar", "toggle": { "enabled": "Habilitado", @@ -25,8 +25,10 @@ "grantPermission": "Conceder permissão" }, "intro": { - "termsAndPolicyCaution(rich)": "ao continuar você concorda com ${tap( @:about.termsAndConditions)}", - "start": "Começar" + "termsAndPolicyCaution(rich)": "Ao continuar, você concorda com ${tap(@:about.termsAndConditions)}", + "start": "Começar", + "welcomeTitle": "Bem-vindo ao Umbrix", + "subtitle": "Simples. Rápido. Confiável." }, "home": { "pageTitle": "Inicio", @@ -127,12 +129,14 @@ } }, "proxies": { - "pageTitle": "Proxies", + "pageTitle": "Localizações", "emptyProxiesMsg": "Nenhum proxy disponível", - "delayTestTooltip": "Atraso de teste", + "delayTestTooltip": "Localizações", "sortTooltip": "Ordenar proxies", "checkIp": "Verifique o IP", "unknownIp": "IP desconhecido", + "globalAuto": "Auto", + "globalAutoDesc": "Seleção automática de todos os locais", "sortOptions": { "unsorted": "Padrão", "name": "Alfabeticamente", @@ -186,8 +190,7 @@ "themeModes": { "system": "Seguir o tema do sistema", "dark": "Modo Escuro", - "light": "Modo Claro", - "black": "Modo Preto" + "light": "Modo Claro" }, "enableAnalytics": "Habilitar Análise", "enableAnalyticsMsg": "Dê permissão para coletar análises e enviar relatórios de falhas para melhorar o aplicativo", @@ -220,7 +223,26 @@ }, "showSystemApps": "Mostrar aplicativos do sistema", "hideSystemApps": "Ocultar aplicativos do sistema", - "clearSelection": "Limpar seleção" + "clearSelection": "Limpar seleção", + "excludedDomains": { + "pageTitle": "Exclusões", + "domainsTab": "Domínios", + "appsTab": "Aplicativos", + "addButton": "Adicionar domínios ou zonas", + "addModalTitle": "+ Adicionar domínios", + "addOwnDomain": "Adicione para exclusão", + "domainInputHint": "site.com ou .br", + "domainInputDescription": "Ou toda a zona de domínio", + "selectReadyZones": "Ou selecione zonas prontas:", + "cancel": "Cancelar", + "ok": "OK", + "helpTitle": "Domínios excluídos", + "helpDescription": "Domínios e zonas de domínio desta lista ignorarão a VPN e usarão conexão direta.", + "helpButton": "Entendi", + "emptyState": "Nenhum domínio excluído", + "emptyStateDescription": "Adicione domínios que devem ignorar a VPN", + "fabButton": "Adicionar" + } }, "geoAssets": { "pageTitle": "Ativos de roteamento", @@ -310,9 +332,9 @@ } }, "play": { - "title": "Hiddify (Pré-visualização)", + "title": "Umbrix (Pré-visualização)", "short_description": "Auto, SSH, VLESS, VMess, Trojan, Reality, Sing-Box, Clash, XRay, Shadowsocks", - "full_description": "O principal objetivo do Hiddify é fornecer um cliente de tunelamento seguro, fácil de usar e eficiente. Ele permite que você direcione todo o tráfego ou tráfego de aplicativo selecionado para um servidor remoto de sua escolha, utilizando a permissão do serviço VPN.\nNota: Não fornecemos nenhum servidor; os usuários são obrigados a garantir que suas atividades online permaneçam privadas usando seu próprio servidor auto-hospedado ou servidores confiáveis.\nOferecemos suporte a servidores com:\n- Link de assinatura V2Ray/XRay normal\n- Link de assinatura do Clash\n- Link de assinatura do Sing-Box\nQuais são os nossos recursos exclusivos?\n- Amigo do usuário\n- Otimizado e rápido\n- Selecione automaticamente o LowerPing\n- Mostrar informações de uso do usuário\n- Importe facilmente sublinks com um clique usando deeplinking\n- Gratuito e sem anúncios\n- Alterne facilmente sublinks de usuários\n- Mais e mais\nApoiar:\n- Todos os protocolos suportados pelo Sing-Box\n- VLESS + XTLS Realidade, Visão\n- VMess\n- Trojan\n- ShadowSocks\n- Realidade\n- WireGuard\n-V2Ray\n- Histeria2\n-TUICv5\n-SSH\n- ShadowTLS\nO código-fonte existe em https://github.com/hiddify/Hiddify-Next\nO núcleo do aplicativo é baseado no Sing-Box de código aberto.\nDescrição da permissão:\n- Serviço VPN: Como o objetivo desta aplicação é fornecer um cliente de tunelamento seguro, fácil de usar e eficiente, precisamos dessa permissão para poder rotear o tráfego via túnel para o servidor remoto.\n- CONSULTAR TODOS OS PACOTES: Esta permissão é usada para permitir que os usuários incluam ou excluam aplicativos específicos para tunelamento.\n- RECEBER BOOT COMPLETED: Esta permissão pode ser habilitada ou desabilitada nas configurações do aplicativo para ativar este aplicativo na inicialização do dispositivo.\n- PÓS NOTIFICAÇÕES: Esta permissão é essencial, pois empregamos um serviço de primeiro plano para garantir a operação contínua do serviço VPN.\n- Este aplicativo está livre de anúncios. A análise e os dados de travamento só ocorrem com o consentimento explícito do usuário na primeira utilização do aplicativo." + "full_description": "O principal objetivo do Umbrix é fornecer um cliente de tunelamento seguro, fácil de usar e eficiente. Ele permite que você direcione todo o tráfego ou tráfego de aplicativo selecionado para um servidor remoto de sua escolha, utilizando a permissão do serviço VPN.\nNota: Não fornecemos nenhum servidor; os usuários são obrigados a garantir que suas atividades online permaneçam privadas usando seu próprio servidor auto-hospedado ou servidores confiáveis.\nOferecemos suporte a servidores com:\n- Link de assinatura V2Ray/XRay normal\n- Link de assinatura do Clash\n- Link de assinatura do Sing-Box\nQuais são os nossos recursos exclusivos?\n- Amigo do usuário\n- Otimizado e rápido\n- Selecione automaticamente o LowerPing\n- Mostrar informações de uso do usuário\n- Importe facilmente sublinks com um clique usando deeplinking\n- Gratuito e sem anúncios\n- Alterne facilmente sublinks de usuários\n- Mais e mais\nApoiar:\n- Todos os protocolos suportados pelo Sing-Box\n- VLESS + XTLS Realidade, Visão\n- VMess\n- Trojan\n- ShadowSocks\n- Realidade\n- WireGuard\n-V2Ray\n- Histeria2\n-TUICv5\n-SSH\n- ShadowTLS\nO código-fonte existe em https://github.com/hiddify/Hiddify-Next\nO núcleo do aplicativo é baseado no Sing-Box de código aberto.\nDescrição da permissão:\n- Serviço VPN: Como o objetivo desta aplicação é fornecer um cliente de tunelamento seguro, fácil de usar e eficiente, precisamos dessa permissão para poder rotear o tráfego via túnel para o servidor remoto.\n- CONSULTAR TODOS OS PACOTES: Esta permissão é usada para permitir que os usuários incluam ou excluam aplicativos específicos para tunelamento.\n- RECEBER BOOT COMPLETED: Esta permissão pode ser habilitada ou desabilitada nas configurações do aplicativo para ativar este aplicativo na inicialização do dispositivo.\n- PÓS NOTIFICAÇÕES: Esta permissão é essencial, pois empregamos um serviço de primeiro plano para garantir a operação contínua do serviço VPN.\n- Este aplicativo está livre de anúncios. A análise e os dados de travamento só ocorrem com o consentimento explícito do usuário na primeira utilização do aplicativo." }, "connection": { "tapToConnect": "Toque para conectar", diff --git a/assets/translations/strings_ru.i18n.json b/assets/translations/strings_ru.i18n.json index d2fba1b5..9cba17a9 100644 --- a/assets/translations/strings_ru.i18n.json +++ b/assets/translations/strings_ru.i18n.json @@ -26,7 +26,9 @@ }, "intro": { "termsAndPolicyCaution(rich)": "Продолжая, Вы соглашаетесь с ${tap(@:about.termsAndConditions)}", - "start": "Старт" + "start": "Старт", + "welcomeTitle": "Добро пожаловать в Umbrix", + "subtitle": "Просто. Быстро. Надёжно." }, "home": { "pageTitle": "Главная", @@ -127,12 +129,14 @@ } }, "proxies": { - "pageTitle": "Прокси", + "pageTitle": "Локации", "emptyProxiesMsg": "Нет доступных прокси", - "delayTestTooltip": "Тестирование задержки", + "delayTestTooltip": "Локации", "sortTooltip": "Сортировка прокси", "checkIp": "Проверить IP", "unknownIp": "Неизвестный IP", + "globalAuto": "Авто", + "globalAutoDesc": "Автовыбор из всех локаций", "sortOptions": { "unsorted": "По умолчанию", "name": "По алфавиту", @@ -187,8 +191,7 @@ "themeModes": { "system": "Системная тема", "dark": "Тёмная тема", - "light": "Светлая тема", - "black": "Чёрная тема" + "light": "Светлая тема" }, "enableAnalytics": "Сбор аналитики", "enableAnalyticsMsg": "Сбор данных аналитики и отправка отчётов о сбоях для улучшения приложения", @@ -219,11 +222,11 @@ "perAppProxyPageTitle": "Раздельное проксирование приложений", "perAppProxyModes": { "off": "Все", - "offMsg": "Проксировать все приложения", + "offMsg": "Все приложения будут использовать VPN", "include": "Включить", - "includeMsg": "Проксировать выбранные приложения", + "includeMsg": "Выбранные приложения будут использовать VPN", "exclude": "Исключить", - "excludeMsg": "Не проксировать выбранные приложения" + "excludeMsg": "Выбранные приложения НЕ будут использовать VPN" }, "showSystemApps": "Показать системные приложения", "hideSystemApps": "Скрыть системные приложения", @@ -234,8 +237,9 @@ "appsTab": "Приложения", "addButton": "Добавить домены или зоны", "addModalTitle": "+ Добавить домены", - "addOwnDomain": "Добавьте свой сайт:", - "domainInputHint": "site.com", + "addOwnDomain": "Добавьте для исключения", + "domainInputHint": "site.com или .com", + "domainInputDescription": "Или всю доменную зону", "selectReadyZones": "Или выберите готовые зоны:", "cancel": "Отмена", "ok": "ОК", @@ -268,7 +272,43 @@ "telegramChannel": "Telegram-канал", "checkForUpdate": "Проверка обновления", "privacyPolicy": "Политика конфиденциальности", - "termsAndConditions": "Условия и положения" + "termsAndConditions": "Условия и положения", + "licenses": "Лицензии", + "openLicenses": "Открытые лицензии" + }, + "privacyPolicy": { + "lastUpdated": "Последнее обновление: 27 декабря 2025 г.", + "section1Title": "Общие положения", + "section1Content": "Umbrix — это приватный прокси-клиент, созданный для защиты вашей конфиденциальности. Мы не собираем, не храним и не передаём третьим лицам ваши личные данные.", + "section2Title": "Какие данные мы НЕ собираем", + "section2Content": "❌ IP-адреса\n❌ История посещённых сайтов\n❌ Содержимое трафика\n❌ Персональные идентификаторы (IMEI, MAC-адрес и т.д.)\n❌ Платёжную информацию", + "section3Title": "Аналитика (добровольно)", + "section3Content": "Приложение может собирать анонимную аналитику только если вы явно включите эту функцию в настройках:\n• Информация о сбоях приложения (для исправления ошибок)\n• Общая информация об устройстве (только версия ОС)\n• Эти данные используются исключительно для улучшения приложения\n\nВы можете в любой момент отключить аналитику в настройках.", + "section4Title": "Логи и отладка", + "section4Content": "Логи приложения хранятся только локально на вашем устройстве. Вы можете добровольно отправить логи разработчику через защищённый канал для помощи в решении проблем.", + "section5Title": "Ваши права", + "section5Content": "• Право на полную анонимность\n• Право знать какие данные обрабатываются (никакие)\n• Право удалить приложение без следов", + "section6Title": "Изменения в политике", + "section6Content": "При изменении политики вы будете уведомлены при обновлении приложения.", + "section7Title": "Контакты", + "section7Content": "Вопросы по конфиденциальности: support@umbrix.app" + }, + "termsAndConditions": { + "lastUpdated": "Последнее обновление: 27 декабря 2025 г.", + "section1Title": "Принятие условий", + "section1Content": "Используя Umbrix, вы соглашаетесь с данными условиями.", + "section2Title": "Описание сервиса", + "section2Content": "Umbrix — это прокси-клиент для безопасного подключения к прокси-серверам.", + "section3Title": "Конфиденциальность", + "section3Content": "🔒 Umbrix не собирает и не хранит ваши данные\n🔒 Мы не отслеживаем вашу активность\n🔒 Аналитика отключена по умолчанию (включается добровольно)\n🔒 Логи хранятся только локально на вашем устройстве", + "section4Title": "Ответственное использование", + "section4Content": "✅ Используйте для защиты конфиденциальности\n✅ Соблюдайте законы вашей страны\n❌ Не используйте для незаконной деятельности", + "section5Title": "Отказ от ответственности", + "section5Content": "Приложение предоставляется \"как есть\" без каких-либо гарантий. Вы несёте полную ответственность за использование приложения.", + "section6Title": "Ограничение ответственности", + "section6Content": "Разработчики не несут ответственности за:\n• Потерю данных\n• Нарушение работы других сервисов\n• Действия третьих лиц", + "section7Title": "Изменения условий", + "section7Content": "Мы оставляем за собой право изменять данные условия. Продолжение использования после изменений означает принятие новых условий." }, "appUpdate": { "notAvailableMsg": "Уже используется последняя версия", @@ -335,9 +375,9 @@ } }, "play": { - "title": "Hiddify (Предварительная версия)", + "title": "Umbrix (Предварительная версия)", "short_description": "Автовыбор, SSH, VLESS, VMess, Trojan, Reality, Sing-Box, Clash, Xray, Shadowsocks", - "full_description": "Основная цель Hiddify — предоставить безопасный, удобный и эффективный клиент туннелирования. Он позволяет направлять весь трафик или трафик выбранного приложения на указанный Вами удалённый сервер.\n\nПримечание: мы не предоставляем серверы, пользователи должны сами обеспечивать конфиденциальность своих действий в Интернете, используя собственный сервер или доверенные серверы. \nПоддерживаются сервера с:\n- Обычной ссылкой на подписку V2ray/Xray\n- Ссылкой на подписку Clash\n- Ссылкой на подписку на Sing–Box\n\nВ чём уникальные особенности? \n- Удобство\n- Оптимизация и скорость\n- Автоматический выбор минимальной задержки\n- Отображение информации об использовании\n- Простой импорт подписок одним щелчком мыши\n- Бесплатно и без рекламы\n- Простое переключение подписок\n- И многое другое...\n\nПоддерживаются:\n- Все протоколы, поддерживаемые Sing-Box\n- VLESS + XTLS Reality, Vision\n- VMESS\n- Trojan\n- ShoadowSocks\n- Reality\n- V2ray\n- Hystria2\n- TUIC\n- SSH\n- ShadowTLS\n\nИсходный код доступен по адресу https://github.com/hiddify/Hiddify-Next\nЯдро приложения основано на открытом исходном коде Sing–Box.\n\nОписание разрешений:\n- СЛУЖБА VPN: поскольку целью данного приложения является предоставление безопасного, удобного и эффективного клиента туннелирования, это разрешение необходимо, чтобы иметь возможность направлять трафик через туннель на удалённый сервер.\n- ЗАПРОС ВСЕХ ПАКЕТОВ: это разрешение позволяет добавлять или удалять определённые приложения из списка для туннелирования.\n- ИНФОРМИРОВАНИЕ О ЗАВЕРШЕНИИ ЗАГРУЗКИ: это разрешение можно включить или отключить в настройках приложения, чтобы (де)активировать запуск приложения при загрузке устройства.\n- ПОСТОЯННОЕ УВЕДОМЛЕНИЕ: это разрешение необходимо, так как используется приоритетная служба для обеспечения непрерывной работы VPN.\n- Приложение не содержит рекламы. Сбор аналитики и данных о сбоях происходят только с явного согласия пользователя при первом использовании приложения." + "full_description": "Основная цель Umbrix — предоставить безопасный, удобный и эффективный клиент туннелирования. Он позволяет направлять весь трафик или трафик выбранного приложения на указанный Вами удалённый сервер.\n\nПримечание: мы не предоставляем серверы, пользователи должны сами обеспечивать конфиденциальность своих действий в Интернете, используя собственный сервер или доверенные серверы. \nПоддерживаются сервера с:\n- Обычной ссылкой на подписку V2ray/Xray\n- Ссылкой на подписку Clash\n- Ссылкой на подписку на Sing–Box\n\nВ чём уникальные особенности? \n- Удобство\n- Оптимизация и скорость\n- Автоматический выбор минимальной задержки\n- Отображение информации об использовании\n- Простой импорт подписок одним щелчком мыши\n- Бесплатно и без рекламы\n- Простое переключение подписок\n- И многое другое...\n\nПоддерживаются:\n- Все протоколы, поддерживаемые Sing-Box\n- VLESS + XTLS Reality, Vision\n- VMESS\n- Trojan\n- ShoadowSocks\n- Reality\n- V2ray\n- Hystria2\n- TUIC\n- SSH\n- ShadowTLS\n\nИсходный код доступен по адресу https://github.com/hiddify/Hiddify-Next\nЯдро приложения основано на открытом исходном коде Sing–Box.\n\nОписание разрешений:\n- СЛУЖБА VPN: поскольку целью данного приложения является предоставление безопасного, удобного и эффективного клиента туннелирования, это разрешение необходимо, чтобы иметь возможность направлять трафик через туннель на удалённый сервер.\n- ЗАПРОС ВСЕХ ПАКЕТОВ: это разрешение позволяет добавлять или удалять определённые приложения из списка для туннелирования.\n- ИНФОРМИРОВАНИЕ О ЗАВЕРШЕНИИ ЗАГРУЗКИ: это разрешение можно включить или отключить в настройках приложения, чтобы (де)активировать запуск приложения при загрузке устройства.\n- ПОСТОЯННОЕ УВЕДОМЛЕНИЕ: это разрешение необходимо, так как используется приоритетная служба для обеспечения непрерывной работы VPN.\n- Приложение не содержит рекламы. Сбор аналитики и данных о сбоях происходят только с явного согласия пользователя при первом использовании приложения." }, "connection": { "tapToConnect": "Нажмите для подключения", @@ -444,7 +484,18 @@ "warpNoise": "Шум", "warpNoiseSize": "Размер шума", "warpNoiseMode": "Шумовой режим", - "warpNoiseDelay": "Задержка шума" + "warpNoiseDelay": "Задержка шума", + "bypassLanWarning": { + "title": "Внимание", + "subtitle": "Только для доверенных сетей (дом/офис)", + "message": "Включайте обход локальной сети только в доверенных сетях (дома или в офисе).\n\n❌ НЕ включайте в:\n • Публичном WiFi (кафе, аэропорты)\n • Гостиницах\n • Незнакомых сетях\n\nВ публичных сетях это может быть небезопасно.\n\nСоединение НЕ будет разорвано - изменения применятся к новым подключениям.", + "cancel": "Отмена", + "enable": "Включить" + }, + "blockAdsWarning": { + "subtitle": "Может вызвать проблемы на некоторых сайтах", + "message": "Блокировка рекламы может привести к проблемам на некоторых сайтах:\n\n• Сайты могут не загружаться полностью\n• Некоторые функции могут не работать\n• Проблемы с авторизацией\n\nЕсли возникнут проблемы, попробуйте отключить эту опцию.\n\nСоединение НЕ будет разорвано - изменения применятся к новым подключениям." + } }, "window": { "hide": "Скрыть", diff --git a/assets/translations/strings_tr.i18n.json b/assets/translations/strings_tr.i18n.json index 6ad6a6d8..4a69c028 100644 --- a/assets/translations/strings_tr.i18n.json +++ b/assets/translations/strings_tr.i18n.json @@ -1,6 +1,6 @@ { "general": { - "appTitle": "Hiddify", + "appTitle": "Umbrix", "reset": "Sıfırla", "toggle": { "enabled": "Etkin", @@ -25,8 +25,10 @@ "grantPermission": "İzin Ver" }, "intro": { - "termsAndPolicyCaution(rich)": "devam ederek ${tap(@:about.termsAndConditions)} kabul etmiş olursunuz", - "start": "Başla" + "termsAndPolicyCaution(rich)": "Devam ederek ${tap(@:about.termsAndConditions)} kabul ediyorsunuz", + "start": "Başlat", + "welcomeTitle": "Umbrix'e hoş geldiniz", + "subtitle": "Basit. Hızlı. Güvenilir." }, "home": { "pageTitle": "Ana Sayfa", @@ -127,12 +129,14 @@ } }, "proxies": { - "pageTitle": "Proxyler", + "pageTitle": "Konumlar", "emptyProxiesMsg": "Kullanılabilir proxy yok", - "delayTestTooltip": "Test Gecikmesi", + "delayTestTooltip": "Konumlar", "sortTooltip": "Proxy'leri Sırala", "checkIp": "IP'yi kontrol edin", "unknownIp": "Bilinmeyen IP", + "globalAuto": "Otomatik", + "globalAutoDesc": "Tüm konumlardan otomatik seçim", "sortOptions": { "unsorted": "Varsayılan", "name": "Alfabetik olarak", @@ -186,8 +190,7 @@ "themeModes": { "system": "Sistem temasını takip et", "dark": "Karanlık mod", - "light": "Işık modu", - "black": "Siyah mod" + "light": "Işık modu" }, "enableAnalytics": "Analitikleri Etkinleştir", "enableAnalyticsMsg": "Uygulamayı iyileştirmek için analiz toplama ve kilitlenme raporları göndermeye izni verin", @@ -218,9 +221,28 @@ "exclude": "Atlatma", "excludeMsg": "Seçilen uygulamalara proxy uygulama" }, - "showSystemApps": "Sistem uygulamalarını göster", - "hideSystemApps": "Sistem uygulamalarını gizle", - "clearSelection": "Seçimi temizle" + "showSystemApps": "Sistem Uygulamalarını Göster", + "hideSystemApps": "Sistem Uygulamalarını Gizle", + "clearSelection": "Seçimi Temizle", + "excludedDomains": { + "pageTitle": "İstisnalar", + "domainsTab": "Alan Adları", + "appsTab": "Uygulamalar", + "addButton": "Alan Adları veya Bölgeler Ekle", + "addModalTitle": "+ Alan Adları Ekle", + "addOwnDomain": "İstisnaya ekle", + "domainInputHint": "site.com veya .com", + "domainInputDescription": "Veya tüm alan adı bölgesi", + "selectReadyZones": "Veya hazır bölgeleri seçin:", + "cancel": "İptal", + "ok": "Tamam", + "helpTitle": "Hariç tutulan alan adları", + "helpDescription": "Bu listedeki alan adları ve alan adı bölgeleri VPN'i atlayacak ve doğrudan bağlantı kullanacaktır.", + "helpButton": "Anladım", + "emptyState": "Hariç tutulan alan adı yok", + "emptyStateDescription": "VPN'i atlaması gereken alan adlarını ekleyin", + "fabButton": "Ekle" + } }, "geoAssets": { "pageTitle": "Varlıkları Yönlendirme", @@ -310,9 +332,9 @@ } }, "play": { - "title": "Hiddify (Önizleme)", + "title": "Umbrix (Önizleme)", "short_description": "Otomatik, SSH, VLESS, Vmess, Trojan, Reality, Sing-Box, Clash, Xray, Shadowsocks", - "full_description": "Hiddify'in temel hedefi güvenli, kullanıcı dostu ve verimli bir tünel istemcisi sağlamaktır. VPN Hizmeti iznini kullanarak tüm trafiği veya seçilen uygulama trafiğini seçtiğiniz uzak bir sunucuya yönlendirmenizi sağlar. Not: Herhangi bir sunucu sağlamıyoruz; kullanıcıların kendi barındırılan sunucularını veya güvenilir sunucularını kullanarak çevrimiçi etkinliklerinin gizli kalmasını sağlamaları gerekir. Sunucuları aşağıdakilerle destekliyoruz: - Normal V2ray/Xray Abonelik Bağlantısı - Clash Abonelik Bağlantısı - Sing-Box Abonelik Bağlantısı Benzersiz özelliklerimiz nelerdir? - Kullanıcı Dostu - Optimize Edilmiş ve Hızlı - En Düşük Ping'i otomatik olarak seçin - Kullanıcı kullanım bilgilerini gösterin - Derin bağlantı kullanarak tek tıklamayla alt bağlantıyı kolayca içe aktarın - Ücretsiz ve ADS Yok - Kullanıcı alt bağlantılarını kolayca değiştirin - giderek daha fazla Destek: - Sing-Box tarafından desteklenen tüm Protokoller - VLESS + xtls gerçeklik, vizyon - VMESS - Trojan - ShoadowSocks - Reality - V2ray - Hystria2 - TUIC - SSH - ShadowTLS Kaynak kodu https://github.com/hiddify/Hiddify-Next adresinde mevcuttur. Uygulama çekirdeği açık tabanlıdır. kaynak şarkı kutusu. İzin Açıklaması: - VPN Hizmeti: Bu uygulamanın amacı güvenli, kullanıcı dostu ve verimli bir tünel istemcisi sağlamak olduğundan, trafiği tünel aracılığıyla uzak sunucuya yönlendirebilmek için bu izne ihtiyacımız var. - TÜM PAKETLERİ SORGULAYIN: Bu izin, kullanıcıların tünelleme için belirli uygulamaları dahil etmesine veya hariç tutmasına izin vermek için kullanılır. - ALMA ÖNYÜKLEME TAMAMLANDI: Bu izin, cihaz önyüklemesi sırasında bu uygulamayı etkinleştirmek için uygulama ayarlarından etkinleştirilebilir veya devre dışı bırakılabilir. - BİLDİRİMLER SONRASI: VPN hizmetinin sürekli çalışmasını sağlamak için bir ön plan hizmeti kullandığımız için bu izin önemlidir. - Bu uygulama reklam içermez. Analitik ve kilitlenme verileri yalnızca uygulamanın ilk kullanımında kullanıcının açık rızası ile gerçekleşir." + "full_description": "Umbrix'in temel hedefi güvenli, kullanıcı dostu ve verimli bir tünel istemcisi sağlamaktır. VPN Hizmeti iznini kullanarak tüm trafiği veya seçilen uygulama trafiğini seçtiğiniz uzak bir sunucuya yönlendirmenizi sağlar. Not: Herhangi bir sunucu sağlamıyoruz; kullanıcıların kendi barındırılan sunucularını veya güvenilir sunucularını kullanarak çevrimiçi etkinliklerinin gizli kalmasını sağlamaları gerekir. Sunucuları aşağıdakilerle destekliyoruz: - Normal V2ray/Xray Abonelik Bağlantısı - Clash Abonelik Bağlantısı - Sing-Box Abonelik Bağlantısı Benzersiz özelliklerimiz nelerdir? - Kullanıcı Dostu - Optimize Edilmiş ve Hızlı - En Düşük Ping'i otomatik olarak seçin - Kullanıcı kullanım bilgilerini gösterin - Derin bağlantı kullanarak tek tıklamayla alt bağlantıyı kolayca içe aktarın - Ücretsiz ve ADS Yok - Kullanıcı alt bağlantılarını kolayca değiştirin - giderek daha fazla Destek: - Sing-Box tarafından desteklenen tüm Protokoller - VLESS + xtls gerçeklik, vizyon - VMESS - Trojan - ShoadowSocks - Reality - V2ray - Hystria2 - TUIC - SSH - ShadowTLS Kaynak kodu https://github.com/hiddify/Hiddify-Next adresinde mevcuttur. Uygulama çekirdeği açık tabanlıdır. kaynak şarkı kutusu. İzin Açıklaması: - VPN Hizmeti: Bu uygulamanın amacı güvenli, kullanıcı dostu ve verimli bir tünel istemcisi sağlamak olduğundan, trafiği tünel aracılığıyla uzak sunucuya yönlendirebilmek için bu izne ihtiyacımız var. - TÜM PAKETLERİ SORGULAYIN: Bu izin, kullanıcıların tünelleme için belirli uygulamaları dahil etmesine veya hariç tutmasına izin vermek için kullanılır. - ALMA ÖNYÜKLEME TAMAMLANDI: Bu izin, cihaz önyüklemesi sırasında bu uygulamayı etkinleştirmek için uygulama ayarlarından etkinleştirilebilir veya devre dışı bırakılabilir. - BİLDİRİMLER SONRASI: VPN hizmetinin sürekli çalışmasını sağlamak için bir ön plan hizmeti kullandığımız için bu izin önemlidir. - Bu uygulama reklam içermez. Analitik ve kilitlenme verileri yalnızca uygulamanın ilk kullanımında kullanıcının açık rızası ile gerçekleşir." }, "connection": { "tapToConnect": "Bağlanmak için dokunun", diff --git a/assets/translations/strings_zh-CN.i18n.json b/assets/translations/strings_zh-CN.i18n.json index 796795f2..63d44b0a 100644 --- a/assets/translations/strings_zh-CN.i18n.json +++ b/assets/translations/strings_zh-CN.i18n.json @@ -1,6 +1,6 @@ { "general": { - "appTitle": "Hiddify", + "appTitle": "Umbrix", "reset": "重置", "toggle": { "enabled": "启用", @@ -26,7 +26,9 @@ }, "intro": { "termsAndPolicyCaution(rich)": "继续即表示您同意 ${tap(@:about.termsAndConditions)}", - "start": "开始" + "start": "开始", + "welcomeTitle": "欢迎使用 Umbrix", + "subtitle": "快速且安全" }, "home": { "pageTitle": "主页", @@ -127,12 +129,14 @@ } }, "proxies": { - "pageTitle": "代理", + "pageTitle": "位置", "emptyProxiesMsg": "无可用的代理", - "delayTestTooltip": "测试延迟", + "delayTestTooltip": "位置", "sortTooltip": "对代理进行排序", "checkIp": "检测 IP 地址", "unknownIp": "未知的 IP", + "globalAuto": "自动", + "globalAutoDesc": "从所有位置自动选择", "sortOptions": { "unsorted": "默认", "name": "按字母顺序", @@ -187,8 +191,7 @@ "themeModes": { "system": "遵循系统主题", "dark": "暗色", - "light": "浅色", - "black": "黑色" + "light": "浅色" }, "enableAnalytics": "启用分析", "enableAnalyticsMsg": "授予收集分析并发送崩溃报告以改进应用程序的权限", @@ -219,9 +222,28 @@ "exclude": "绕过", "excludeMsg": "不代理选中的应用程序" }, - "showSystemApps": "显示系统应用程序", - "hideSystemApps": "隐藏系统应用程序", - "clearSelection": "清空选项" + "showSystemApps": "显示系统应用", + "hideSystemApps": "隐藏系统应用", + "clearSelection": "清除选择", + "excludedDomains": { + "pageTitle": "排除项", + "domainsTab": "域名", + "appsTab": "应用程序", + "addButton": "添加域名或区域", + "addModalTitle": "+ 添加域名", + "addOwnDomain": "添加到排除", + "domainInputHint": "site.com 或 .com", + "domainInputDescription": "或整个域名区域", + "selectReadyZones": "或选择现成区域:", + "cancel": "取消", + "ok": "确定", + "helpTitle": "已排除域名", + "helpDescription": "此列表中的域名和域名区域将绕过VPN并使用直接连接。", + "helpButton": "明白了", + "emptyState": "无已排除域名", + "emptyStateDescription": "添加应绕过VPN的域名", + "fabButton": "添加" + } }, "geoAssets": { "pageTitle": "路由资源文件", @@ -311,9 +333,9 @@ } }, "play": { - "title": "Hiddify(预览)", + "title": "Umbrix(预览)", "short_description": "自动,SSH, VLESS, Vmess, Trojan, Reality, Sing-Box, Clash, Xray, Shadowsocks", - "full_description": "Hiddify 的主要目标是提供安全、用户友好且高效的隧道客户端。它使您能够利用 VPN 服务权限将所有流量或选定的应用程序流量路由到您选择的远程服务器。\n\n注:我们不提供任何服务器;用户需要使用自己托管的服务器或可信的服务器来确保您在线活动的私密性。\n \n我们支持以下类型的服务器:\n- 普通 V2ray/Xray 订阅链接\n- Clash 订阅链接\n- Sing-Box 订阅链接\n\n我们的特色是什么?\n\n- 用户友好\n- 优化和高速\n- 自动选择最低延迟\n- 显示用户使用信息\n- 通过一键链接轻松导入\n- 免费且无广告\n- 轻松切换线路\n- 等等\n\n支持:\n- Sing-Box 支持的所有协议\n- VLESS + XTLS Reality、Vision 协议\n- VMESS\n- Trojan\n- ShadowSocks\n- Reality\n- WireGuard\n- V2ray\n- Hystria2\n- TUIC\n- SSH\n- ShadowTLS\n\n\n源代码位于 https://github.com/hiddify/Hiddify-Next\n应用程序核心基于开源的 Sing-Box。\n\n权限说明:\n\n- VPN 服务:由于此应用程序的目标是提供安全、用户友好和高效的隧道客户端,我们需要此权限以能够通过隧道将流量路由到远程服务器。\n获取应用程序列表:此权限用于允许用户包括或排除特定应用程序以进行隧道传输。\n- 接收开机广播:可以从应用程序设置中启用或禁用此权限,以便在设备启动时激活此应用程序。\n- 发送通知:此权限是必需的,因为我们使用前台服务来确保 VPN 服务的持续运行。\n- 本应用程序没有广告。分析和崩溃数据仅在首次使用应用程序时经用户明确同意的情况下发生。" + "full_description": "Umbrix 的主要目标是提供安全、用户友好且高效的隧道客户端。它使您能够利用 VPN 服务权限将所有流量或选定的应用程序流量路由到您选择的远程服务器。\n\n注:我们不提供任何服务器;用户需要使用自己托管的服务器或可信的服务器来确保您在线活动的私密性。\n \n我们支持以下类型的服务器:\n- 普通 V2ray/Xray 订阅链接\n- Clash 订阅链接\n- Sing-Box 订阅链接\n\n我们的特色是什么?\n\n- 用户友好\n- 优化和高速\n- 自动选择最低延迟\n- 显示用户使用信息\n- 通过一键链接轻松导入\n- 免费且无广告\n- 轻松切换线路\n- 等等\n\n支持:\n- Sing-Box 支持的所有协议\n- VLESS + XTLS Reality、Vision 协议\n- VMESS\n- Trojan\n- ShadowSocks\n- Reality\n- WireGuard\n- V2ray\n- Hystria2\n- TUIC\n- SSH\n- ShadowTLS\n\n\n应用程序核心基于开源的 Sing-Box。\n\n权限说明:\n\n- VPN 服务:由于此应用程序的目标是提供安全、用户友好和高效的隧道客户端,我们需要此权限以能够通过隧道将流量路由到远程服务器。\n获取应用程序列表:此权限用于允许用户包括或排除特定应用程序以进行隧道传输。\n- 接收开机广播:可以从应用程序设置中启用或禁用此权限,以便在设备启动时激活此应用程序。\n- 发送通知:此权限是必需的,因为我们使用前台服务来确保 VPN 服务的持续运行。\n- 本应用程序没有广告。分析和崩溃数据仅在首次使用应用程序时经用户明确同意的情况下发生。" }, "connection": { "tapToConnect": "点击连接", diff --git a/assets/translations/strings_zh-TW.i18n.json b/assets/translations/strings_zh-TW.i18n.json index 2cb7cb55..8fdaef0e 100644 --- a/assets/translations/strings_zh-TW.i18n.json +++ b/assets/translations/strings_zh-TW.i18n.json @@ -1,6 +1,6 @@ { "general": { - "appTitle": "Hiddify", + "appTitle": "Umbrix", "reset": "重置", "toggle": { "enabled": "啟用", @@ -25,8 +25,12 @@ "grantPermission": "授予權限" }, "intro": { - "termsAndPolicyCaution(rich)": "繼續即表示您同意合約 ${tap(@:about.termsAndConditions)}", - "start": "開始" + "termsAndPolicyCaution(rich)": "繼續即表示您同意 ${tap(@:about.termsAndConditions)}", + "start": "開始", + "welcomeTitle": "歡迎使用 Umbrix", + "subtitle": "簡單。快速。可靠。", + "welcomeTitle": "歡迎使用 Umbrix", + "subtitle": "快速且安全" }, "home": { "pageTitle": "首頁", @@ -129,10 +133,12 @@ "proxies": { "pageTitle": "代理", "emptyProxiesMsg": "沒有可用的代理", - "delayTestTooltip": "測試延遲", + "delayTestTooltip": "位置", "sortTooltip": "對代理進行排序", "checkIp": "檢測 IP 地址", "unknownIp": "不明的 IP", + "globalAuto": "自動", + "globalAutoDesc": "從所有位置自動選擇", "sortOptions": { "unsorted": "預設", "name": "按字母排序", @@ -186,8 +192,7 @@ "themeModes": { "system": "遵循系統主題", "dark": "深色", - "light": "淺色", - "black": "黑色" + "light": "淺色" }, "enableAnalytics": "啟用分析", "enableAnalyticsMsg": "授予收集分析並傳送崩潰報告以改進應用程式的權限", @@ -220,7 +225,26 @@ }, "showSystemApps": "顯示系統應用程式", "hideSystemApps": "隱藏系統應用程式", - "clearSelection": "清空選項" + "clearSelection": "清空選項", + "excludedDomains": { + "pageTitle": "排除項", + "domainsTab": "域名", + "appsTab": "應用程式", + "addButton": "添加域名或區域", + "addModalTitle": "+ 添加域名", + "addOwnDomain": "添加以排除", + "domainInputHint": "site.com 或 .tw", + "domainInputDescription": "或整個域名區域", + "selectReadyZones": "或選擇現成區域:", + "cancel": "取消", + "ok": "確定", + "helpTitle": "排除的域名", + "helpDescription": "此列表中的域名和域名區域將繞過 VPN 並使用直接連接。", + "helpButton": "明白了", + "emptyState": "沒有排除的域名", + "emptyStateDescription": "添加應該繞過 VPN 的域名", + "fabButton": "添加" + } }, "geoAssets": { "pageTitle": "路由資源文件", @@ -310,9 +334,9 @@ } }, "play": { - "title": "Hiddify(預覽)", + "title": "Umbrix(預覽)", "short_description": "自動、SSH、VLESS、Vmess、Trojan、Reality、Sing-Box、Clash、Xray、Shadowsocks", - "full_description": "Hiddify 的主要目標是提供安全、使用者友好且高效率的隧道用戶端。它使您能夠利用 VPN 服務權限將所有流量或選定的應用程式流量路由到您選擇的遠端伺服器。\n\n註:我們不提供任何伺服器;使用者需要使用自己的自託管伺服器或受信任的伺服器來確保其線上活動的隱私。\n\n我們透過以下方式支援伺服器:\n - 普通 V2ray/Xray 訂閱連結\n - Clash 訂閱連結\n - Sing-Box 訂閱連結\n\n 我們的獨特功能是什麼?\n - 使用者友善\n - 最佳化且快速\n - 自動選擇最低延遲\n - 顯示使用者使用資訊\n - 使用一鍵連結輕鬆導入\n - 免費且無廣告\n - 輕鬆切換線路\n - 等等\n 支援:\n - Sing-Box 支援的所有協定 \n - VLESS + XTLS Reality、Vision 協定 \n - VMESS\n - Trojan\n - ShadowSocks\n - Reality\n - WireGuard\n - V2ray\n - Hystria2\n - TUIC \n - SSH\n - ShadowTLS\n\n\n 原始碼位於 https://github.com/hiddify/Hiddify-Next\n 應用程式核心基於開源的 Sing-Box。\n\n權限說明:\n\n - VPN 服務:由於此應用程式的目標是提供安全性、使用者友好且高效的隧道用戶端,因此我們需要此權限才能透過隧道將流量路由到遠端伺服器。\n - 獲取應用程式列表:此權限用於允許使用者包含或排除隧道的特定應用程式。\n - 接收啟動廣播:可以從應用程式設定中啟用或停用此權限,以在裝置啟動時啟動此應用程式。\n - 傳送通知:此權限至關重要,因為我們使用前台服務來確保 VPN 服務的持續運作。\n - 該應用程式沒有廣告。分析和崩潰數據僅在用戶首次使用應用程式時明確同意的情況下才會出現。" + "full_description": "Umbrix 的主要目標是提供安全、使用者友好且高效率的隧道用戶端。它使您能夠利用 VPN 服務權限將所有流量或選定的應用程式流量路由到您選擇的遠端伺服器。\n\n註:我們不提供任何伺服器;使用者需要使用自己的自託管伺服器或受信任的伺服器來確保其線上活動的隱私。\n\n我們透過以下方式支援伺服器:\n - 普通 V2ray/Xray 訂閱連結\n - Clash 訂閱連結\n - Sing-Box 訂閱連結\n\n 我們的獨特功能是什麼?\n - 使用者友善\n - 最佳化且快速\n - 自動選擇最低延遲\n - 顯示使用者使用資訊\n - 使用一鍵連結輕鬆導入\n - 免費且無廣告\n - 輕鬆切換線路\n - 等等\n 支援:\n - Sing-Box 支援的所有協定 \n - VLESS + XTLS Reality、Vision 協定 \n - VMESS\n - Trojan\n - ShadowSocks\n - Reality\n - WireGuard\n - V2ray\n - Hystria2\n - TUIC \n - SSH\n - ShadowTLS\n\n\n 原始碼位於 https://github.com/hiddify/Hiddify-Next\n 應用程式核心基於開源的 Sing-Box。\n\n權限說明:\n\n - VPN 服務:由於此應用程式的目標是提供安全性、使用者友好且高效的隧道用戶端,因此我們需要此權限才能透過隧道將流量路由到遠端伺服器。\n - 獲取應用程式列表:此權限用於允許使用者包含或排除隧道的特定應用程式。\n - 接收啟動廣播:可以從應用程式設定中啟用或停用此權限,以在裝置啟動時啟動此應用程式。\n - 傳送通知:此權限至關重要,因為我們使用前台服務來確保 VPN 服務的持續運作。\n - 該應用程式沒有廣告。分析和崩潰數據僅在用戶首次使用應用程式時明確同意的情況下才會出現。" }, "connection": { "tapToConnect": "點擊以連線", diff --git a/docs/privacy.html b/docs/privacy.html new file mode 100644 index 00000000..b19bc5f5 --- /dev/null +++ b/docs/privacy.html @@ -0,0 +1,63 @@ + + + + + + Политика конфиденциальности - Umbrix + + + +
+

Политика конфиденциальности Umbrix

+

Обновлено: 28 декабря 2025 г.

+ +

1. Сбор данных

+

Umbrix не собирает, не хранит и не передаёт третьим лицам ваши личные данные.

+ +

2. Что мы НЕ собираем

+
    +
  • Историю посещённых сайтов
  • +
  • IP-адреса
  • +
  • DNS-запросы
  • +
  • Информацию о трафике
  • +
  • Личную информацию
  • +
+ +

3. Локальное хранение

+

Приложение хранит настройки и конфигурации только локально на вашем устройстве.

+ +

4. Сторонние сервисы

+

Если вы используете WARP от Cloudflare, применяется их политика конфиденциальности.

+ +

5. Разрешения приложения

+
    +
  • VPN Service: Для маршрутизации трафика через безопасный туннель
  • +
  • Доступ к приложениям: Для выборочного туннелирования приложений
  • +
  • Автозапуск: Для активации при загрузке устройства (опционально)
  • +
  • Уведомления: Для отображения статуса подключения
  • +
+ +

6. Контакты

+

По вопросам конфиденциальности: @umbrix_app

+
+ + diff --git a/docs/terms.html b/docs/terms.html new file mode 100644 index 00000000..eb6b52c6 --- /dev/null +++ b/docs/terms.html @@ -0,0 +1,65 @@ + + + + + + Условия использования - Umbrix + + + +
+

Условия использования Umbrix

+

Обновлено: 28 декабря 2025 г.

+ +

1. Принятие условий

+

Используя Umbrix, вы соглашаетесь с данными условиями.

+ +

2. Описание сервиса

+

Umbrix — это прокси-клиент с открытым исходным кодом для безопасного доступа к интернету.

+ +

3. Ваши обязанности

+
    +
  • Соблюдать законы вашей страны
  • +
  • Не использовать для незаконной деятельности
  • +
  • Предоставить свой собственный прокси-сервер
  • +
+ +

4. Отказ от гарантий

+

Приложение предоставляется "как есть" без гарантий работоспособности.

+ +

5. Серверы

+

Umbrix не предоставляет серверы. Пользователи должны использовать собственные или доверенные серверы.

+ +

6. Ответственность

+

Разработчики не несут ответственности за использование приложения.

+ +

7. Открытый исходный код

+

Umbrix основан на Sing-Box и распространяется под открытой лицензией.

+ +

8. Изменения

+

Условия могут быть изменены без предварительного уведомления.

+ +

9. Контакты

+

Вопросы и поддержка: @umbrix_app

+
+ + diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index ad7d1b17..ef3568a3 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart'; -import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:hiddify/core/analytics/analytics_controller.dart'; import 'package:hiddify/core/app_info/app_info_provider.dart'; import 'package:hiddify/core/directories/directories_provider.dart'; @@ -16,8 +15,8 @@ import 'package:hiddify/core/preferences/preferences_migration.dart'; import 'package:hiddify/core/preferences/preferences_provider.dart'; import 'package:hiddify/features/app/widget/app.dart'; import 'package:hiddify/features/auto_start/notifier/auto_start_notifier.dart'; +import 'package:hiddify/features/common/custom_splash_screen.dart'; import 'package:hiddify/features/deep_link/notifier/deep_link_notifier.dart'; - import 'package:hiddify/features/log/data/log_data_providers.dart'; import 'package:hiddify/features/profile/data/profile_data_providers.dart'; import 'package:hiddify/features/profile/notifier/active_profile_notifier.dart'; @@ -32,8 +31,6 @@ Future lazyBootstrap( WidgetsBinding widgetsBinding, Environment env, ) async { - FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); - LoggerController.preInit(); FlutterError.onError = Logger.logFlutterError; WidgetsBinding.instance.platformDispatcher.onError = Logger.logPlatformDispatcherError; @@ -46,6 +43,24 @@ Future lazyBootstrap( ], ); + // Показываем кастомный splash screen с круговым индикатором + runApp( + MaterialApp( + debugShowCheckedModeBanner: false, + home: CustomSplashScreen( + onInitializationComplete: () async { + await _performBootstrap(container, stopWatch, env); + }, + ), + ), + ); +} + +Future _performBootstrap( + ProviderContainer container, + Stopwatch stopWatch, + Environment env, +) async { await _init( "directories", () => container.read(appDirectoriesProvider.future), @@ -160,8 +175,6 @@ Future lazyBootstrap( ), ), ); - - FlutterNativeSplash.remove(); } Future _init( diff --git a/lib/core/analytics/analytics_controller.dart b/lib/core/analytics/analytics_controller.dart index a27e6498..4d3c3967 100644 --- a/lib/core/analytics/analytics_controller.dart +++ b/lib/core/analytics/analytics_controller.dart @@ -20,7 +20,8 @@ bool _testCrashReport = false; class AnalyticsController extends _$AnalyticsController with AppLogger { @override Future build() async { - return _preferences.getBool(enableAnalyticsPrefKey) ?? true; + // UMBRIX: По умолчанию ВЫКЛЮЧЕНО для приватности + return _preferences.getBool(enableAnalyticsPrefKey) ?? false; } SharedPreferences get _preferences => ref.read(sharedPreferencesProvider).requireValue; @@ -45,13 +46,21 @@ class AnalyticsController extends _$AnalyticsController with AppLogger { options.environment = env.name; options.dist = appInfo.release.name; options.debug = kDebugMode; + + // UMBRIX: Только fatal crashes, никакой аналитики options.enableNativeCrashHandling = true; options.enableNdkScopeSync = true; - // options.attachScreenshot = true; + + // UMBRIX: Отключаем всю трассировку и слежку + options.tracesSampleRate = 0.0; // Было 0.20 + options.enableUserInteractionTracing = false; // Было true + options.enableAutoPerformanceTracing = false; + options.attachScreenshot = false; + options.attachViewHierarchy = false; + + // UMBRIX: Максимальная анонимизация options.serverName = ""; - options.attachThreads = true; - options.tracesSampleRate = 0.20; - options.enableUserInteractionTracing = true; + options.attachThreads = false; // Было true options.addIntegration(sentryLogger); options.beforeSend = sentryBeforeSend; }, diff --git a/lib/core/haptic/haptic_service.dart b/lib/core/haptic/haptic_service.dart index 99e8d39a..93067e78 100644 --- a/lib/core/haptic/haptic_service.dart +++ b/lib/core/haptic/haptic_service.dart @@ -13,8 +13,7 @@ class HapticService extends _$HapticService { } static const String hapticFeedbackPrefKey = "haptic_feedback"; - SharedPreferences get _preferences => - ref.read(sharedPreferencesProvider).requireValue; + SharedPreferences get _preferences => ref.read(sharedPreferencesProvider).requireValue; Future updatePreference(bool value) async { state = value; diff --git a/lib/core/http_client/dio_http_client.dart b/lib/core/http_client/dio_http_client.dart index 90a106a5..d997d493 100644 --- a/lib/core/http_client/dio_http_client.dart +++ b/lib/core/http_client/dio_http_client.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:dio/io.dart'; import 'package:dio_smart_retry/dio_smart_retry.dart'; -import 'package:flutter_loggy_dio/flutter_loggy_dio.dart'; import 'package:hiddify/utils/custom_loggers.dart'; class DioHttpClient with InfraLogger { @@ -14,7 +13,7 @@ class DioHttpClient with InfraLogger { required String userAgent, required bool debug, }) { - for (var mode in ["proxy", "direct", "both"]) { + for (final mode in ["proxy", "direct", "both"]) { _dio[mode] = Dio( BaseOptions( connectTimeout: timeout, @@ -31,7 +30,7 @@ class DioHttpClient with InfraLogger { if (mode != "proxy") ...[ const Duration(seconds: 2), const Duration(seconds: 3), - ] + ], ], ), ); diff --git a/lib/core/model/constants.dart b/lib/core/model/constants.dart index 08108b31..2e8bb435 100644 --- a/lib/core/model/constants.dart +++ b/lib/core/model/constants.dart @@ -1,19 +1,14 @@ abstract class Constants { - static const appName = "Hiddify"; - static const githubUrl = "https://github.com/hiddify/hiddify-next"; - static const githubReleasesApiUrl = - "https://api.github.com/repos/hiddify/hiddify-next/releases"; - static const githubLatestReleaseUrl = - "https://github.com/hiddify/hiddify-next/releases/latest"; - static const appCastUrl = - "https://raw.githubusercontent.com/hiddify/hiddify-next/main/appcast.xml"; - static const telegramChannelUrl = "https://t.me/hiddify"; - static const privacyPolicyUrl = "https://hiddify.com/privacy-policy/"; - static const termsAndConditionsUrl = "https://hiddify.com/terms/"; - static const cfWarpPrivacyPolicy = - "https://www.cloudflare.com/application/privacypolicy/"; - static const cfWarpTermsOfService = - "https://www.cloudflare.com/application/terms/"; + static const appName = "Umbrix"; + static const githubUrl = "https://github.com/umbrix-app/umbrix"; + static const githubReleasesApiUrl = "https://api.github.com/repos/umbrix-app/umbrix/releases"; + static const githubLatestReleaseUrl = "https://github.com/umbrix-app/umbrix/releases/latest"; + static const appCastUrl = "https://raw.githubusercontent.com/umbrix-app/umbrix/main/appcast.xml"; + static const telegramChannelUrl = "https://t.me/umbrix_app"; + static const privacyPolicyUrl = "https://umbrix.net/privacy.html"; + static const termsAndConditionsUrl = "https://umbrix.net/terms.html"; + static const cfWarpPrivacyPolicy = "https://www.cloudflare.com/application/privacypolicy/"; + static const cfWarpTermsOfService = "https://www.cloudflare.com/application/terms/"; } const kAnimationDuration = Duration(milliseconds: 250); diff --git a/lib/core/model/region.dart b/lib/core/model/region.dart index cee54e9f..3b9bd669 100644 --- a/lib/core/model/region.dart +++ b/lib/core/model/region.dart @@ -8,6 +8,7 @@ enum Region { id, tr, br, + in_, // India other; String present(TranslationsEn t) => switch (this) { @@ -18,6 +19,7 @@ enum Region { af => t.settings.general.regions.af, id => t.settings.general.regions.id, br => t.settings.general.regions.br, + in_ => "India", // Добавим в переводы позже other => t.settings.general.regions.other, }; } diff --git a/lib/core/model/secrets.dart b/lib/core/model/secrets.dart new file mode 100644 index 00000000..0b3a76cf --- /dev/null +++ b/lib/core/model/secrets.dart @@ -0,0 +1,30 @@ +/// UMBRIX: Секретные ключи для Telegram Bot (отправка логов) +/// +/// ⚠️ ВАЖНО: НЕ КОММИТИТЬ ЭТОТ ФАЙЛ В GIT! +/// Добавьте в .gitignore: lib/core/model/secrets.dart +/// +/// Инструкция по получению токена: +/// 1. Откройте Telegram, найдите @BotFather +/// 2. Отправьте команду /newbot +/// 3. Следуйте инструкциям, придумайте имя (например, UmbrixLogsBot) +/// 4. Скопируйте токен и вставьте ниже +/// 5. Отправьте команду /mybots → выберите бота → Bot Settings → Group Privacy → Turn OFF +/// 6. Добавьте бота в свою группу/канал +/// 7. Отправьте любое сообщение в группу +/// 8. Откройте: https://api.telegram.org/bot/getUpdates +/// 9. Найдите "chat":{"id":-100XXXXXXXXX} и скопируйте этот ID +library; + +abstract class Secrets { + /// Токен Telegram бота для отправки логов + /// Получить: @BotFather в Telegram → /newbot + static const String telegramBotToken = ""; // ← ВСТАВЬТЕ СЮДА ВАШ ТОКЕН + + /// ID чата/канала куда отправлять логи + /// Формат: -100XXXXXXXXX для каналов/групп + /// Или просто число для личных сообщений + static const String telegramChatId = ""; // ← ВСТАВЬТЕ СЮДА ID ЧАТА + + /// Проверка что токены настроены + static bool get isConfigured => telegramBotToken.isNotEmpty && telegramChatId.isNotEmpty; +} diff --git a/lib/core/preferences/general_preferences.dart b/lib/core/preferences/general_preferences.dart index 34926b42..086bf57a 100644 --- a/lib/core/preferences/general_preferences.dart +++ b/lib/core/preferences/general_preferences.dart @@ -5,7 +5,10 @@ import 'package:hiddify/core/preferences/actions_at_closing.dart'; // import 'package:hiddify/core/model/region.dart'; import 'package:hiddify/core/preferences/preferences_provider.dart'; import 'package:hiddify/core/utils/preferences_utils.dart'; +import 'package:hiddify/features/connection/model/connection_status.dart'; +import 'package:hiddify/features/connection/notifier/connection_notifier.dart'; import 'package:hiddify/features/per_app_proxy/model/per_app_proxy_mode.dart'; +import 'package:hiddify/features/profile/notifier/active_profile_notifier.dart'; import 'package:hiddify/utils/platform_utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -33,7 +36,7 @@ abstract class Preferences { static final perAppProxyMode = PreferencesNotifier.create( "per_app_proxy_mode", - PerAppProxyMode.off, + PerAppProxyMode.exclude, mapFrom: PerAppProxyMode.values.byName, mapTo: (value) => value.name, ); @@ -103,14 +106,45 @@ class PerAppProxyList extends _$PerAppProxyList { ); @override - List build() => ref.watch(Preferences.perAppProxyMode) == PerAppProxyMode.include ? _include.read() : _exclude.read(); + List build() { + // Слушаем изменения режима и перестраиваем список + final mode = ref.watch(Preferences.perAppProxyMode); + return mode == PerAppProxyMode.include ? _include.read() : _exclude.read(); + } - Future update(List value) { - state = value; - if (ref.read(Preferences.perAppProxyMode) == PerAppProxyMode.include) { - return _include.write(value); + Future update(List value) async { + print('[PerAppProxyList] update() вызван с ${value.length} приложениями'); + final mode = ref.read(Preferences.perAppProxyMode); + print('[PerAppProxyList] Текущий режим: $mode'); + + // Сначала сохраняем в SharedPreferences + if (mode == PerAppProxyMode.include) { + await _include.write(value); + print('[PerAppProxyList] Записан include список: $value'); + } else { + await _exclude.write(value); + print('[PerAppProxyList] Записан exclude список: $value'); + } + + // Затем обновляем локальное состояние + state = value; + print('[PerAppProxyList] State обновлён'); + + // Автоматически перезапускаем VPN если он активен + print('[PerAppProxyList] Вызываю _reconnectVpnIfActive()'); + await _reconnectVpnIfActive(); + } + + Future _reconnectVpnIfActive() async { + try { + final connectionNotifier = await ref.read(connectionNotifierProvider.future); + if (connectionNotifier is Connected) { + final profile = await ref.read(activeProfileProvider.future); + await ref.read(connectionNotifierProvider.notifier).reconnect(profile); + } + } catch (_) { + // Игнорируем ошибки если connection provider не инициализирован } - return _exclude.write(value); } } @@ -125,8 +159,22 @@ class ExcludedDomainsList extends _$ExcludedDomainsList { @override List build() => _pref.read(); - Future update(List value) { + Future update(List value) async { state = value; - return _pref.write(value); + await _pref.write(value); + // Автоматически перезапускаем VPN если он активен + await _reconnectVpnIfActive(); + } + + Future _reconnectVpnIfActive() async { + try { + final connectionNotifier = await ref.read(connectionNotifierProvider.future); + if (connectionNotifier is Connected) { + final profile = await ref.read(activeProfileProvider.future); + await ref.read(connectionNotifierProvider.notifier).reconnect(profile); + } + } catch (_) { + // Игнорируем ошибки если connection provider не инициализирован + } } } diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index 34a208dd..88cddedd 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -55,7 +55,6 @@ final tabLocations = [ const PerAppProxyRoute().location, const ConfigOptionsRoute().location, const SettingsRoute().location, - const LogsOverviewRoute().location, const AboutRoute().location, ]; diff --git a/lib/core/router/routes.dart b/lib/core/router/routes.dart index d60713c3..f981328e 100644 --- a/lib/core/router/routes.dart +++ b/lib/core/router/routes.dart @@ -319,14 +319,14 @@ class ConfigOptionsRoute extends GoRouteData { @override Page buildPage(BuildContext context, GoRouterState state) { if (useMobileRouter) { - return MaterialPage( + return const MaterialPage( name: name, - child: ConfigOptionsPage(section: section), + child: ConfigOptionsPage(), ); } - return NoTransitionPage( + return const NoTransitionPage( name: name, - child: ConfigOptionsPage(section: section), + child: ConfigOptionsPage(), ); } } diff --git a/lib/core/telegram/telegram_logger.dart b/lib/core/telegram/telegram_logger.dart new file mode 100644 index 00000000..7c26fe3b --- /dev/null +++ b/lib/core/telegram/telegram_logger.dart @@ -0,0 +1,116 @@ +import 'dart:io'; +import 'package:dio/dio.dart'; +import 'package:hiddify/core/telegram_config.dart'; +import 'package:hiddify/utils/utils.dart'; + +/// Сервис для анонимной отправки логов в Telegram +class TelegramLogger with InfraLogger { + static const int maxLogSize = 4096; // Telegram ограничение на текст сообщения + static const int maxFileSize = 50 * 1024 * 1024; // 50MB максимум для файла + + final Dio _dio = Dio( + BaseOptions( + connectTimeout: const Duration(seconds: 10), + receiveTimeout: const Duration(seconds: 10), + ), + ); + + /// Отправить логи как текстовое сообщение (для коротких логов) + Future sendLogsAsText(String logs, {String? deviceInfo}) async { + if (!TelegramConfig.isConfigured) { + loggy.warning('Telegram not configured'); + return false; + } + + try { + final message = _formatMessage(logs, deviceInfo); + + final response = await _dio.post( + 'https://api.telegram.org/bot${TelegramConfig.botToken}/sendMessage', + data: { + 'chat_id': TelegramConfig.chatId, + 'text': message, + 'parse_mode': 'HTML', + }, + ); + + return response.statusCode == 200; + } catch (e) { + loggy.error('Failed to send logs to Telegram', e); + return false; + } + } + + /// Отправить файл логов (для больших логов) + Future sendLogsAsFile(File logFile, {String? deviceInfo}) async { + if (!TelegramConfig.isConfigured) { + loggy.warning('Telegram not configured'); + return false; + } + + try { + // Проверка размера файла + final fileSize = await logFile.length(); + if (fileSize > maxFileSize) { + loggy.warning('Log file too large: $fileSize bytes'); + return false; + } + + final fileName = logFile.path.split('/').last; + final caption = deviceInfo != null ? '📱 Device: $deviceInfo\n📅 ${DateTime.now().toIso8601String()}' : '📅 ${DateTime.now().toIso8601String()}'; + + final formData = FormData.fromMap({ + 'chat_id': TelegramConfig.chatId, + 'document': await MultipartFile.fromFile( + logFile.path, + filename: fileName, + ), + 'caption': caption, + }); + + final response = await _dio.post( + 'https://api.telegram.org/bot${TelegramConfig.botToken}/sendDocument', + data: formData, + ); + + return response.statusCode == 200; + } catch (e) { + loggy.error('Failed to send log file to Telegram', e); + return false; + } + } + + String _formatMessage(String logs, String? deviceInfo) { + final header = deviceInfo != null ? '📱 Umbrix Logs\nDevice: $deviceInfo\n' : '📱 Umbrix Logs\n'; + + final timestamp = '📅 ${DateTime.now().toIso8601String()}\n'; + const separator = '━━━━━━━━━━━━━━━━\n'; + + // Обрезаем логи если они слишком длинные + var logContent = logs; + final maxContentSize = maxLogSize - header.length - timestamp.length - separator.length - 50; + + if (logContent.length > maxContentSize) { + logContent = '${logContent.substring(0, maxContentSize)}\n\n... (truncated)'; + } + + return '$header$timestamp$separator$logContent'; + } + + /// Получить информацию об устройстве для логов (анонимно) + static String getAnonymousDeviceInfo() { + // Только общая информация без идентификаторов + if (Platform.isAndroid) { + return 'Android ${Platform.operatingSystemVersion}'; + } else if (Platform.isIOS) { + return 'iOS ${Platform.operatingSystemVersion}'; + } else if (Platform.isWindows) { + return 'Windows'; + } else if (Platform.isMacOS) { + return 'macOS'; + } else if (Platform.isLinux) { + return 'Linux'; + } + return 'Unknown OS'; + } +} diff --git a/lib/core/telegram_config.dart b/lib/core/telegram_config.dart new file mode 100644 index 00000000..f0fcc794 --- /dev/null +++ b/lib/core/telegram_config.dart @@ -0,0 +1,20 @@ +/// ⚠️ НЕ КОММИТЬТЕ ЭТОТ ФАЙЛ В GIT! +/// Замените значения ниже на реальные данные вашего Telegram бота +/// +/// Инструкция по настройке: см. TELEGRAM_SETUP.md +library; + +class TelegramConfig { + /// Токен вашего Telegram бота от @BotFather + /// Пример: '1234567890:ABCdefGHIjklMNOpqrsTUVwxyz123456789' + static const String botToken = 'YOUR_BOT_TOKEN_HERE'; + + /// Chat ID группы/канала куда отправлять логи + /// Пример: '-1001234567890' + static const String chatId = 'YOUR_CHAT_ID_HERE'; + + /// Проверка что конфиг настроен + static bool get isConfigured { + return botToken != 'YOUR_BOT_TOKEN_HERE' && chatId != 'YOUR_CHAT_ID_HERE'; + } +} diff --git a/lib/core/telegram_config.dart.example b/lib/core/telegram_config.dart.example new file mode 100644 index 00000000..fbc6371a --- /dev/null +++ b/lib/core/telegram_config.dart.example @@ -0,0 +1,39 @@ +// 🔐 ИНСТРУКЦИЯ ПО НАСТРОЙКЕ TELEGRAM БОТА +// +// 1. Создайте Telegram бота через @BotFather: +// - Откройте Telegram и найдите @BotFather +// - Отправьте команду: /newbot +// - Введите имя бота: Umbrix Log Bot +// - Введите username: @umbrix_logs_bot (или любой свободный) +// - Скопируйте TOKEN который выдаст BotFather +// +// 2. Получите CHAT_ID: +// - Создайте приватную группу/канал для логов +// - Добавьте туда бота +// - Отправьте любое сообщение в группу +// - Откройте: https://api.telegram.org/bot/getUpdates +// - Найдите "chat":{"id":-1001234567890} - это ваш CHAT_ID +// +// 3. Скопируйте этот файл в telegram_config.dart: +// cp lib/core/telegram_config.dart.example lib/core/telegram_config.dart +// +// 4. Вставьте свои данные ниже +// +// 5. Добавьте в .gitignore (чтобы не попало в репозиторий): +// lib/core/telegram_config.dart +// + +/// ⚠️ НЕ КОММИТЬТЕ ЭТОТ ФАЙЛ В GIT! +class TelegramConfig { + /// Токен вашего Telegram бота от @BotFather + static const String botToken = 'YOUR_BOT_TOKEN_HERE'; + + /// ID чата/канала куда отправлять логи (с минусом для групп) + static const String chatId = 'YOUR_CHAT_ID_HERE'; + + /// URL Telegram Bot API + static const String apiUrl = 'https://api.telegram.org'; + + /// Максимальный размер одного сообщения (4096 символов - лимит Telegram) + static const int maxMessageLength = 4000; +} diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart index 091a9744..cb2a1025 100644 --- a/lib/core/theme/app_theme.dart +++ b/lib/core/theme/app_theme.dart @@ -20,15 +20,92 @@ class AppTheme { } ThemeData darkTheme(ColorScheme? darkColorScheme) { - final ColorScheme scheme = darkColorScheme ?? - ColorScheme.fromSeed( - seedColor: const Color(0xFF293CA0), - brightness: Brightness.dark, - ); + // Кастомная темная тема с хорошей контрастностью для блоков + const ColorScheme scheme = ColorScheme( + brightness: Brightness.dark, + // Основной цвет Outline - бирюзовый + primary: Color(0xFF2fbea5), // hsl(170, 60%, 46%) + onPrimary: Color(0xFFFFFFFF), // Белый текст на кнопках + primaryContainer: Color(0xFF005048), + onPrimaryContainer: Color(0xFF7df8dd), + // Фон карточек/блоков - заметно светлее фона приложения + surface: Color(0xFF263238), // Светло-серый для карточек + onSurface: Color(0xFFE1E2E6), // Светлый текст на карточках + surfaceContainerHighest: Color(0xFF37474F), // Еще светлее для выделенных элементов + // Дополнительные цвета + secondary: Color(0xFF2fbea5), + onSecondary: Color(0xFFFFFFFF), // Белый текст + secondaryContainer: Color(0xFF005048), + onSecondaryContainer: Color(0xFF7df8dd), + // Ошибки + error: Color(0xFFf44336), + onError: Color(0xFFFFFFFF), + errorContainer: Color(0xFF93000a), + onErrorContainer: Color(0xFFffdad6), + // Контуры и границы - видимые + outline: Color(0xFF4CAF50), // Зеленоватый контур для видимости + outlineVariant: Color(0xFF546E7A), + ); + return ThemeData( useMaterial3: true, colorScheme: scheme, - scaffoldBackgroundColor: mode.trueBlack ? Colors.black : scheme.background, + scaffoldBackgroundColor: const Color(0xFF191f23), // Очень темный фон + cardTheme: CardTheme( + elevation: 4, + shadowColor: Colors.black45, + surfaceTintColor: const Color(0xFF2fbea5), // Легкий бирюзовый оттенок + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: const BorderSide( + color: Color(0xFF37474F), // Видимая граница карточки + ), + ), + ), + // Настройка текста кнопок + textTheme: const TextTheme( + labelLarge: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ), + filledButtonTheme: FilledButtonThemeData( + style: ButtonStyle( + foregroundColor: WidgetStateProperty.all(const Color(0xFFFFFFFF)), // Белый текст + backgroundColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.disabled)) { + return const Color(0xFF263238).withOpacity(0.5); + } + return const Color(0xFF263238); // Темно-серый фон + }), + elevation: WidgetStateProperty.all(4), + shadowColor: WidgetStateProperty.all(Colors.black45), + padding: WidgetStateProperty.all( + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + textStyle: WidgetStateProperty.all( + const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ), + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + foregroundColor: const Color(0xFF2fbea5), // Бирюзовый текст + side: const BorderSide(color: Color(0xFF2fbea5), width: 1.5), + textStyle: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ), + ), fontFamily: fontFamily, extensions: const >{ ConnectionButtonTheme.light, diff --git a/lib/core/theme/app_theme_mode.dart b/lib/core/theme/app_theme_mode.dart index 5696f70c..c5b5e57b 100644 --- a/lib/core/theme/app_theme_mode.dart +++ b/lib/core/theme/app_theme_mode.dart @@ -4,22 +4,17 @@ import 'package:hiddify/core/localization/translations.dart'; enum AppThemeMode { system, light, - dark, - black; + dark; String present(TranslationsEn t) => switch (this) { system => t.settings.general.themeModes.system, light => t.settings.general.themeModes.light, dark => t.settings.general.themeModes.dark, - black => t.settings.general.themeModes.black, }; ThemeMode get flutterThemeMode => switch (this) { system => ThemeMode.system, light => ThemeMode.light, dark => ThemeMode.dark, - black => ThemeMode.dark, }; - - bool get trueBlack => this == black; } diff --git a/lib/core/theme/theme_preferences.dart b/lib/core/theme/theme_preferences.dart index 109aadb3..eb739263 100644 --- a/lib/core/theme/theme_preferences.dart +++ b/lib/core/theme/theme_preferences.dart @@ -9,7 +9,8 @@ class ThemePreferences extends _$ThemePreferences { @override AppThemeMode build() { final persisted = ref.watch(sharedPreferencesProvider).requireValue.getString("theme_mode"); - if (persisted == null) return AppThemeMode.system; + // UMBRIX: Темная тема по умолчанию + if (persisted == null) return AppThemeMode.dark; return AppThemeMode.values.byName(persisted); } diff --git a/lib/core/utils/preferences_utils.dart b/lib/core/utils/preferences_utils.dart index 5edfc193..d2e948c6 100644 --- a/lib/core/utils/preferences_utils.dart +++ b/lib/core/utils/preferences_utils.dart @@ -106,7 +106,7 @@ class PreferencesNotifier extends StateNotifier { final List? possibleValues; static StateNotifierProvider, T> create(String key, T defaultValue, - {T Function(Ref ref)? defaultValueFunction, T Function(P value)? mapFrom, P Function(T value)? mapTo, bool Function(T value)? validator, T? overrideValue, List? possibleValues}) => + {T Function(Ref ref)? defaultValueFunction, T Function(P value)? mapFrom, P Function(T value)? mapTo, bool Function(T value)? validator, T? overrideValue, List? possibleValues,}) => StateNotifierProvider( (ref) => PreferencesNotifier._( ref: ref, @@ -119,7 +119,7 @@ class PreferencesNotifier extends StateNotifier { validator: validator, ), overrideValue: overrideValue, - possibleValues: possibleValues), + possibleValues: possibleValues,), ); static AutoDisposeStateNotifierProvider, T> createAutoDispose( diff --git a/lib/core/widget/custom_alert_dialog.dart b/lib/core/widget/custom_alert_dialog.dart index c3ca3f27..b54b7730 100644 --- a/lib/core/widget/custom_alert_dialog.dart +++ b/lib/core/widget/custom_alert_dialog.dart @@ -20,7 +20,6 @@ class CustomAlertDialog extends StatelessWidget { Future show(BuildContext context) async { await showDialog( context: context, - useRootNavigator: true, builder: (context) => this, ); } diff --git a/lib/features/app/widget/app.dart b/lib/features/app/widget/app.dart index 202aca4b..6ad4891c 100644 --- a/lib/features/app/widget/app.dart +++ b/lib/features/app/widget/app.dart @@ -1,5 +1,4 @@ import 'package:accessibility_tools/accessibility_tools.dart'; -import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; @@ -18,7 +17,6 @@ import 'package:hiddify/features/system_tray/widget/system_tray_wrapper.dart'; import 'package:hiddify/features/window/widget/window_wrapper.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:upgrader/upgrader.dart'; bool _debugAccessibility = false; @@ -40,33 +38,26 @@ class App extends HookConsumerWidget with PresLogger { TrayWrapper( ShortcutWrapper( ConnectionWrapper( - DynamicColorBuilder( - builder: (ColorScheme? lightColorScheme, ColorScheme? darkColorScheme) { - return MaterialApp.router( - routerConfig: router, - locale: locale.flutterLocale, - supportedLocales: AppLocaleUtils.supportedLocales, - localizationsDelegates: GlobalMaterialLocalizations.delegates, - debugShowCheckedModeBanner: false, - themeMode: themeMode.flutterThemeMode, - theme: theme.lightTheme(lightColorScheme), - darkTheme: theme.darkTheme(darkColorScheme), - title: Constants.appName, - builder: (context, child) { - child = UpgradeAlert( - upgrader: upgrader, - navigatorKey: router.routerDelegate.navigatorKey, - child: child ?? const SizedBox(), - ); - if (kDebugMode && _debugAccessibility) { - return AccessibilityTools( - checkFontOverflows: true, - child: child, - ); - } - return child; - }, - ); + MaterialApp.router( + routerConfig: router, + locale: locale.flutterLocale, + supportedLocales: AppLocaleUtils.supportedLocales, + localizationsDelegates: GlobalMaterialLocalizations.delegates, + debugShowCheckedModeBanner: false, + themeMode: themeMode.flutterThemeMode, + theme: theme.lightTheme(null), + darkTheme: theme.darkTheme(null), + title: Constants.appName, + builder: (context, child) { + // UMBRIX: Отключили UpgradeAlert - используем свой сервер обновлений + child = child ?? const SizedBox(); + if (kDebugMode && _debugAccessibility) { + return AccessibilityTools( + checkFontOverflows: true, + child: child, + ); + } + return child; }, ), ), diff --git a/lib/features/app_update/notifier/app_update_notifier.dart b/lib/features/app_update/notifier/app_update_notifier.dart index d59330d5..a3b9b97c 100644 --- a/lib/features/app_update/notifier/app_update_notifier.dart +++ b/lib/features/app_update/notifier/app_update_notifier.dart @@ -1,7 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:hiddify/core/app_info/app_info_provider.dart'; import 'package:hiddify/core/localization/locale_preferences.dart'; -import 'package:hiddify/core/model/constants.dart'; import 'package:hiddify/core/preferences/preferences_provider.dart'; import 'package:hiddify/core/utils/preferences_utils.dart'; import 'package:hiddify/features/app_update/data/app_update_data_providers.dart'; @@ -19,7 +18,7 @@ const _debugUpgrader = true; @riverpod Upgrader upgrader(UpgraderRef ref) => Upgrader( - appcastConfig: AppcastConfiguration(url: Constants.appCastUrl), + // Removed appcastConfig - no updates for Umbrix debugLogging: _debugUpgrader && kDebugMode, durationUntilAlertAgain: const Duration(hours: 12), messages: UpgraderMessages( diff --git a/lib/features/app_update/widget/new_version_dialog.dart b/lib/features/app_update/widget/new_version_dialog.dart index e636fd8c..f5390278 100644 --- a/lib/features/app_update/widget/new_version_dialog.dart +++ b/lib/features/app_update/widget/new_version_dialog.dart @@ -24,7 +24,6 @@ class NewVersionDialog extends HookConsumerWidget with PresLogger { if (_dialogKey.currentContext == null) { return showDialog( context: context, - useRootNavigator: true, builder: (context) => this, ); } else { diff --git a/lib/features/common/adaptive_root_scaffold.dart b/lib/features/common/adaptive_root_scaffold.dart index f5b2c4a9..35ff7b56 100644 --- a/lib/features/common/adaptive_root_scaffold.dart +++ b/lib/features/common/adaptive_root_scaffold.dart @@ -5,7 +5,12 @@ import 'package:hiddify/gen/assets.gen.dart'; import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/router/router.dart'; import 'package:hiddify/features/stats/widget/side_bar_stats_overview.dart'; +import 'package:hiddify/core/router/routes.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:hiddify/core/theme/theme_preferences.dart'; +import 'package:hiddify/core/theme/app_theme_mode.dart'; +import 'package:hiddify/core/localization/locale_preferences.dart'; +import 'package:hiddify/core/localization/locale_extensions.dart'; abstract interface class RootScaffold { static final stateKey = GlobalKey(); @@ -40,20 +45,13 @@ class AdaptiveRootScaffold extends HookConsumerWidget { 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, - ), NavigationDestination( icon: const Icon(FluentIcons.settings_20_filled), label: t.settings.pageTitle, ), NavigationDestination( - icon: const Icon(FluentIcons.document_text_20_filled), - label: t.logs.pageTitle, - ), - NavigationDestination( - icon: const Icon(FluentIcons.info_20_filled), + icon: const Icon(FluentIcons.info_20_regular), + selectedIcon: const Icon(FluentIcons.info_20_filled), label: t.about.pageTitle, ), ]; @@ -150,28 +148,43 @@ class _CustomAdaptiveScaffold extends HookConsumerWidget { 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]; + // О программе + Builder( + builder: (context) { + final t = ref.watch(translationsProvider); 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), + icon: FluentIcons.info_24_regular, + selectedIcon: FluentIcons.info_24_filled, + label: t.about.pageTitle, + isSelected: false, + onTap: () { + RootScaffold.stateKey.currentState?.closeDrawer(); + const AboutRoute().push(context); + }, ); }, ), + // Настройки + Builder( + builder: (context) { + final t = ref.watch(translationsProvider); + return _DrawerMenuItem( + icon: FluentIcons.settings_24_regular, + selectedIcon: FluentIcons.settings_24_filled, + label: t.settings.pageTitle, + isSelected: false, + onTap: () { + RootScaffold.stateKey.currentState?.closeDrawer(); + const SettingsRoute().push(context); + }, + ); + }, + ), + const SizedBox(height: 16), + const Divider(), + const _DrawerThemeItem(), + const _DrawerLanguageItem(), + const _DrawerLicensesItem(), ], ), ), @@ -267,3 +280,93 @@ class _DrawerMenuItem extends StatelessWidget { ); } } + +// Виджет для выбора темы в боковом меню +class _DrawerThemeItem extends ConsumerWidget { + const _DrawerThemeItem(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + final themeMode = ref.watch(themePreferencesProvider); + + return ListTile( + leading: const Icon(FluentIcons.weather_moon_20_regular, size: 24), + title: Text(t.settings.general.themeMode), + subtitle: Text(themeMode.present(t)), + onTap: () async { + final selectedThemeMode = await showDialog( + context: context, + builder: (context) => SimpleDialog( + title: Text(t.settings.general.themeMode), + children: AppThemeMode.values + .map((e) => RadioListTile( + title: Text(e.present(t)), + value: e, + groupValue: themeMode, + onChanged: Navigator.of(context).maybePop, + )) + .toList(), + ), + ); + if (selectedThemeMode != null) { + await ref.read(themePreferencesProvider.notifier).changeThemeMode(selectedThemeMode); + } + }, + ); + } +} + +// Виджет для выбора языка в боковом меню +class _DrawerLanguageItem extends ConsumerWidget { + const _DrawerLanguageItem(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + final locale = ref.watch(localePreferencesProvider); + + return ListTile( + leading: const Icon(FluentIcons.local_language_24_regular, size: 24), + title: Text(t.settings.general.locale), + subtitle: Text(locale.localeName), + onTap: () async { + final selectedLocale = await showDialog( + context: context, + builder: (context) => SimpleDialog( + title: Text(t.settings.general.locale), + children: AppLocale.values + .map((e) => RadioListTile( + title: Text(e.localeName), + value: e, + groupValue: locale, + onChanged: Navigator.of(context).maybePop, + )) + .toList(), + ), + ); + if (selectedLocale != null) { + await ref.read(localePreferencesProvider.notifier).changeLocale(selectedLocale); + } + }, + ); + } +} + +// Виджет для открытия лицензий +class _DrawerLicensesItem extends ConsumerWidget { + const _DrawerLicensesItem(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + + return ListTile( + leading: const Icon(FluentIcons.document_text_20_regular, size: 24), + title: Text(MaterialLocalizations.of(context).licensesPageTitle), + onTap: () { + showLicensePage(context: context); + }, + ); + } +} diff --git a/lib/features/common/adaptive_root_scaffold.dart.bak b/lib/features/common/adaptive_root_scaffold.dart.bak new file mode 100644 index 00000000..0c2f4bea --- /dev/null +++ b/lib/features/common/adaptive_root_scaffold.dart.bak @@ -0,0 +1,368 @@ +import 'package:fluentui_system_icons/fluentui_system_icons.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart'; +import 'package:hiddify/gen/assets.gen.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/router/router.dart'; +import 'package:hiddify/features/stats/widget/side_bar_stats_overview.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:hiddify/core/theme/theme_preferences.dart'; +import 'package:hiddify/core/theme/app_theme_mode.dart'; +import 'package:hiddify/core/localization/locale_preferences.dart'; +import 'package:hiddify/core/localization/locale_extensions.dart'; + +abstract interface class RootScaffold { + static final stateKey = GlobalKey(); + + static bool canShowDrawer(BuildContext context) => Breakpoints.small.isActive(context); +} + +class AdaptiveRootScaffold extends HookConsumerWidget { + const AdaptiveRootScaffold(this.navigator, {super.key}); + + final Widget navigator; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + + final selectedIndex = getCurrentIndex(context); + + final 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, + ), + NavigationDestination( + icon: const Icon(FluentIcons.box_edit_20_filled), + label: t.config.pageTitle, + ), + NavigationDestination( + icon: const Icon(FluentIcons.settings_20_filled), + label: t.settings.pageTitle, + ), + NavigationDestination( + icon: const Icon(FluentIcons.document_text_20_filled), + label: t.logs.pageTitle, + ), + NavigationDestination( + icon: const Icon(FluentIcons.info_20_filled), + label: t.about.pageTitle, + ), + ]; + + return _CustomAdaptiveScaffold( + selectedIndex: selectedIndex, + onSelectedIndexChange: (index) { + RootScaffold.stateKey.currentState?.closeDrawer(); + switchTab(index, context); + }, + destinations: destinations, + drawerDestinationRange: useMobileRouter ? (3, null) : (0, null), + bottomDestinationRange: (0, 3), + useBottomSheet: useMobileRouter, + sidebarTrailing: const Expanded( + child: Align( + alignment: Alignment.bottomCenter, + child: SideBarStatsOverview(), + ), + ), + body: navigator, + ); + } +} + +class _CustomAdaptiveScaffold extends HookConsumerWidget { + const _CustomAdaptiveScaffold({ + required this.selectedIndex, + required this.onSelectedIndexChange, + required this.destinations, + required this.drawerDestinationRange, + required this.bottomDestinationRange, + this.useBottomSheet = false, + this.sidebarTrailing, + required this.body, + }); + + final int selectedIndex; + final Function(int) onSelectedIndexChange; + final List destinations; + final (int, int?) drawerDestinationRange; + final (int, int?) bottomDestinationRange; + final bool useBottomSheet; + final Widget? sidebarTrailing; + final Widget body; + + List 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; + } + + void selectWithOffset(int index, (int, int?) range) => onSelectedIndexChange(index + range.$1); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + key: RootScaffold.stateKey, + drawer: Breakpoints.small.isActive(context) + ? Drawer( + width: (MediaQuery.sizeOf(context).width * 0.88).clamp(1, 304), + child: Column( + children: [ + // Логотип и название приложения + Container( + padding: const EdgeInsets.symmetric(vertical: 32), + child: Column( + children: [ + Container( + width: 80, + height: 80, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Theme.of(context).colorScheme.primaryContainer, + ), + child: Assets.images.umbrixLogo.image( + fit: BoxFit.contain, + ), + ), + 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), + ); + }, + ), + const SizedBox(height: 16), + const Divider(), + const _DrawerThemeItem(), + const _DrawerLanguageItem(), + const _DrawerLicensesItem(), + ], + ), + ), + ], + ), + ) + : null, + body: AdaptiveLayout( + primaryNavigation: SlotLayout( + config: { + Breakpoints.medium: SlotLayout.from( + key: const Key('primaryNavigation'), + builder: (_) => AdaptiveScaffold.standardNavigationRail( + selectedIndex: selectedIndex, + destinations: destinations.map((dest) => AdaptiveScaffold.toRailDestination(dest)).toList(), + onDestinationSelected: onSelectedIndexChange, + ), + ), + Breakpoints.large: SlotLayout.from( + key: const Key('primaryNavigation1'), + builder: (_) => AdaptiveScaffold.standardNavigationRail( + extended: true, + selectedIndex: selectedIndex, + destinations: destinations.map((dest) => AdaptiveScaffold.toRailDestination(dest)).toList(), + onDestinationSelected: onSelectedIndexChange, + trailing: sidebarTrailing, + ), + ), + }, + ), + body: SlotLayout( + config: { + Breakpoints.standard: SlotLayout.from( + key: const Key('body'), + inAnimation: AdaptiveScaffold.fadeIn, + outAnimation: AdaptiveScaffold.fadeOut, + builder: (context) => body, + ), + }, + ), + ), + // AdaptiveLayout bottom sheet has accessibility issues + bottomNavigationBar: useBottomSheet && Breakpoints.small.isActive(context) + ? NavigationBar( + selectedIndex: selectedWithOffset(bottomDestinationRange) ?? 0, + destinations: destinationsSlice(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), + ), + ); + } +} + +// Виджет для выбора темы в боковом меню +class _DrawerThemeItem extends ConsumerWidget { + const _DrawerThemeItem(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + final themeMode = ref.watch(themePreferencesProvider); + + return ListTile( + leading: const Icon(FluentIcons.weather_moon_20_regular, size: 24), + title: Text(t.settings.general.themeMode), + subtitle: Text(themeMode.present(t)), + onTap: () async { + final selectedThemeMode = await showDialog( + context: context, + builder: (context) => SimpleDialog( + title: Text(t.settings.general.themeMode), + children: AppThemeMode.values + .map((e) => RadioListTile( + title: Text(e.present(t)), + value: e, + groupValue: themeMode, + onChanged: Navigator.of(context).maybePop, + )) + .toList(), + ), + ); + if (selectedThemeMode != null) { + await ref.read(themePreferencesProvider.notifier).changeThemeMode(selectedThemeMode); + } + }, + ); + } +} + +// Виджет для выбора языка в боковом меню +class _DrawerLanguageItem extends ConsumerWidget { + const _DrawerLanguageItem(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + final locale = ref.watch(localePreferencesProvider); + + return ListTile( + leading: const Icon(FluentIcons.local_language_24_regular, size: 24), + title: Text(t.settings.general.locale), + subtitle: Text(locale.localeName), + onTap: () async { + final selectedLocale = await showDialog( + context: context, + builder: (context) => SimpleDialog( + title: Text(t.settings.general.locale), + children: AppLocale.values + .map((e) => RadioListTile( + title: Text(e.localeName), + value: e, + groupValue: locale, + onChanged: Navigator.of(context).maybePop, + )) + .toList(), + ), + ); + if (selectedLocale != null) { + await ref.read(localePreferencesProvider.notifier).changeLocale(selectedLocale); + } + }, + ); + } +} + +// Виджет для открытия лицензий +class _DrawerLicensesItem extends ConsumerWidget { + const _DrawerLicensesItem(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + + return ListTile( + leading: const Icon(FluentIcons.document_text_20_regular, size: 24), + title: Text(MaterialLocalizations.of(context).licensesPageTitle), + onTap: () { + showLicensePage(context: context); + }, + ); + } +} diff --git a/lib/features/common/adaptive_root_scaffold.dart.bak2 b/lib/features/common/adaptive_root_scaffold.dart.bak2 new file mode 100644 index 00000000..d27336b7 --- /dev/null +++ b/lib/features/common/adaptive_root_scaffold.dart.bak2 @@ -0,0 +1,361 @@ +import 'package:fluentui_system_icons/fluentui_system_icons.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart'; +import 'package:hiddify/gen/assets.gen.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/router/router.dart'; +import 'package:hiddify/features/stats/widget/side_bar_stats_overview.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:hiddify/core/theme/theme_preferences.dart'; +import 'package:hiddify/core/theme/app_theme_mode.dart'; +import 'package:hiddify/core/localization/locale_preferences.dart'; +import 'package:hiddify/core/localization/locale_extensions.dart'; + +abstract interface class RootScaffold { + static final stateKey = GlobalKey(); + + static bool canShowDrawer(BuildContext context) => Breakpoints.small.isActive(context); +} + +class AdaptiveRootScaffold extends HookConsumerWidget { + const AdaptiveRootScaffold(this.navigator, {super.key}); + + final Widget navigator; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + + final selectedIndex = getCurrentIndex(context); + + final 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, + ), + NavigationDestination( + icon: const Icon(FluentIcons.settings_20_filled), + label: t.settings.pageTitle, + ), + NavigationDestination( + icon: const Icon(FluentIcons.info_20_regular), + selectedIcon: const Icon(FluentIcons.info_20_filled), + label: t.about.pageTitle, + ), + ]; + + return _CustomAdaptiveScaffold( + selectedIndex: selectedIndex, + onSelectedIndexChange: (index) { + RootScaffold.stateKey.currentState?.closeDrawer(); + switchTab(index, context); + }, + destinations: destinations, + drawerDestinationRange: useMobileRouter ? (3, null) : (0, null), + bottomDestinationRange: (0, 3), + useBottomSheet: useMobileRouter, + sidebarTrailing: const Expanded( + child: Align( + alignment: Alignment.bottomCenter, + child: SideBarStatsOverview(), + ), + ), + body: navigator, + ); + } +} + +class _CustomAdaptiveScaffold extends HookConsumerWidget { + const _CustomAdaptiveScaffold({ + required this.selectedIndex, + required this.onSelectedIndexChange, + required this.destinations, + required this.drawerDestinationRange, + required this.bottomDestinationRange, + this.useBottomSheet = false, + this.sidebarTrailing, + required this.body, + }); + + final int selectedIndex; + final Function(int) onSelectedIndexChange; + final List destinations; + final (int, int?) drawerDestinationRange; + final (int, int?) bottomDestinationRange; + final bool useBottomSheet; + final Widget? sidebarTrailing; + final Widget body; + + List 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; + } + + void selectWithOffset(int index, (int, int?) range) => onSelectedIndexChange(index + range.$1); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + key: RootScaffold.stateKey, + drawer: Breakpoints.small.isActive(context) + ? Drawer( + width: (MediaQuery.sizeOf(context).width * 0.88).clamp(1, 304), + child: Column( + children: [ + // Логотип и название приложения + Container( + padding: const EdgeInsets.symmetric(vertical: 32), + child: Column( + children: [ + Container( + width: 80, + height: 80, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Theme.of(context).colorScheme.primaryContainer, + ), + child: Assets.images.umbrixLogo.image( + fit: BoxFit.contain, + ), + ), + 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), + ); + }, + ), + const SizedBox(height: 16), + const Divider(), + const _DrawerThemeItem(), + const _DrawerLanguageItem(), + const _DrawerLicensesItem(), + ], + ), + ), + ], + ), + ) + : null, + body: AdaptiveLayout( + primaryNavigation: SlotLayout( + config: { + Breakpoints.medium: SlotLayout.from( + key: const Key('primaryNavigation'), + builder: (_) => AdaptiveScaffold.standardNavigationRail( + selectedIndex: selectedIndex, + destinations: destinations.map((dest) => AdaptiveScaffold.toRailDestination(dest)).toList(), + onDestinationSelected: onSelectedIndexChange, + ), + ), + Breakpoints.large: SlotLayout.from( + key: const Key('primaryNavigation1'), + builder: (_) => AdaptiveScaffold.standardNavigationRail( + extended: true, + selectedIndex: selectedIndex, + destinations: destinations.map((dest) => AdaptiveScaffold.toRailDestination(dest)).toList(), + onDestinationSelected: onSelectedIndexChange, + trailing: sidebarTrailing, + ), + ), + }, + ), + body: SlotLayout( + config: { + Breakpoints.standard: SlotLayout.from( + key: const Key('body'), + inAnimation: AdaptiveScaffold.fadeIn, + outAnimation: AdaptiveScaffold.fadeOut, + builder: (context) => body, + ), + }, + ), + ), + // AdaptiveLayout bottom sheet has accessibility issues + bottomNavigationBar: useBottomSheet && Breakpoints.small.isActive(context) + ? NavigationBar( + selectedIndex: selectedWithOffset(bottomDestinationRange) ?? 0, + destinations: destinationsSlice(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), + ), + ); + } +} + +// Виджет для выбора темы в боковом меню +class _DrawerThemeItem extends ConsumerWidget { + const _DrawerThemeItem(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + final themeMode = ref.watch(themePreferencesProvider); + + return ListTile( + leading: const Icon(FluentIcons.weather_moon_20_regular, size: 24), + title: Text(t.settings.general.themeMode), + subtitle: Text(themeMode.present(t)), + onTap: () async { + final selectedThemeMode = await showDialog( + context: context, + builder: (context) => SimpleDialog( + title: Text(t.settings.general.themeMode), + children: AppThemeMode.values + .map((e) => RadioListTile( + title: Text(e.present(t)), + value: e, + groupValue: themeMode, + onChanged: Navigator.of(context).maybePop, + )) + .toList(), + ), + ); + if (selectedThemeMode != null) { + await ref.read(themePreferencesProvider.notifier).changeThemeMode(selectedThemeMode); + } + }, + ); + } +} + +// Виджет для выбора языка в боковом меню +class _DrawerLanguageItem extends ConsumerWidget { + const _DrawerLanguageItem(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + final locale = ref.watch(localePreferencesProvider); + + return ListTile( + leading: const Icon(FluentIcons.local_language_24_regular, size: 24), + title: Text(t.settings.general.locale), + subtitle: Text(locale.localeName), + onTap: () async { + final selectedLocale = await showDialog( + context: context, + builder: (context) => SimpleDialog( + title: Text(t.settings.general.locale), + children: AppLocale.values + .map((e) => RadioListTile( + title: Text(e.localeName), + value: e, + groupValue: locale, + onChanged: Navigator.of(context).maybePop, + )) + .toList(), + ), + ); + if (selectedLocale != null) { + await ref.read(localePreferencesProvider.notifier).changeLocale(selectedLocale); + } + }, + ); + } +} + +// Виджет для открытия лицензий +class _DrawerLicensesItem extends ConsumerWidget { + const _DrawerLicensesItem(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + + return ListTile( + leading: const Icon(FluentIcons.document_text_20_regular, size: 24), + title: Text(MaterialLocalizations.of(context).licensesPageTitle), + onTap: () { + showLicensePage(context: context); + }, + ); + } +} diff --git a/lib/features/common/custom_splash_screen.dart b/lib/features/common/custom_splash_screen.dart new file mode 100644 index 00000000..60f014c4 --- /dev/null +++ b/lib/features/common/custom_splash_screen.dart @@ -0,0 +1,82 @@ +import 'dart:math' as math; +import 'package:flutter/material.dart'; + +class CustomSplashScreen extends StatefulWidget { + const CustomSplashScreen({ + super.key, + required this.onInitializationComplete, + }); + + final VoidCallback onInitializationComplete; + + @override + State createState() => _CustomSplashScreenState(); +} + +class _CustomSplashScreenState extends State with SingleTickerProviderStateMixin { + late AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(seconds: 2), + vsync: this, + )..repeat(); + + // Имитируем время загрузки, затем вызываем callback + Future.delayed(const Duration(milliseconds: 100), () { + if (mounted) { + widget.onInitializationComplete(); + } + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + // UMBRIX: Всегда используем темную тему для splash screen + const isDark = true; + + return Material( + color: const Color(0xFF191f23), + child: Center( + child: Stack( + alignment: Alignment.center, + children: [ + // Крутящийся индикатор + AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return Transform.rotate( + angle: _controller.value * 2 * math.pi, + child: const SizedBox( + width: 120, + height: 120, + child: CircularProgressIndicator( + strokeWidth: 6, + valueColor: AlwaysStoppedAnimation( + Color(0xFF2fbea5), + ), + ), + ), + ); + }, + ), + // Логотип в центре + Image.asset( + 'assets/images/logo_splash.png', + width: 80, + height: 80, + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/common/nested_app_bar.dart b/lib/features/common/nested_app_bar.dart index e0090769..fd2489f2 100644 --- a/lib/features/common/nested_app_bar.dart +++ b/lib/features/common/nested_app_bar.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:hiddify/bootstrap.dart'; import 'package:hiddify/core/router/router.dart'; import 'package:hiddify/features/common/adaptive_root_scaffold.dart'; import 'package:hiddify/utils/utils.dart'; diff --git a/lib/features/common/qr_code_dialog.dart b/lib/features/common/qr_code_dialog.dart index cc6da442..8d4574b5 100644 --- a/lib/features/common/qr_code_dialog.dart +++ b/lib/features/common/qr_code_dialog.dart @@ -39,7 +39,7 @@ class QrCodeDialog extends StatelessWidget { SizedBox( width: width, child: Material( - color: theme.colorScheme.background, + color: theme.colorScheme.surface, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -47,7 +47,7 @@ class QrCodeDialog extends StatelessWidget { child: Text( message!, overflow: TextOverflow.ellipsis, - style: TextStyle(color: theme.colorScheme.onBackground), + style: TextStyle(color: theme.colorScheme.onSurface), ), ), ], diff --git a/lib/features/common/qr_code_scanner_screen.dart b/lib/features/common/qr_code_scanner_screen.dart index 85786ef5..dbfc95a7 100644 --- a/lib/features/common/qr_code_scanner_screen.dart +++ b/lib/features/common/qr_code_scanner_screen.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:developer'; import 'package:dartx/dartx.dart'; import 'package:fluentui_system_icons/fluentui_system_icons.dart'; @@ -131,7 +130,7 @@ class _QRCodeScannerScreenState extends ConsumerState with Future _checkPermissionAndStartScanner() async { // Simplified: assuming permission is granted - final hasPermission = true; + const hasPermission = true; /* Original: final hasPermission = await FlutterEasyPermission.has( perms: permissions, @@ -159,7 +158,7 @@ class _QRCodeScannerScreenState extends ConsumerState with Future startQrScannerIfPermissionIsGranted() async { // Simplified: assuming permission is granted - final hasPermission = true; + const hasPermission = true; /* Original: final hasPermission = await FlutterEasyPermission.has( perms: permissions, @@ -206,7 +205,7 @@ class _QRCodeScannerScreenState extends ConsumerState with final Translations t = ref.watch(translationsProvider); // Simplified: assuming permission is granted - final hasPermission = true; + const hasPermission = true; return FutureBuilder( future: Future.value(hasPermission), /* Original: diff --git a/lib/features/config_option/data/config_option_repository.dart b/lib/features/config_option/data/config_option_repository.dart index 5c2243e6..5325a9b1 100644 --- a/lib/features/config_option/data/config_option_repository.dart +++ b/lib/features/config_option/data/config_option_repository.dart @@ -3,7 +3,6 @@ import 'package:fpdart/fpdart.dart'; import 'package:hiddify/core/model/optional_range.dart'; import 'package:hiddify/core/model/region.dart'; import 'package:hiddify/core/preferences/general_preferences.dart'; - import 'package:hiddify/core/utils/exception_handler.dart'; import 'package:hiddify/core/utils/json_converters.dart'; import 'package:hiddify/core/utils/preferences_utils.dart'; @@ -423,6 +422,27 @@ abstract class ConfigOptions { (ref) async { // final region = ref.watch(Preferences.region); final rules = []; + + // Добавляем правила для исключенных доменов + final excludedDomains = ref.watch(excludedDomainsListProvider); + if (excludedDomains.isNotEmpty) { + final domainsString = excludedDomains.map((domain) { + if (domain.startsWith('.')) { + // Зона домена (например, .com) - используем domain: prefix + return 'domain:$domain'; + } else { + // Конкретный домен (например, site.com) - используем domain: для поддомена + return 'domain:.$domain'; + } + }).join(','); + + rules.add( + SingboxRule( + domains: domainsString, + outbound: RuleOutbound.bypass, + ), + ); + } // final rules = switch (region) { // Region.ir => [ // const SingboxRule( diff --git a/lib/features/config_option/overview/config_options_page.dart b/lib/features/config_option/overview/config_options_page.dart index 1040f2de..9ae8835a 100644 --- a/lib/features/config_option/overview/config_options_page.dart +++ b/lib/features/config_option/overview/config_options_page.dart @@ -1,10 +1,7 @@ import 'package:dartx/dartx.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hiddify/core/localization/translations.dart'; -import 'package:hiddify/core/model/optional_range.dart'; -import 'package:hiddify/core/model/region.dart'; import 'package:hiddify/core/notification/in_app_notification_controller.dart'; import 'package:hiddify/core/widget/adaptive_icon.dart'; import 'package:hiddify/core/widget/tip_card.dart'; @@ -12,9 +9,7 @@ import 'package:hiddify/features/common/confirmation_dialogs.dart'; import 'package:hiddify/features/common/nested_app_bar.dart'; import 'package:hiddify/features/config_option/data/config_option_repository.dart'; import 'package:hiddify/features/config_option/notifier/config_option_notifier.dart'; -import 'package:hiddify/features/config_option/overview/warp_options_widgets.dart'; import 'package:hiddify/features/config_option/widget/preference_tile.dart'; -import 'package:hiddify/features/log/model/log_level.dart'; import 'package:hiddify/features/settings/widgets/sections_widgets.dart'; import 'package:hiddify/features/settings/widgets/settings_input_dialog.dart'; import 'package:hiddify/singbox/model/singbox_config_enum.dart'; @@ -22,56 +17,15 @@ import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:humanizer/humanizer.dart'; -enum ConfigOptionSection { - warp, - fragment; - - static final _warpKey = GlobalKey(debugLabel: "warp-section-key"); - static final _fragmentKey = GlobalKey(debugLabel: "fragment-section-key"); - - GlobalKey get key => switch (this) { - ConfigOptionSection.warp => _warpKey, - ConfigOptionSection.fragment => _fragmentKey, - }; -} - class ConfigOptionsPage extends HookConsumerWidget { - ConfigOptionsPage({super.key, String? section}) : section = section != null ? ConfigOptionSection.values.byName(section) : null; - - final ConfigOptionSection? section; + const ConfigOptionsPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); - final scrollController = useScrollController(); - - useMemoized( - () { - if (section != null) { - WidgetsBinding.instance.addPostFrameCallback( - (_) { - final box = section!.key.currentContext?.findRenderObject() as RenderBox?; - final offset = box?.localToGlobal(Offset.zero); - if (offset == null) return; - final height = scrollController.offset + offset.dy - MediaQueryData.fromView(View.of(context)).padding.top - kToolbarHeight; - scrollController.animateTo( - height, - duration: const Duration(milliseconds: 500), - curve: Curves.decelerate, - ); - }, - ); - } - }, - ); - - String experimental(String txt) { - return "$txt (${t.settings.experimental})"; - } return Scaffold( body: CustomScrollView( - controller: scrollController, shrinkWrap: true, slivers: [ NestedAppBar( @@ -131,76 +85,71 @@ class ConfigOptionsPage extends HookConsumerWidget { child: Column( children: [ TipCard(message: t.settings.experimentalMsg), - ChoicePreferenceWidget( - selected: ref.watch(ConfigOptions.logLevel), - preferences: ref.watch(ConfigOptions.logLevel.notifier), - choices: LogLevel.choices, - title: t.config.logLevel, - presentChoice: (value) => value.name.toUpperCase(), - ), const SettingsDivider(), - SettingsSection(t.config.section.route), - ChoicePreferenceWidget( - selected: ref.watch(ConfigOptions.region), - preferences: ref.watch(ConfigOptions.region.notifier), - choices: Region.values, - title: t.settings.general.region, - presentChoice: (value) => value.present(t), - onChanged: (val) => ref.watch(ConfigOptions.directDnsAddress.notifier).reset(), - ), - SwitchListTile( - title: Text(experimental(t.config.blockAds)), - value: ref.watch(ConfigOptions.blockAds), - onChanged: ref.watch(ConfigOptions.blockAds.notifier).update, - ), - SwitchListTile( - title: Text(experimental(t.config.bypassLan)), - value: ref.watch(ConfigOptions.bypassLan), - onChanged: ref.watch(ConfigOptions.bypassLan.notifier).update, - ), - SwitchListTile( - title: Text(t.config.resolveDestination), - value: ref.watch(ConfigOptions.resolveDestination), - onChanged: ref.watch(ConfigOptions.resolveDestination.notifier).update, - ), - ChoicePreferenceWidget( - selected: ref.watch(ConfigOptions.ipv6Mode), - preferences: ref.watch(ConfigOptions.ipv6Mode.notifier), - choices: IPv6Mode.values, - title: t.config.ipv6Mode, - presentChoice: (value) => value.present(t), + // Варианты маршрутизации - раскрывающаяся секция + Theme( + data: Theme.of(context).copyWith(dividerColor: Colors.transparent), + child: ExpansionTile( + leading: const Icon(Icons.route), + title: Text(t.config.section.route), + initiallyExpanded: false, + children: [ + SwitchListTile( + title: Text(t.config.resolveDestination), + value: ref.watch(ConfigOptions.resolveDestination), + onChanged: ref.watch(ConfigOptions.resolveDestination.notifier).update, + ), + ChoicePreferenceWidget( + selected: ref.watch(ConfigOptions.ipv6Mode), + preferences: ref.watch(ConfigOptions.ipv6Mode.notifier), + choices: IPv6Mode.values, + title: t.config.ipv6Mode, + presentChoice: (value) => value.present(t), + ), + ], + ), ), const SettingsDivider(), - SettingsSection(t.config.section.dns), - ValuePreferenceWidget( - value: ref.watch(ConfigOptions.remoteDnsAddress), - preferences: ref.watch(ConfigOptions.remoteDnsAddress.notifier), - title: t.config.remoteDnsAddress, - ), - ChoicePreferenceWidget( - selected: ref.watch(ConfigOptions.remoteDnsDomainStrategy), - preferences: ref.watch(ConfigOptions.remoteDnsDomainStrategy.notifier), - choices: DomainStrategy.values, - title: t.config.remoteDnsDomainStrategy, - presentChoice: (value) => value.displayName, - ), - ValuePreferenceWidget( - value: ref.watch(ConfigOptions.directDnsAddress), - preferences: ref.watch(ConfigOptions.directDnsAddress.notifier), - title: t.config.directDnsAddress, - ), - ChoicePreferenceWidget( - selected: ref.watch(ConfigOptions.directDnsDomainStrategy), - preferences: ref.watch(ConfigOptions.directDnsDomainStrategy.notifier), - choices: DomainStrategy.values, - title: t.config.directDnsDomainStrategy, - presentChoice: (value) => value.displayName, - ), - SwitchListTile( - title: Text(t.config.enableDnsRouting), - value: ref.watch(ConfigOptions.enableDnsRouting), - onChanged: ref.watch(ConfigOptions.enableDnsRouting.notifier).update, + // Параметры DNS - раскрывающаяся секция + Theme( + data: Theme.of(context).copyWith(dividerColor: Colors.transparent), + child: ExpansionTile( + leading: const Icon(Icons.dns), + title: Text(t.config.section.dns), + initiallyExpanded: false, + children: [ + ValuePreferenceWidget( + value: ref.watch(ConfigOptions.remoteDnsAddress), + preferences: ref.watch(ConfigOptions.remoteDnsAddress.notifier), + title: t.config.remoteDnsAddress, + ), + ChoicePreferenceWidget( + selected: ref.watch(ConfigOptions.remoteDnsDomainStrategy), + preferences: ref.watch(ConfigOptions.remoteDnsDomainStrategy.notifier), + choices: DomainStrategy.values, + title: t.config.remoteDnsDomainStrategy, + presentChoice: (value) => value.displayName, + ), + ValuePreferenceWidget( + value: ref.watch(ConfigOptions.directDnsAddress), + preferences: ref.watch(ConfigOptions.directDnsAddress.notifier), + title: t.config.directDnsAddress, + ), + ChoicePreferenceWidget( + selected: ref.watch(ConfigOptions.directDnsDomainStrategy), + preferences: ref.watch(ConfigOptions.directDnsDomainStrategy.notifier), + choices: DomainStrategy.values, + title: t.config.directDnsDomainStrategy, + presentChoice: (value) => value.displayName, + ), + SwitchListTile( + title: Text(t.config.enableDnsRouting), + value: ref.watch(ConfigOptions.enableDnsRouting), + onChanged: ref.watch(ConfigOptions.enableDnsRouting.notifier).update, + ), + ], + ), ), // const SettingsDivider(), // SettingsSection(experimental(t.config.section.mux)), @@ -226,144 +175,103 @@ class ConfigOptionsPage extends HookConsumerWidget { // digitsOnly: true, // ), const SettingsDivider(), - SettingsSection(t.config.section.inbound), - ChoicePreferenceWidget( - selected: ref.watch(ConfigOptions.serviceMode), - preferences: ref.watch(ConfigOptions.serviceMode.notifier), - choices: ServiceMode.choices, - title: t.config.serviceMode, - presentChoice: (value) => value.present(t), - ), - SwitchListTile( - title: Text(t.config.strictRoute), - value: ref.watch(ConfigOptions.strictRoute), - onChanged: ref.watch(ConfigOptions.strictRoute.notifier).update, - ), - ChoicePreferenceWidget( - selected: ref.watch(ConfigOptions.tunImplementation), - preferences: ref.watch(ConfigOptions.tunImplementation.notifier), - choices: TunImplementation.values, - title: t.config.tunImplementation, - presentChoice: (value) => value.name, - ), - ValuePreferenceWidget( - value: ref.watch(ConfigOptions.mixedPort), - preferences: ref.watch(ConfigOptions.mixedPort.notifier), - title: t.config.mixedPort, - inputToValue: int.tryParse, - digitsOnly: true, - validateInput: isPort, - ), - ValuePreferenceWidget( - value: ref.watch(ConfigOptions.tproxyPort), - preferences: ref.watch(ConfigOptions.tproxyPort.notifier), - title: t.config.tproxyPort, - inputToValue: int.tryParse, - digitsOnly: true, - validateInput: isPort, - ), - ValuePreferenceWidget( - value: ref.watch(ConfigOptions.localDnsPort), - preferences: ref.watch(ConfigOptions.localDnsPort.notifier), - title: t.config.localDnsPort, - inputToValue: int.tryParse, - digitsOnly: true, - validateInput: isPort, - ), - SwitchListTile( - title: Text( - experimental(t.config.allowConnectionFromLan), + // Входящие параметры - раскрывающаяся секция + Theme( + data: Theme.of(context).copyWith(dividerColor: Colors.transparent), + child: ExpansionTile( + leading: const Icon(Icons.input), + title: Text(t.config.section.inbound), + initiallyExpanded: false, + children: [ + ChoicePreferenceWidget( + selected: ref.watch(ConfigOptions.serviceMode), + preferences: ref.watch(ConfigOptions.serviceMode.notifier), + choices: ServiceMode.choices, + title: t.config.serviceMode, + presentChoice: (value) => value.present(t), + ), + SwitchListTile( + title: Text(t.config.strictRoute), + value: ref.watch(ConfigOptions.strictRoute), + onChanged: ref.watch(ConfigOptions.strictRoute.notifier).update, + ), + ChoicePreferenceWidget( + selected: ref.watch(ConfigOptions.tunImplementation), + preferences: ref.watch(ConfigOptions.tunImplementation.notifier), + choices: TunImplementation.values, + title: t.config.tunImplementation, + presentChoice: (value) => value.name, + ), + ValuePreferenceWidget( + value: ref.watch(ConfigOptions.mixedPort), + preferences: ref.watch(ConfigOptions.mixedPort.notifier), + title: t.config.mixedPort, + inputToValue: int.tryParse, + digitsOnly: true, + validateInput: isPort, + ), + ValuePreferenceWidget( + value: ref.watch(ConfigOptions.tproxyPort), + preferences: ref.watch(ConfigOptions.tproxyPort.notifier), + title: t.config.tproxyPort, + inputToValue: int.tryParse, + digitsOnly: true, + validateInput: isPort, + ), + ValuePreferenceWidget( + value: ref.watch(ConfigOptions.localDnsPort), + preferences: ref.watch(ConfigOptions.localDnsPort.notifier), + title: t.config.localDnsPort, + inputToValue: int.tryParse, + digitsOnly: true, + validateInput: isPort, + ), + ], ), - value: ref.watch(ConfigOptions.allowConnectionFromLan), - onChanged: ref.read(ConfigOptions.allowConnectionFromLan.notifier).update, ), const SettingsDivider(), - SettingsSection( - experimental(t.config.section.tlsTricks), - key: ConfigOptionSection._fragmentKey, - ), - SwitchListTile( - title: Text(t.config.enableTlsFragment), - value: ref.watch(ConfigOptions.enableTlsFragment), - onChanged: ref.watch(ConfigOptions.enableTlsFragment.notifier).update, - ), - ValuePreferenceWidget( - value: ref.watch(ConfigOptions.tlsFragmentSize), - preferences: ref.watch(ConfigOptions.tlsFragmentSize.notifier), - title: t.config.tlsFragmentSize, - inputToValue: OptionalRange.tryParse, - presentValue: (value) => value.present(t), - formatInputValue: (value) => value.format(), - ), - ValuePreferenceWidget( - value: ref.watch(ConfigOptions.tlsFragmentSleep), - preferences: ref.watch(ConfigOptions.tlsFragmentSleep.notifier), - title: t.config.tlsFragmentSleep, - inputToValue: OptionalRange.tryParse, - presentValue: (value) => value.present(t), - formatInputValue: (value) => value.format(), - ), - SwitchListTile( - title: Text(t.config.enableTlsMixedSniCase), - value: ref.watch(ConfigOptions.enableTlsMixedSniCase), - onChanged: ref.watch(ConfigOptions.enableTlsMixedSniCase.notifier).update, - ), - SwitchListTile( - title: Text(t.config.enableTlsPadding), - value: ref.watch(ConfigOptions.enableTlsPadding), - onChanged: ref.watch(ConfigOptions.enableTlsPadding.notifier).update, - ), - ValuePreferenceWidget( - value: ref.watch(ConfigOptions.tlsPaddingSize), - preferences: ref.watch(ConfigOptions.tlsPaddingSize.notifier), - title: t.config.tlsPaddingSize, - inputToValue: OptionalRange.tryParse, - presentValue: (value) => value.format(), - formatInputValue: (value) => value.format(), - ), - const SettingsDivider(), - SettingsSection(experimental(t.config.section.warp)), - WarpOptionsTiles(key: ConfigOptionSection._warpKey), - const SettingsDivider(), - SettingsSection(t.config.section.misc), - ValuePreferenceWidget( - value: ref.watch(ConfigOptions.connectionTestUrl), - preferences: ref.watch(ConfigOptions.connectionTestUrl.notifier), - title: t.config.connectionTestUrl, - ), - ListTile( - title: Text(t.config.urlTestInterval), - subtitle: Text( - ref.watch(ConfigOptions.urlTestInterval).toApproximateTime(isRelativeToNow: false), + // Разное - раскрывающаяся секция + Theme( + data: Theme.of(context).copyWith(dividerColor: Colors.transparent), + child: ExpansionTile( + leading: const Icon(Icons.more_horiz), + title: Text(t.config.section.misc), + initiallyExpanded: false, + children: [ + ValuePreferenceWidget( + value: ref.watch(ConfigOptions.connectionTestUrl), + preferences: ref.watch(ConfigOptions.connectionTestUrl.notifier), + title: t.config.connectionTestUrl, + ), + ListTile( + title: Text(t.config.urlTestInterval), + subtitle: Text( + ref.watch(ConfigOptions.urlTestInterval).toApproximateTime(isRelativeToNow: false), + ), + onTap: () async { + final urlTestInterval = await SettingsSliderDialog( + title: t.config.urlTestInterval, + initialValue: ref.watch(ConfigOptions.urlTestInterval).inMinutes.coerceIn(0, 60).toDouble(), + onReset: ref.read(ConfigOptions.urlTestInterval.notifier).reset, + min: 1, + max: 60, + divisions: 60, + labelGen: (value) => Duration(minutes: value.toInt()).toApproximateTime(isRelativeToNow: false), + ).show(context); + if (urlTestInterval == null) return; + await ref.read(ConfigOptions.urlTestInterval.notifier).update(Duration(minutes: urlTestInterval.toInt())); + }, + ), + ValuePreferenceWidget( + value: ref.watch(ConfigOptions.clashApiPort), + preferences: ref.watch(ConfigOptions.clashApiPort.notifier), + title: t.config.clashApiPort, + validateInput: isPort, + digitsOnly: true, + inputToValue: int.tryParse, + ), + ], ), - onTap: () async { - final urlTestInterval = await SettingsSliderDialog( - title: t.config.urlTestInterval, - initialValue: ref.watch(ConfigOptions.urlTestInterval).inMinutes.coerceIn(0, 60).toDouble(), - onReset: ref.read(ConfigOptions.urlTestInterval.notifier).reset, - min: 1, - max: 60, - divisions: 60, - labelGen: (value) => Duration(minutes: value.toInt()).toApproximateTime(isRelativeToNow: false), - ).show(context); - if (urlTestInterval == null) return; - await ref.read(ConfigOptions.urlTestInterval.notifier).update(Duration(minutes: urlTestInterval.toInt())); - }, - ), - ValuePreferenceWidget( - value: ref.watch(ConfigOptions.clashApiPort), - preferences: ref.watch(ConfigOptions.clashApiPort.notifier), - title: t.config.clashApiPort, - validateInput: isPort, - digitsOnly: true, - inputToValue: int.tryParse, - ), - - SwitchListTile( - title: Text(experimental(t.config.useXrayCoreWhenPossible.Label)), - subtitle: Text(t.config.useXrayCoreWhenPossible.Description), - value: ref.watch(ConfigOptions.useXrayCoreWhenPossible), - onChanged: ref.watch(ConfigOptions.useXrayCoreWhenPossible.notifier).update, ), const Gap(24), ], diff --git a/lib/features/config_option/widget/quick_settings_modal.dart b/lib/features/config_option/widget/quick_settings_modal.dart index f3ffb578..836b1509 100644 --- a/lib/features/config_option/widget/quick_settings_modal.dart +++ b/lib/features/config_option/widget/quick_settings_modal.dart @@ -5,13 +5,105 @@ import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/router/router.dart'; import 'package:hiddify/features/config_option/data/config_option_repository.dart'; import 'package:hiddify/features/config_option/notifier/warp_option_notifier.dart'; -import 'package:hiddify/features/config_option/overview/config_options_page.dart'; +import 'package:hiddify/features/settings/experimental_features_page.dart'; import 'package:hiddify/singbox/model/singbox_config_enum.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; class QuickSettingsModal extends HookConsumerWidget { const QuickSettingsModal({super.key}); + void _showHelpDialog(BuildContext context, Translations t) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Row( + children: [ + const Icon(FluentIcons.info_24_regular, size: 28), + const SizedBox(width: 12), + Expanded(child: Text(t.config.quickSettings)), + ], + ), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHelpSection( + icon: FluentIcons.server_20_filled, + title: 'Прокси / VPN', + description: 'Выберите режим подключения:\n\n' + '• Прокси — использует локальный прокси-сервер для перенаправления трафика приложений\n' + '• VPN — создает виртуальную частную сеть для защиты всего трафика устройства', + ), + const Divider(height: 24), + _buildHelpSection( + icon: FluentIcons.shield_20_filled, + title: t.config.setupWarp, + description: 'Технология Cloudflare WARP для дополнительной защиты:\n\n' + '• Шифрует DNS-запросы\n' + '• Скрывает ваш IP-адрес\n' + '• Улучшает скорость подключения в некоторых регионах\n' + '• Требует первоначальной настройки с получением лицензионного ключа', + ), + const Divider(height: 24), + _buildHelpSection( + icon: FluentIcons.shield_keyhole_20_filled, + title: t.config.enableTlsFragment, + description: 'Разбивает TLS-пакеты на фрагменты для обхода блокировок:\n\n' + '• Помогает обойти DPI (глубокую проверку пакетов)\n' + '• Работает на уровне TLS-рукопожатия\n' + '• Может немного замедлить начальное соединение\n' + '• Эффективно против систем блокировки на основе анализа SNI', + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(t.window.close), + ), + ], + ), + ); + } + + Widget _buildHelpSection({ + required IconData icon, + required String title, + required String description, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(icon, size: 20), + const SizedBox(width: 8), + Expanded( + child: Text( + title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + description, + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + height: 1.4, + ), + ), + ], + ); + } + @override Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); @@ -21,6 +113,28 @@ class QuickSettingsModal extends HookConsumerWidget { return SingleChildScrollView( child: Column( children: [ + // Заголовок с кнопкой помощи + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 8, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + t.config.quickSettings, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + IconButton( + icon: const Icon(FluentIcons.question_circle_24_regular), + onPressed: () => _showHelpDialog(context, t), + tooltip: 'Справка', + ), + ], + ), + ), + const Gap(8), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: SegmentedButton( @@ -42,31 +156,25 @@ class QuickSettingsModal extends HookConsumerWidget { ), const Gap(8), if (warpPrefaceCompleted) - GestureDetector( - onLongPress: () { - ConfigOptionsRoute(section: ConfigOptionSection.warp.name).go(context); - }, - child: SwitchListTile( - value: ref.watch(ConfigOptions.enableWarp), - onChanged: ref.watch(ConfigOptions.enableWarp.notifier).update, - title: Text(t.config.enableWarp), - ), + SwitchListTile( + value: ref.watch(ConfigOptions.enableWarp), + onChanged: ref.watch(ConfigOptions.enableWarp.notifier).update, + title: Text(t.config.enableWarp), ) else ListTile( title: Text(t.config.setupWarp), trailing: const Icon(FluentIcons.chevron_right_24_regular), - onTap: () => ConfigOptionsRoute(section: ConfigOptionSection.warp.name).go(context), - ), - GestureDetector( - onLongPress: () { - ConfigOptionsRoute(section: ConfigOptionSection.fragment.name).go(context); - }, - child: SwitchListTile( - value: ref.watch(ConfigOptions.enableTlsFragment), - onChanged: ref.watch(ConfigOptions.enableTlsFragment.notifier).update, - title: Text(t.config.enableTlsFragment), + onTap: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const ExperimentalFeaturesPage(), + ), + ), ), + SwitchListTile( + value: ref.watch(ConfigOptions.enableTlsFragment), + onChanged: ref.watch(ConfigOptions.enableTlsFragment.notifier).update, + title: Text(t.config.enableTlsFragment), ), // SwitchListTile( // value: ref.watch(ConfigOptions.enableMux), diff --git a/lib/features/connection/data/connection_platform_source.dart b/lib/features/connection/data/connection_platform_source.dart index a2c6093e..18ca2c33 100644 --- a/lib/features/connection/data/connection_platform_source.dart +++ b/lib/features/connection/data/connection_platform_source.dart @@ -26,7 +26,7 @@ class ConnectionPlatformSourceImpl (pElevation) { if (OpenProcessToken( GetCurrentProcess(), - TOKEN_QUERY, + TOKEN_ACCESS_MASK.TOKEN_QUERY, phToken.cast(), ) == 1) { diff --git a/lib/features/connection/data/connection_repository.dart b/lib/features/connection/data/connection_repository.dart index 28740177..1f4fd6c1 100644 --- a/lib/features/connection/data/connection_repository.dart +++ b/lib/features/connection/data/connection_repository.dart @@ -149,7 +149,7 @@ class ConnectionRepositoryImpl with ExceptionHandler, InfraLogger implements Con ) { return TaskEither.Do( ($) async { - var options = await $(getConfigOption()); + final options = await $(getConfigOption()); loggy.info( "config options: ${options.format()}\nMemory Limit: ${!disableMemoryLimit}", ); @@ -215,7 +215,7 @@ class ConnectionRepositoryImpl with ExceptionHandler, InfraLogger implements Con ) { return TaskEither.Do( ($) async { - var options = await $(getConfigOption()); + final options = await $(getConfigOption()); loggy.info( "config options: ${options.format()}\nMemory Limit: ${!disableMemoryLimit}", ); diff --git a/lib/features/home/widget/connection_button.dart b/lib/features/home/widget/connection_button.dart index f4c748b8..b3ec7e15 100644 --- a/lib/features/home/widget/connection_button.dart +++ b/lib/features/home/widget/connection_button.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:gap/gap.dart'; import 'package:hiddify/core/localization/translations.dart'; @@ -16,7 +17,6 @@ import 'package:hiddify/gen/assets.gen.dart'; import 'package:hiddify/utils/alerts.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -// TODO: rewrite class ConnectionButton extends HookConsumerWidget { const ConnectionButton({super.key}); @@ -80,7 +80,7 @@ class ConnectionButton extends HookConsumerWidget { }, buttonColor: switch (connectionStatus) { AsyncData(value: Connected()) when requiresReconnect == true => Colors.teal, - AsyncData(value: Connected()) when delay <= 0 || delay >= 65000 => Color.fromARGB(255, 185, 176, 103), + AsyncData(value: Connected()) when delay <= 0 || delay >= 65000 => const Color.fromARGB(255, 185, 176, 103), AsyncData(value: Connected()) => buttonTheme.connectedColor!, AsyncData(value: _) => buttonTheme.idleColor!, _ => Colors.red, @@ -90,20 +90,14 @@ class ConnectionButton extends HookConsumerWidget { AsyncData(value: Connected()) => Assets.images.connectNorouz, AsyncData(value: _) => Assets.images.disconnectNorouz, _ => Assets.images.disconnectNorouz, - AsyncData(value: Disconnected()) || AsyncError() => Assets.images.disconnectNorouz, - AsyncData(value: Connected()) => Assets.images.connectNorouz, - _ => Assets.images.disconnectNorouz, - }, - useImage: today.day >= 19 && today.day <= 23 && today.month == 3, - isConnected: switch (connectionStatus) { - AsyncData(value: Connected()) => true, - _ => false, }, + useImage: true, // Всегда показывать картинки + isConnected: connectionStatus is AsyncData && (connectionStatus as AsyncData).value is Connected, ); } } -class _ConnectionButton extends StatelessWidget { +class _ConnectionButton extends StatefulWidget { const _ConnectionButton({ required this.onTap, required this.enabled, @@ -122,62 +116,257 @@ class _ConnectionButton extends StatelessWidget { final bool useImage; final bool isConnected; + @override + State<_ConnectionButton> createState() => _ConnectionButtonState(); +} + +class _ConnectionButtonState extends State<_ConnectionButton> with SingleTickerProviderStateMixin { + late AnimationController _pulseController; + bool _isPressed = false; + + @override + void initState() { + super.initState(); + _pulseController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 2000), + ); + + if (widget.isConnected) { + _pulseController.repeat(); + } + } + + @override + void didUpdateWidget(_ConnectionButton oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.isConnected != oldWidget.isConnected) { + if (widget.isConnected) { + _pulseController.repeat(); + } else { + _pulseController.stop(); + _pulseController.reset(); + } + } + } + + @override + void dispose() { + _pulseController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Semantics( button: true, - enabled: enabled, - label: label, - child: Container( - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - blurRadius: 16, - color: buttonColor.withOpacity(0.5), + enabled: widget.enabled, + label: widget.label, + child: GestureDetector( + onTapDown: widget.enabled + ? (_) { + HapticFeedback.lightImpact(); + setState(() => _isPressed = true); + } + : null, + onTapUp: widget.enabled + ? (_) { + setState(() => _isPressed = false); + HapticFeedback.mediumImpact(); + widget.onTap(); + } + : null, + onTapCancel: () { + setState(() => _isPressed = false); + }, + child: AnimatedScale( + scale: _isPressed ? 0.95 : 1.0, + duration: const Duration(milliseconds: 100), + curve: Curves.easeOut, + child: Container( + width: 200, + height: 200, + decoration: BoxDecoration( + shape: BoxShape.circle, + boxShadow: [ + // Основная тень + BoxShadow( + blurRadius: 32, + spreadRadius: 0, + color: widget.buttonColor.withOpacity(0.4), + offset: const Offset(0, 8), + ), + // Внутренняя подсветка + if (widget.isConnected) + BoxShadow( + blurRadius: 40, + spreadRadius: -8, + color: widget.buttonColor.withOpacity(0.6), + offset: const Offset(0, 0), + ), + ], ), - ], + child: Stack( + alignment: Alignment.center, + children: [ + // Pulse эффект при подключении + if (widget.isConnected) + AnimatedBuilder( + animation: _pulseController, + builder: (context, child) { + return Container( + width: 200 + (_pulseController.value * 20), + height: 200 + (_pulseController.value * 20), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: widget.buttonColor.withOpacity( + 0.3 * (1 - _pulseController.value), + ), + width: 3, + ), + ), + ); + }, + ), + + // Внешний круг с neomorphism эффектом + Container( + width: 200, + height: 200, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: isDark + ? [ + const Color(0xFF2C2C2C), + const Color(0xFF1A1A1A), + ] + : [ + const Color(0xFFF5F5F5), + const Color(0xFFE0E0E0), + ], + ), + boxShadow: [ + // Внешняя тень + BoxShadow( + color: isDark ? Colors.black.withOpacity(0.5) : Colors.grey.withOpacity(0.3), + offset: const Offset(8, 8), + blurRadius: 16, + ), + // Внутренняя подсветка + BoxShadow( + color: isDark ? Colors.white.withOpacity(0.05) : Colors.white.withOpacity(0.8), + offset: const Offset(-8, -8), + blurRadius: 16, + ), + ], + ), + ), + + // Внутренний круг с градиентом + Container( + width: 160, + height: 160, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [ + widget.buttonColor.withOpacity(0.95), + widget.buttonColor, + widget.buttonColor.withOpacity(1.0), + ], + stops: const [0.0, 0.7, 1.0], + ), + boxShadow: [ + BoxShadow( + color: widget.buttonColor.withOpacity(0.5), + blurRadius: 20, + spreadRadius: -5, + ), + ], + ), + child: Material( + key: const ValueKey("home_connection_button"), + shape: const CircleBorder(), + color: Colors.transparent, + child: InkWell( + customBorder: const CircleBorder(), + onTap: null, // Handled by GestureDetector + splashColor: Colors.white.withOpacity(0.2), + highlightColor: Colors.white.withOpacity(0.1), + child: Center( + child: TweenAnimationBuilder( + tween: Tween( + begin: 0.0, + end: widget.isConnected ? 1.0 : 0.0, + ), + duration: const Duration(milliseconds: 400), + curve: Curves.easeInOut, + builder: (context, value, child) { + return Transform.rotate( + angle: value * 0.5, // Легкий поворот при подключении + child: Icon( + Icons.power_settings_new_rounded, + color: Colors.white, + size: 72, + shadows: [ + Shadow( + color: Colors.black.withOpacity(0.3), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + ); + }, + ), + ), + ), + ), + ), + + // Glossy overlay эффект + Positioned( + top: 30, + child: Container( + width: 100, + height: 50, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(50), + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.white.withOpacity(0.3), + Colors.white.withOpacity(0.0), + ], + ), + ), + ), + ), + ], + ), + ).animate(target: widget.enabled ? 0 : 1).blurXY(end: 2, curve: Curves.easeOut).animate(target: widget.enabled ? 0 : 1).scaleXY(end: 0.92, curve: Curves.easeInOut), ), - width: 120, - height: 120, - child: Material( - key: const ValueKey("home_connection_button"), - shape: const CircleBorder(), - color: Colors.white, - child: InkWell( - onTap: onTap, - child: Padding( - padding: const EdgeInsets.all(30), - child: TweenAnimationBuilder( - tween: ColorTween(end: buttonColor), - duration: const Duration(milliseconds: 250), - builder: (context, value, child) { - if (useImage) { - return image.image(filterQuality: FilterQuality.medium); - } else { - // Определяем какую иконку показывать: play для отключенного, stop для подключенного - return Icon( - isConnected ? Icons.stop_rounded : Icons.play_arrow_rounded, - color: value, - size: 60, - ); - } - }, - ), - ), - ), - ).animate(target: enabled ? 0 : 1).blurXY(end: 1), - ).animate(target: enabled ? 0 : 1).scaleXY(end: .88, curve: Curves.easeIn), + ), ), - const Gap(16), + const Gap(28), ExcludeSemantics( child: AnimatedText( - label, - style: Theme.of(context).textTheme.titleMedium, + widget.label, + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w600, + letterSpacing: 0.5, + ), ), ), ], diff --git a/lib/features/home/widget/home_page.dart b/lib/features/home/widget/home_page.dart index ef59114d..07042ea4 100644 --- a/lib/features/home/widget/home_page.dart +++ b/lib/features/home/widget/home_page.dart @@ -12,7 +12,6 @@ import 'package:hiddify/features/profile/notifier/active_profile_notifier.dart'; import 'package:hiddify/features/profile/widget/profile_tile.dart'; import 'package:hiddify/features/proxy/active/active_proxy_delay_indicator.dart'; import 'package:hiddify/features/proxy/active/active_proxy_footer.dart'; -import 'package:hiddify/features/proxy/active/active_proxy_notifier.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:sliver_tools/sliver_tools.dart'; @@ -33,12 +32,12 @@ class HomePage extends HookConsumerWidget { CustomScrollView( slivers: [ NestedAppBar( - title: Text.rich( + title: const Text.rich( TextSpan( children: [ - const TextSpan(text: "Umbrix"), - const TextSpan(text: " "), - const WidgetSpan( + TextSpan(text: "Umbrix"), + TextSpan(text: " "), + WidgetSpan( child: AppVersionLabel(), alignment: PlaceholderAlignment.middle, ), diff --git a/lib/features/intro/widget/intro_page.dart b/lib/features/intro/widget/intro_page.dart index c6a79475..50105907 100644 --- a/lib/features/intro/widget/intro_page.dart +++ b/lib/features/intro/widget/intro_page.dart @@ -1,17 +1,17 @@ +import 'dart:ui' show PlatformDispatcher; + import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hiddify/core/analytics/analytics_controller.dart'; -import 'package:hiddify/core/http_client/dio_http_client.dart'; import 'package:hiddify/core/localization/locale_preferences.dart'; import 'package:hiddify/core/localization/translations.dart'; -import 'package:hiddify/core/model/constants.dart'; import 'package:hiddify/core/model/region.dart'; -import 'package:hiddify/gen/assets.gen.dart'; import 'package:hiddify/core/preferences/general_preferences.dart'; import 'package:hiddify/features/common/general_pref_tiles.dart'; import 'package:hiddify/features/config_option/data/config_option_repository.dart'; +import 'package:hiddify/features/settings/about/terms_and_conditions_screen.dart'; import 'package:hiddify/gen/assets.gen.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -19,9 +19,7 @@ import 'package:sliver_tools/sliver_tools.dart'; import 'package:timezone_to_country/timezone_to_country.dart'; class IntroPage extends HookConsumerWidget with PresLogger { - IntroPage({super.key}); - - bool locationInfoLoaded = false; + const IntroPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -29,10 +27,11 @@ class IntroPage extends HookConsumerWidget with PresLogger { final theme = Theme.of(context); final isStarting = useState(false); - if (!locationInfoLoaded) { + // Автовыбор региона теперь через useEffect (хуки) + useEffect(() { autoSelectRegion(ref).then((value) => loggy.debug("Auto Region selection finished!")); - locationInfoLoaded = true; - } + return null; + }, const [],); return Scaffold( body: Container( @@ -82,7 +81,7 @@ class IntroPage extends HookConsumerWidget with PresLogger { const Gap(24), // Заголовок Text( - 'Welcome to Umbrix', + t.intro.welcomeTitle, style: theme.textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.bold, color: theme.colorScheme.onSurface, @@ -91,7 +90,7 @@ class IntroPage extends HookConsumerWidget with PresLogger { ), const Gap(8), Text( - 'Fast and Secure', + t.intro.subtitle, style: theme.textTheme.bodyLarge?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), @@ -101,138 +100,150 @@ class IntroPage extends HookConsumerWidget with PresLogger { ), ), ), - + // Настройки в виде карточек SliverCrossAxisConstrained( maxCrossAxisExtent: 400, child: MultiSliver( children: [ // Язык - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 6), - child: _SettingCard( - child: const LocalePrefTile(), + const SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24, vertical: 6), + child: _SettingCard( + child: LocalePrefTile(), + ), ), ), - + // Регион - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 6), - child: _SettingCard( - child: const RegionPrefTile(), + const SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24, vertical: 6), + child: _SettingCard( + child: RegionPrefTile(), + ), ), ), - + // Аналитика - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 6), - child: _SettingCard( - child: const EnableAnalyticsPrefTile(), + const SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24, vertical: 6), + child: _SettingCard( + child: EnableAnalyticsPrefTile(), + ), ), ), - + const SliverGap(16), - + // Условия использования - Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: Text.rich( - t.intro.termsAndPolicyCaution( - tap: (text) => TextSpan( - text: text, - style: TextStyle( - color: theme.colorScheme.primary, - fontWeight: FontWeight.w600, - decoration: TextDecoration.underline, + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Text.rich( + t.intro.termsAndPolicyCaution( + tap: (text) => TextSpan( + text: text, + style: TextStyle( + color: theme.colorScheme.primary, + fontWeight: FontWeight.w600, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const TermsAndConditionsScreen(), + ), + ); + }, ), - recognizer: TapGestureRecognizer() - ..onTap = () async { - await UriUtils.tryLaunch( - Uri.parse(Constants.termsAndConditionsUrl), - ); - }, ), + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, ), - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - textAlign: TextAlign.center, ), ), - + // Кнопка начать - Padding( - padding: const EdgeInsets.fromLTRB(24, 24, 24, 32), - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - theme.colorScheme.primary, - theme.colorScheme.primary.withOpacity(0.8), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.fromLTRB(24, 24, 24, 32), + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + theme.colorScheme.primary, + theme.colorScheme.primary.withOpacity(0.8), + ], + ), + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: theme.colorScheme.primary.withOpacity(0.4), + blurRadius: 12, + offset: const Offset(0, 6), + ), ], ), - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: theme.colorScheme.primary.withOpacity(0.4), - blurRadius: 12, - offset: const Offset(0, 6), - ), - ], - ), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: isStarting.value - ? null - : () async { - isStarting.value = true; - if (!ref.read(analyticsControllerProvider).requireValue) { - loggy.info("disabling analytics per user request"); - try { - await ref.read(analyticsControllerProvider.notifier).disableAnalytics(); - } catch (error, stackTrace) { - loggy.error( - "could not disable analytics", - error, - stackTrace, - ); + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: isStarting.value + ? null + : () async { + isStarting.value = true; + if (!ref.read(analyticsControllerProvider).requireValue) { + loggy.info("disabling analytics per user request"); + try { + await ref.read(analyticsControllerProvider.notifier).disableAnalytics(); + } catch (error, stackTrace) { + loggy.error( + "could not disable analytics", + error, + stackTrace, + ); + } } - } - await ref.read(Preferences.introCompleted.notifier).update(true); - }, - borderRadius: BorderRadius.circular(16), - child: Container( - height: 56, - alignment: Alignment.center, - child: isStarting.value - ? SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator( - strokeWidth: 3, - valueColor: AlwaysStoppedAnimation( - theme.colorScheme.onPrimary, - ), - ), - ) - : Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - t.intro.start, - style: theme.textTheme.titleMedium?.copyWith( - color: theme.colorScheme.onPrimary, - fontWeight: FontWeight.bold, + await ref.read(Preferences.introCompleted.notifier).update(true); + }, + borderRadius: BorderRadius.circular(16), + child: Container( + height: 56, + alignment: Alignment.center, + child: isStarting.value + ? SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 3, + valueColor: AlwaysStoppedAnimation( + theme.colorScheme.onPrimary, ), ), - const Gap(8), - Icon( - Icons.arrow_forward_rounded, - color: theme.colorScheme.onPrimary, - ), - ], - ), + ) + : Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + t.intro.start, + style: theme.textTheme.titleMedium?.copyWith( + color: theme.colorScheme.onPrimary, + fontWeight: FontWeight.bold, + ), + ), + const Gap(8), + Icon( + Icons.arrow_forward_rounded, + color: theme.colorScheme.onPrimary, + ), + ], + ), + ), ), ), ), @@ -266,28 +277,25 @@ class IntroPage extends HookConsumerWidget with PresLogger { ); } + // UMBRIX: Используем timezone вместо IP для приватности try { - final DioHttpClient client = DioHttpClient( - timeout: const Duration(seconds: 2), - userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0", - debug: true, - ); - final response = await client.get>('https://api.ip.sb/geoip/'); + final timezone = DateTime.now().timeZoneName; + final systemLocale = PlatformDispatcher.instance.locale; - if (response.statusCode == 200) { - final jsonData = response.data!; - final regionLocale = _getRegionLocale(jsonData['country_code']?.toString() ?? ""); + loggy.debug('Using timezone: $timezone, system locale: ${systemLocale.languageCode}'); - loggy.debug( - 'Region: ${regionLocale.region} Locale: ${regionLocale.locale}', - ); + // Определяем регион по системной локали (без интернет-запросов!) + final countryCode = systemLocale.countryCode ?? ''; + if (countryCode.isNotEmpty) { + final regionLocale = _getRegionLocale(countryCode); await ref.read(ConfigOptions.region.notifier).update(regionLocale.region); await ref.read(localePreferencesProvider.notifier).changeLocale(regionLocale.locale); + loggy.debug('Region set from system locale: ${regionLocale.region}'); } else { - loggy.warning('Request failed with status: ${response.statusCode}'); + loggy.debug('Could not determine region from system locale, using default'); } } catch (e) { - loggy.warning('Could not get the local country code from ip'); + loggy.warning('Could not auto-select region: $e'); } } @@ -327,7 +335,7 @@ class _SettingCard extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); - + return Container( decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHighest.withOpacity(0.5), diff --git a/lib/features/log/data/log_repository.dart b/lib/features/log/data/log_repository.dart index 4f4cb443..7c7c4f9f 100644 --- a/lib/features/log/data/log_repository.dart +++ b/lib/features/log/data/log_repository.dart @@ -1,3 +1,4 @@ +import 'dart:io'; import 'package:fpdart/fpdart.dart'; import 'package:hiddify/core/utils/exception_handler.dart'; import 'package:hiddify/features/log/data/log_parser.dart'; @@ -13,9 +14,7 @@ abstract interface class LogRepository { TaskEither clearLogs(); } -class LogRepositoryImpl - with ExceptionHandler, InfraLogger - implements LogRepository { +class LogRepositoryImpl with ExceptionHandler, InfraLogger implements LogRepository { LogRepositoryImpl({ required this.singbox, required this.logPathResolver, @@ -24,6 +23,10 @@ class LogRepositoryImpl final SingboxService singbox; final LogPathResolver logPathResolver; + // Ограничения на размер файлов логов + static const int _maxLogFileSize = 5 * 1024 * 1024; // 5 МБ + static const int _maxBackupFiles = 2; // Храним только 2 backup файла + @override TaskEither init() { return exceptionHandler( @@ -31,28 +34,84 @@ class LogRepositoryImpl if (!await logPathResolver.directory.exists()) { await logPathResolver.directory.create(recursive: true); } - if (await logPathResolver.coreFile().exists()) { - await logPathResolver.coreFile().writeAsString(""); - } else { - await logPathResolver.coreFile().create(recursive: true); - } - if (await logPathResolver.appFile().exists()) { - await logPathResolver.appFile().writeAsString(""); - } else { - await logPathResolver.appFile().create(recursive: true); - } + + // Инициализация core логов с ротацией + await _initLogFileWithRotation(logPathResolver.coreFile()); + + // Инициализация app логов с ротацией + await _initLogFileWithRotation(logPathResolver.appFile()); + return right(unit); }, LogUnexpectedFailure.new, ); } + /// Инициализация файла логов с автоматической ротацией + Future _initLogFileWithRotation(File logFile) async { + try { + if (await logFile.exists()) { + final fileSize = await logFile.length(); + + // Если файл превышает лимит - делаем ротацию + if (fileSize > _maxLogFileSize) { + loggy.info('Log file too large: ${fileSize / 1024 / 1024}MB, rotating...'); + await _rotateLogFile(logFile); + } else { + // Просто очищаем если размер нормальный + await logFile.writeAsString(""); + } + } else { + await logFile.create(recursive: true); + } + } catch (e, st) { + loggy.warning('Error initializing log file: $e', e, st); + // В случае ошибки просто создаём новый файл + await logFile.create(recursive: true); + } + } + + /// Ротация файла логов (создаём backup и очищаем текущий) + Future _rotateLogFile(File logFile) async { + try { + final basePath = logFile.path; + + // Удаляем самый старый backup если есть + for (int i = _maxBackupFiles; i > 0; i--) { + final oldBackup = File('$basePath.$i'); + if (i == _maxBackupFiles && await oldBackup.exists()) { + await oldBackup.delete(); + loggy.debug('Deleted old backup: $basePath.$i'); + } + + // Переименовываем backups (example.log.1 → example.log.2) + if (i > 1) { + final prevBackup = File('$basePath.${i - 1}'); + if (await prevBackup.exists()) { + await prevBackup.rename('$basePath.$i'); + } + } + } + + // Текущий файл → backup.1 + if (await logFile.exists()) { + await logFile.rename('$basePath.1'); + loggy.debug('Rotated log file to: $basePath.1'); + } + + // Создаём новый пустой файл + await logFile.create(); + loggy.info('Log file rotation completed successfully'); + } catch (e, st) { + loggy.warning('Error rotating log file: $e', e, st); + // В случае ошибки просто перезаписываем файл + await logFile.writeAsString(""); + } + } + @override Stream>> watchLogs() { - return singbox - .watchLogs(logPathResolver.coreFile().path) - .map((event) => event.map(LogParser.parseSingbox).toList()) - .handleExceptions( + return singbox.watchLogs(logPathResolver.coreFile().path).map((event) => event.map(LogParser.parseSingbox).toList()).handleExceptions( (error, stackTrace) { loggy.warning("error watching logs", error, stackTrace); return LogFailure.unexpected(error, stackTrace); diff --git a/lib/features/log/overview/logs_overview_notifier.dart b/lib/features/log/overview/logs_overview_notifier.dart index 8b3c4243..1374e389 100644 --- a/lib/features/log/overview/logs_overview_notifier.dart +++ b/lib/features/log/overview/logs_overview_notifier.dart @@ -85,13 +85,20 @@ class LogsOverviewNotifier extends _$LogsOverviewNotifier with AppLogger { LogLevel? _levelFilter; String _filter = ""; + // Ограничения для защиты памяти + static const int _maxLogsInMemory = 1000; // Максимум 1000 записей в памяти + static const int _trimTo = 500; // Обрезать до 500 при превышении + Future> _computeLogs() async { + // Защита от переполнения памяти + if (_logs.length > _maxLogsInMemory) { + loggy.info('Trimming logs: ${_logs.length} → $_trimTo (memory protection)'); + _logs = _logs.take(_trimTo); + } + if (_levelFilter == null && _filter.isEmpty) return _logs.toList(); return _logs.where((e) { - return (_filter.isEmpty || e.message.contains(_filter)) && - (_levelFilter == null || - e.level == null || - e.level!.index >= _levelFilter!.index); + return (_filter.isEmpty || e.message.contains(_filter)) && (_levelFilter == null || e.level == null || e.level!.index >= _levelFilter!.index); }).toList(); } diff --git a/lib/features/log/overview/logs_overview_page.dart b/lib/features/log/overview/logs_overview_page.dart index 33d3c817..f5a0733a 100644 --- a/lib/features/log/overview/logs_overview_page.dart +++ b/lib/features/log/overview/logs_overview_page.dart @@ -6,8 +6,11 @@ import 'package:gap/gap.dart'; import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/model/failures.dart'; import 'package:hiddify/core/preferences/general_preferences.dart'; +import 'package:hiddify/core/telegram/telegram_logger.dart'; +import 'package:hiddify/core/telegram_config.dart'; import 'package:hiddify/core/widget/adaptive_icon.dart'; import 'package:hiddify/features/common/nested_app_bar.dart'; +import 'package:hiddify/features/config_option/data/config_option_repository.dart'; import 'package:hiddify/features/log/data/log_data_providers.dart'; import 'package:hiddify/features/log/model/log_level.dart'; import 'package:hiddify/features/log/overview/logs_overview_notifier.dart'; @@ -29,28 +32,113 @@ class LogsOverviewPage extends HookConsumerWidget with PresLogger { final filterController = useTextEditingController(text: state.filter); - final List popupButtons = debug || PlatformUtils.isDesktop - ? [ - PopupMenuItem( - child: Text(t.logs.shareCoreLogs), - onTap: () async { - await UriUtils.tryShareOrLaunchFile( - Uri.parse(pathResolver.coreFile().path), - fileOrDir: pathResolver.directory.uri, + // UMBRIX: Кнопки логов доступны всегда (не только в debug режиме) + final List popupButtons = [ + PopupMenuItem( + child: Text(t.logs.shareCoreLogs), + onTap: () async { + await UriUtils.tryShareOrLaunchFile( + Uri.parse(pathResolver.coreFile().path), + fileOrDir: pathResolver.directory.uri, + ); + }, + ), + PopupMenuItem( + child: Text(t.logs.shareAppLogs), + onTap: () async { + await UriUtils.tryShareOrLaunchFile( + Uri.parse(pathResolver.appFile().path), + fileOrDir: pathResolver.directory.uri, + ); + }, + ), + ]; + + // Добавляем кнопку отправки в Telegram только если настроен + if (TelegramConfig.isConfigured) { + popupButtons.addAll([ + const PopupMenuDivider(), + PopupMenuItem( + child: const Row( + children: [ + Icon(FluentIcons.send_20_regular, size: 18), + Gap(8), + Text('Отправить разработчику'), + ], + ), + onTap: () async { + // Показываем диалог с подтверждением + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Отправить логи?'), + content: const Text( + 'Логи будут отправлены анонимно разработчику Umbrix через Telegram. ' + 'Это поможет улучшить приложение.\n\n' + 'Отправляется только общая информация об ОС, без личных данных.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Отмена'), + ), + FilledButton( + onPressed: () => Navigator.of(context).pop(true), + child: const Text('Отправить'), + ), + ], + ), + ); + + if (confirmed == true) { + // Показываем индикатор загрузки + if (!context.mounted) return; + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => const Center( + child: CircularProgressIndicator(), + ), + ); + + try { + final telegramLogger = TelegramLogger(); + final deviceInfo = TelegramLogger.getAnonymousDeviceInfo(); + + // Отправляем файл логов + final success = await telegramLogger.sendLogsAsFile( + pathResolver.appFile(), + deviceInfo: deviceInfo, ); - }, - ), - PopupMenuItem( - child: Text(t.logs.shareAppLogs), - onTap: () async { - await UriUtils.tryShareOrLaunchFile( - Uri.parse(pathResolver.appFile().path), - fileOrDir: pathResolver.directory.uri, + + if (!context.mounted) return; + Navigator.of(context).pop(); // Закрываем индикатор + + // Показываем результат + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + success ? '✅ Логи успешно отправлены! Спасибо за помощь!' : '❌ Ошибка отправки. Проверьте интернет-соединение.', + ), + duration: const Duration(seconds: 3), + ), ); - }, - ), - ] - : []; + } catch (e) { + if (!context.mounted) return; + Navigator.of(context).pop(); // Закрываем индикатор + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('❌ Ошибка отправки логов'), + duration: Duration(seconds: 3), + ), + ); + } + } + }, + ), + ]); + } return Scaffold( body: NestedScrollView( @@ -94,52 +182,169 @@ class LogsOverviewPage extends HookConsumerWidget with PresLogger { ], ), SliverPinnedHeader( - child: DecoratedBox( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, - ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - child: Row( - children: [ - Flexible( - child: TextFormField( - controller: filterController, - onChanged: notifier.filterMessage, - decoration: InputDecoration( - isDense: true, - hintText: t.logs.filterHint, - ), + child: Container( + color: Theme.of(context).colorScheme.surface, + child: Column( + children: [ + // Карточка настроек логирования + Container( + margin: const EdgeInsets.fromLTRB(16, 8, 16, 8), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHighest.withOpacity(0.3), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Theme.of(context).colorScheme.outline.withOpacity(0.2), ), ), - const Gap(16), - DropdownButton>( - value: optionOf(state.levelFilter), - onChanged: (v) { - if (v == null) return; - notifier.filterLevel(v.toNullable()); - }, - padding: - const EdgeInsets.symmetric(horizontal: 8), - borderRadius: BorderRadius.circular(4), - items: [ - DropdownMenuItem( - value: none(), - child: Text(t.logs.allLevelsFilter), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + FluentIcons.settings_20_regular, + size: 20, + color: Theme.of(context).colorScheme.primary, + ), + const Gap(8), + Text( + 'Настройки логирования', + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ], ), - ...LogLevel.choices.map( - (e) => DropdownMenuItem( - value: some(e), - child: Text(e.name), + const Gap(12), + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Уровень детализации', + style: Theme.of(context).textTheme.bodyMedium, + ), + const Gap(4), + Text( + 'Влияет на объём записываемой информации', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const Gap(2), + Row( + children: [ + Icon( + FluentIcons.shield_checkmark_20_regular, + size: 14, + color: Theme.of(context).colorScheme.primary.withOpacity(0.7), + ), + const Gap(4), + Text( + 'Макс. 1000 записей • 5 МБ на файл', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + fontSize: 11, + color: Theme.of(context).colorScheme.primary.withOpacity(0.7), + ), + ), + ], + ), + ], + ), + ), + const Gap(12), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(8), + ), + child: DropdownButton( + value: ref.watch(ConfigOptions.logLevel), + onChanged: (value) { + if (value != null) { + ref.read(ConfigOptions.logLevel.notifier).update(value); + } + }, + underline: const SizedBox(), + isDense: true, + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimaryContainer, + fontWeight: FontWeight.w600, + fontSize: 13, + ), + dropdownColor: Theme.of(context).colorScheme.primaryContainer, + items: LogLevel.choices.map((level) { + return DropdownMenuItem( + value: level, + child: Text(level.name.toUpperCase()), + ); + }).toList(), + ), + ), + ], + ), + ], + ), + ), + // Фильтры поиска + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: Row( + children: [ + Flexible( + child: TextFormField( + controller: filterController, + onChanged: notifier.filterMessage, + decoration: InputDecoration( + isDense: true, + hintText: t.logs.filterHint, + prefixIcon: const Icon(FluentIcons.search_20_regular, size: 18), + ), + ), + ), + const Gap(12), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.outline, + ), + borderRadius: BorderRadius.circular(8), + ), + child: DropdownButton>( + value: optionOf(state.levelFilter), + onChanged: (v) { + if (v == null) return; + notifier.filterLevel(v.toNullable()); + }, + underline: const SizedBox(), + borderRadius: BorderRadius.circular(8), + isDense: true, + items: [ + DropdownMenuItem( + value: none(), + child: Text(t.logs.allLevelsFilter), + ), + ...LogLevel.choices.map( + (e) => DropdownMenuItem( + value: some(e), + child: Text(e.name), + ), + ), + ], ), ), ], ), - ], - ), + ), + ], ), ), ), @@ -173,31 +378,24 @@ class LogsOverviewPage extends HookConsumerWidget with PresLogger { children: [ if (log.level != null) Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( log.level!.name.toUpperCase(), - style: Theme.of(context) - .textTheme - .labelMedium - ?.copyWith( + style: Theme.of(context).textTheme.labelMedium?.copyWith( color: log.level!.color, ), ), if (log.time != null) Text( log.time!.toString(), - style: Theme.of(context) - .textTheme - .labelSmall, + style: Theme.of(context).textTheme.labelSmall, ), ], ), Text( log.message, - style: - Theme.of(context).textTheme.bodySmall, + style: Theme.of(context).textTheme.bodySmall, ), ], ), diff --git a/lib/features/per_app_proxy/overview/per_app_proxy_page.dart b/lib/features/per_app_proxy/overview/per_app_proxy_page.dart index 6108d8a8..d5da61ac 100644 --- a/lib/features/per_app_proxy/overview/per_app_proxy_page.dart +++ b/lib/features/per_app_proxy/overview/per_app_proxy_page.dart @@ -2,11 +2,12 @@ import 'package:dartx/dartx.dart'; import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; 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/model/region.dart'; import 'package:hiddify/core/preferences/general_preferences.dart'; import 'package:hiddify/core/router/routes.dart'; import 'package:hiddify/features/common/nested_app_bar.dart'; +import 'package:hiddify/features/config_option/data/config_option_repository.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'; @@ -112,7 +113,7 @@ class PerAppProxyPage extends HookConsumerWidget with PresLogger { return Scaffold( body: CustomScrollView( slivers: [ - isSearching.value ? searchAppBar : appBar, + if (isSearching.value) searchAppBar else appBar, SliverFillRemaining( child: TabBarView( controller: tabController, @@ -145,63 +146,105 @@ class PerAppProxyPage extends HookConsumerWidget with PresLogger { if (currentTab.value == 1) Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - child: Row( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - 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 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), - ], + // Подсказка над кнопками + Padding( + padding: const EdgeInsets.only(left: 4.0, bottom: 8.0), + child: Text( + perAppProxyMode == PerAppProxyMode.include ? t.settings.network.perAppProxyModes.includeMsg : t.settings.network.perAppProxyModes.excludeMsg, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text(MaterialLocalizations.of(context).okButtonLabel), + ), + ), + Row( + children: [ + Expanded( + child: perAppProxyMode == PerAppProxyMode.include + ? ElevatedButton( + onPressed: null, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Theme.of(context).colorScheme.onPrimary, + disabledBackgroundColor: Theme.of(context).colorScheme.primary, + disabledForegroundColor: Theme.of(context).colorScheme.onPrimary, + textStyle: const TextStyle(fontSize: 13), + ), + child: Text(t.settings.network.perAppProxyModes.include), + ) + : OutlinedButton( + onPressed: () async { + await ref.read(Preferences.perAppProxyMode.notifier).update(PerAppProxyMode.include); + }, + style: OutlinedButton.styleFrom( + textStyle: const TextStyle(fontSize: 13), + ), + child: Text(t.settings.network.perAppProxyModes.include), + ), + ), + const SizedBox(width: 12), + Expanded( + child: perAppProxyMode == PerAppProxyMode.exclude + ? ElevatedButton( + onPressed: null, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Theme.of(context).colorScheme.onPrimary, + disabledBackgroundColor: Theme.of(context).colorScheme.primary, + disabledForegroundColor: Theme.of(context).colorScheme.onPrimary, + textStyle: const TextStyle(fontSize: 13), + ), + child: Text(t.settings.network.perAppProxyModes.exclude), + ) + : OutlinedButton( + onPressed: () async { + await ref.read(Preferences.perAppProxyMode.notifier).update(PerAppProxyMode.exclude); + }, + style: OutlinedButton.styleFrom( + textStyle: const TextStyle(fontSize: 13), + ), + 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, + ); + }, + tooltip: t.settings.network.perAppProxyPageTitle, + ), + ], ), ], ), @@ -314,11 +357,11 @@ class PerAppProxyPage extends HookConsumerWidget with PresLogger { ), switch (filteredPackages) { AsyncData(value: final packages) => packages.isEmpty - ? SliverFillRemaining( + ? const SliverFillRemaining( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text('No packages found'), + Text('No packages found'), ], ), ) @@ -354,11 +397,11 @@ class PerAppProxyPage extends HookConsumerWidget with PresLogger { }, itemCount: packages.length, ), - AsyncError() => SliverFillRemaining( + AsyncError() => const SliverFillRemaining( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text('Error loading packages'), + Text('Error loading packages'), ], ), ), @@ -378,15 +421,59 @@ class PerAppProxyPage extends HookConsumerWidget with PresLogger { final t = ref.read(translationsProvider); final excludedDomains = ref.read(excludedDomainsListProvider); - final presetZones = [ - '.ru', - '.рф', - '.su', - '.by', - '.kz', - '.ua', + // Получаем регион пользователя + final userRegion = ref.read(ConfigOptions.region); + + // Определяем зоны по регионам (СОКРАЩЕННЫЙ список для России) + final Map> regionZones = { + // Россия и СНГ (топ-6 самых важных) + Region.ru: ['.ru', '.рф', '.su', '.by', '.kz', '.ua'], + + // Иран и окружение + Region.ir: ['.ir', '.ایران', '.af', '.pk'], + + // Китай и Азия + Region.cn: ['.cn', '.中国', '.hk', '.tw'], + + // Индонезия и Юго-Восточная Азия + Region.id: ['.id', '.my', '.ph', '.vn', '.th'], + + // Турция и Тюркский мир + Region.tr: ['.tr', '.az', '.uz'], + + // Афганистан и окружение + Region.af: ['.af', '.pk', '.tj'], + + // Бразилия и Латинская Америка + Region.br: ['.br', '.pt', '.mx', '.ar'], + + // Индия и Южная Азия + Region.in_: ['.in', '.भारत', '.pk', '.bd'], + + // Остальные + Region.other: ['.com', '.org', '.net'], + }; + + // Популярные глобальные зоны (топ-10) + final globalZones = [ + '.com', + '.org', + '.net', + '.io', + '.ai', + '.co', + '.app', + '.dev', + '.xyz', + '.me', ]; + // Собираем все зоны: региональные + глобальные + final presetZones = { + ...?regionZones[userRegion], + ...globalZones, + }.toList(); // Убираем дубликаты + // Локальное состояние для выбранных зон final selectedZones = Set.from(excludedDomains.where((d) => presetZones.contains(d))); @@ -401,109 +488,131 @@ class PerAppProxyPage extends HookConsumerWidget with PresLogger { 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, + child: SingleChildScrollView( + 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(), + 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.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(height: 16), + Text( + t.settings.network.excludedDomains.addOwnDomain, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 4), + Text( + t.settings.network.excludedDomains.domainInputDescription, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + ), + ), + const SizedBox(height: 8), + TextField( + controller: controller, + decoration: InputDecoration( + hintText: t.settings.network.excludedDomains.domainInputHint, + border: const OutlineInputBorder(), ), - const SizedBox(width: 8), - ElevatedButton( - onPressed: () { - final newList = List.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), + ), + const SizedBox(height: 16), + Text( + t.settings.network.excludedDomains.selectReadyZones, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + // Ограничиваем высоту списка зон + ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.2, // Макс 20% высоты экрана ), - ], - ), - ], + child: ListView( + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + children: presetZones.map((zone) { + final zoneStr = zone as String; + final isSelected = selectedZones.contains(zoneStr); + return CheckboxListTile( + dense: true, + contentPadding: EdgeInsets.zero, + title: Text(zoneStr), + value: isSelected, + activeColor: Theme.of(context).colorScheme.primary, + checkColor: Theme.of(context).colorScheme.onPrimary, + onChanged: (selected) { + setState(() { + if (selected == true) { + selectedZones.add(zoneStr); + } else { + selectedZones.remove(zoneStr); + } + }); + }, + ); + }).toList(), + ), + ), + 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.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), + ), + ], + ), + ], + ), ), ), ), diff --git a/lib/features/profile/add/add_profile_modal.dart b/lib/features/profile/add/add_profile_modal.dart index 8da28c89..d0903394 100644 --- a/lib/features/profile/add/add_profile_modal.dart +++ b/lib/features/profile/add/add_profile_modal.dart @@ -1,4 +1,3 @@ -import 'package:combine/combine.dart'; import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -62,10 +61,10 @@ class AddProfileModal extends HookConsumerWidget { controller: scrollController, child: AnimatedSize( duration: const Duration(milliseconds: 250), - child: Builder( - builder: (context) { - // Fixed button width instead of using LayoutBuilder - final buttonWidth = (MediaQuery.of(context).size.width / 2) - (buttonsPadding + (buttonsGap / 2)); + 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)); return AnimatedCrossFade( firstChild: SizedBox( @@ -98,70 +97,42 @@ class AddProfileModal extends HookConsumerWidget { ), secondChild: Column( children: [ - // Заголовок Padding( - padding: const EdgeInsets.fromLTRB(24, 16, 24, 8), - child: Text( - t.profile.add.buttonText, - style: theme.textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ), - // Основные кнопки в виде больших карточек - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( + padding: const EdgeInsets.symmetric(horizontal: buttonsPadding), + child: Row( children: [ - _ModernButton( + _Button( key: const ValueKey("add_from_clipboard_button"), label: t.profile.add.fromClipboard, - subtitle: "Paste from clipboard", - icon: FluentIcons.clipboard_paste_24_filled, - gradient: LinearGradient( - colors: [ - theme.colorScheme.primaryContainer, - theme.colorScheme.secondaryContainer, - ], - ), + icon: FluentIcons.clipboard_paste_24_regular, + size: buttonWidth, onTap: () async { final captureResult = await Clipboard.getData(Clipboard.kTextPlain).then((value) => value?.text ?? ''); if (addProfileState.isLoading) return; ref.read(addProfileProvider.notifier).add(captureResult); }, ), - const Gap(12), + const Gap(buttonsGap), if (!PlatformUtils.isDesktop) - _ModernButton( + _Button( key: const ValueKey("add_by_qr_code_button"), label: t.profile.add.scanQr, - subtitle: "Camera scanner", - icon: FluentIcons.qr_code_24_filled, - gradient: LinearGradient( - colors: [ - theme.colorScheme.tertiaryContainer, - theme.colorScheme.primaryContainer.withOpacity(0.7), - ], - ), + icon: FluentIcons.qr_code_24_regular, + size: buttonWidth, onTap: () async { - final cr = await QRCodeScannerScreen().open(context); + final cr = await const QRCodeScannerScreen().open(context); + if (cr == null) return; if (addProfileState.isLoading) return; ref.read(addProfileProvider.notifier).add(cr); }, ) else - _ModernButton( + _Button( key: const ValueKey("add_manually_button"), label: t.profile.add.manually, - subtitle: "Create new config", - icon: FluentIcons.edit_24_filled, - gradient: LinearGradient( - colors: [ - theme.colorScheme.tertiaryContainer, - theme.colorScheme.primaryContainer.withOpacity(0.7), - ], - ), + icon: FluentIcons.add_24_regular, + size: buttonWidth, onTap: () async { context.pop(); await const NewProfileRoute().push(context); @@ -170,32 +141,87 @@ class AddProfileModal extends HookConsumerWidget { ], ), ), - const Gap(16), - // Дополнительные опции Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.symmetric( + horizontal: buttonsPadding, + vertical: 16, + ), child: Column( children: [ - _CompactButton( - key: const ValueKey("add_warp_button"), - label: t.profile.add.addWarp, - icon: FluentIcons.cloud_add_24_regular, - color: theme.colorScheme.primary, - onTap: () async { - await addProfileModal(context, ref); - }, + Semantics( + button: true, + child: SizedBox( + height: 36, + child: Material( + key: const ValueKey("add_warp_button"), + elevation: 8, + color: theme.colorScheme.surface, + surfaceTintColor: theme.colorScheme.surfaceTint, + shadowColor: Colors.transparent, + borderRadius: BorderRadius.circular(8), + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: () async { + await addProfileModal(context, ref); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + FluentIcons.add_24_regular, + color: theme.colorScheme.primary, + ), + const SizedBox(width: 8), + Text( + t.profile.add.addWarp, + style: theme.textTheme.labelLarge?.copyWith( + color: theme.colorScheme.primary, + ), + ), + ], + ), + ), + ), + ), ), - if (!PlatformUtils.isDesktop) const Gap(12), + if (!PlatformUtils.isDesktop) const SizedBox(height: 16), // Spacing between the buttons if (!PlatformUtils.isDesktop) - _CompactButton( - key: const ValueKey("add_manually_button"), - label: t.profile.add.manually, - icon: FluentIcons.edit_24_regular, - color: theme.colorScheme.secondary, - onTap: () async { - context.pop(); - await const NewProfileRoute().push(context); - }, + Semantics( + button: true, + child: SizedBox( + height: 36, + child: Material( + key: const ValueKey("add_manually_button"), + elevation: 8, + color: theme.colorScheme.surface, + surfaceTintColor: theme.colorScheme.surfaceTint, + shadowColor: Colors.transparent, + borderRadius: BorderRadius.circular(8), + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: () async { + context.pop(); + await const NewProfileRoute().push(context); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + FluentIcons.add_24_regular, + color: theme.colorScheme.primary, + ), + const SizedBox(width: 8), + Text( + t.profile.add.manually, + style: theme.textTheme.labelLarge?.copyWith( + color: theme.colorScheme.primary, + ), + ), + ], + ), + ), + ), + ), ), ], ), @@ -213,10 +239,10 @@ class AddProfileModal extends HookConsumerWidget { } Future addProfileModal(BuildContext context, WidgetRef ref) async { - final _prefs = ref.read(sharedPreferencesProvider).requireValue; - final _warp = ref.read(warpOptionNotifierProvider.notifier); - final _profile = ref.read(addProfileProvider.notifier); - final consent = (_prefs.getBool(warpConsentGiven) ?? false); + final prefs = ref.read(sharedPreferencesProvider).requireValue; + final warp = ref.read(warpOptionNotifierProvider.notifier); + final profile = ref.read(addProfileProvider.notifier); + final consent = prefs.getBool(warpConsentGiven) ?? false; final region = ref.read(ConfigOptions.region.notifier).raw(); context.pop(); @@ -231,10 +257,10 @@ class AddProfileModal extends HookConsumerWidget { if (agreed != true) return; } - await _prefs.setBool(warpConsentGiven, true); + await prefs.setBool(warpConsentGiven, true); var toast = notification.showInfoToast(t.profile.add.addingWarpMsg, duration: const Duration(milliseconds: 100)); toast?.pause(); - await _warp.generateWarpConfig(); + await warp.generateWarpConfig(); toast?.start(); // final accountId = _prefs.getString("warp2-account-id"); @@ -244,179 +270,17 @@ class AddProfileModal extends HookConsumerWidget { // if (!hasWarp2Config || true) { toast = notification.showInfoToast(t.profile.add.addingWarpMsg, duration: const Duration(milliseconds: 100)); toast?.pause(); - await _warp.generateWarp2Config(); + await warp.generateWarp2Config(); toast?.start(); // } if (region == "cn") { - await _profile.add("#profile-title: Hiddify WARP\nwarp://p1@auto#National&&detour=warp://p2@auto#WoW"); // + await profile.add("#profile-title: Hiddify WARP\nwarp://p1@auto#National&&detour=warp://p2@auto#WoW"); // } else { - await _profile.add("https://raw.githubusercontent.com/hiddify/hiddify-next/main/test.configs/warp"); // + await profile.add("https://raw.githubusercontent.com/hiddify/hiddify-next/main/test.configs/warp"); // } } } -// Современная большая кнопка с градиентом -class _ModernButton extends StatelessWidget { - const _ModernButton({ - super.key, - required this.label, - required this.subtitle, - required this.icon, - required this.gradient, - required this.onTap, - }); - - final String label; - final String subtitle; - final IconData icon; - final Gradient gradient; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return Semantics( - button: true, - child: Material( - elevation: 4, - shadowColor: theme.colorScheme.primary.withOpacity(0.2), - borderRadius: BorderRadius.circular(20), - clipBehavior: Clip.antiAlias, - child: InkWell( - onTap: onTap, - child: Container( - height: 80, - decoration: BoxDecoration( - gradient: gradient, - borderRadius: BorderRadius.circular(20), - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Row( - children: [ - Container( - width: 56, - height: 56, - decoration: BoxDecoration( - color: theme.colorScheme.surface.withOpacity(0.9), - borderRadius: BorderRadius.circular(16), - ), - child: Icon( - icon, - size: 28, - color: theme.colorScheme.primary, - ), - ), - const Gap(16), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - color: theme.colorScheme.onPrimaryContainer, - ), - ), - const Gap(2), - Text( - subtitle, - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onPrimaryContainer.withOpacity(0.7), - ), - ), - ], - ), - ), - Icon( - FluentIcons.chevron_right_24_regular, - color: theme.colorScheme.onPrimaryContainer.withOpacity(0.5), - ), - ], - ), - ), - ), - ), - ), - ); - } -} - -// Компактная кнопка для дополнительных опций -class _CompactButton extends StatelessWidget { - const _CompactButton({ - super.key, - required this.label, - required this.icon, - required this.color, - required this.onTap, - }); - - final String label; - final IconData icon; - final Color color; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return Semantics( - button: true, - child: Material( - elevation: 2, - color: theme.colorScheme.surface, - surfaceTintColor: theme.colorScheme.surfaceTint, - shadowColor: Colors.transparent, - borderRadius: BorderRadius.circular(12), - clipBehavior: Clip.antiAlias, - child: InkWell( - onTap: onTap, - child: Container( - height: 56, - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - children: [ - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: color.withOpacity(0.1), - borderRadius: BorderRadius.circular(10), - ), - child: Icon( - icon, - size: 20, - color: color, - ), - ), - const Gap(12), - Expanded( - child: Text( - label, - style: theme.textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w600, - color: color, - ), - ), - ), - Icon( - FluentIcons.add_24_regular, - size: 20, - color: color.withOpacity(0.5), - ), - ], - ), - ), - ), - ), - ); - } -} - class _Button extends StatelessWidget { const _Button({ super.key, diff --git a/lib/features/profile/data/profile_data_providers.dart b/lib/features/profile/data/profile_data_providers.dart index 52c636c3..ed902281 100644 --- a/lib/features/profile/data/profile_data_providers.dart +++ b/lib/features/profile/data/profile_data_providers.dart @@ -2,7 +2,6 @@ import 'package:hiddify/core/database/database_provider.dart'; import 'package:hiddify/core/directories/directories_provider.dart'; import 'package:hiddify/core/http_client/http_client_provider.dart'; import 'package:hiddify/features/config_option/data/config_option_data_providers.dart'; -import 'package:hiddify/features/config_option/notifier/config_option_notifier.dart'; import 'package:hiddify/features/profile/data/profile_data_source.dart'; import 'package:hiddify/features/profile/data/profile_path_resolver.dart'; import 'package:hiddify/features/profile/data/profile_repository.dart'; diff --git a/lib/features/profile/details/json_editor.dart b/lib/features/profile/details/json_editor.dart index 5994ca00..cb5d88cd 100644 --- a/lib/features/profile/details/json_editor.dart +++ b/lib/features/profile/details/json_editor.dart @@ -1,11 +1,8 @@ -library json_editor_flutter; -import 'dart:convert'; import 'dart:async'; +import 'dart:convert'; import 'dart:math'; -import 'dart:ui'; -import 'package:dartx/dartx.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -49,7 +46,7 @@ const Map> protocolSchemaValues = { "type": "xray", "tag": "xray-out", "xray_outbound_raw": {}, - "xray_fragment": {"packets": "tlshello", "interval": "1-10", "length": "1-10"} + "xray_fragment": {"packets": "tlshello", "interval": "1-10", "length": "1-10"}, }, "warp": {"type": "custom", "key": "", "host": "", "port": 808, "fake_packets": "1-10", "fake_packets_size": "1-10", "fake_packets_delay": "1-10", "fake_packets_mode": "m4"}, "vless": { @@ -107,7 +104,7 @@ const Map> protocolSchemaValues = { "password": "goofy_ahh_password", "tls": { "enabled": true, - } + }, }, "shadowsocks": { "type": "shadowsocks", @@ -159,7 +156,7 @@ const Map> protocolSchemaValues = { "fake_packets": "1-10", "fake_packets_size": "1-10", "fake_packets_delay": "1-10", - "fake_packets_mode": "m4" + "fake_packets_mode": "m4", }, "tuic": { "type": "tuic", @@ -191,16 +188,16 @@ const Map> protocolSchemaValues = { const Map>> exampleSchemaValues = { "config.outbounds.transport": { "browser user-agent": { - "header": {"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0"} - } + "header": {"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0"}, + }, }, "config.outbounds.tls": { "fragment": { - "tls_fragment": {"enabled": true, "size": "1-10", "sleep": "1-10"} + "tls_fragment": {"enabled": true, "size": "1-10", "sleep": "1-10"}, }, "utls": { - "utls": {"enabled": true, "fingerprint": "chrome"} - } + "utls": {"enabled": true, "fingerprint": "chrome"}, + }, }, "config.outbounds": { "multiplex": { @@ -211,7 +208,7 @@ const Map>> exampleSchemaValues = { "min_streams": 4, "max_streams": 0, "padding": false, - "brutal": {"enabled": true, "up_mbps": 100, "down_mbps": 100} + "brutal": {"enabled": true, "up_mbps": 100, "down_mbps": 100}, }, }, "reality": { @@ -221,7 +218,7 @@ const Map>> exampleSchemaValues = { "min_version": "", "max_version": "", "utls": {"enabled": true, "fingerprint": "chrome"}, - "reality": {"enabled": true, "public_key": "", "short_id": ""} + "reality": {"enabled": true, "public_key": "", "short_id": ""}, }, }, "tls": { @@ -234,25 +231,25 @@ const Map>> exampleSchemaValues = { "alpn": [], "min_version": "", "max_version": "", - "tls_fragment": {"enabled": false, "size": "1-10", "sleep": "1-10"} + "tls_fragment": {"enabled": false, "size": "1-10", "sleep": "1-10"}, }, }, "websocket": { - "transport": {"type": "ws", "path": "", "headers": {}, "max_early_data": 0, "early_data_header_name": ""} + "transport": {"type": "ws", "path": "", "headers": {}, "max_early_data": 0, "early_data_header_name": ""}, }, "grpc": { - "transport": {"type": "grpc", "service_name": "TunService", "idle_timeout": "15s", "ping_timeout": "15s", "permit_without_stream": false} + "transport": {"type": "grpc", "service_name": "TunService", "idle_timeout": "15s", "ping_timeout": "15s", "permit_without_stream": false}, }, "quic": { - "transport": {"type": "quic"} + "transport": {"type": "quic"}, }, "http": { - "transport": {"type": "http", "host": [], "path": "", "method": "", "headers": {}, "idle_timeout": "15s", "ping_timeout": "15s"} + "transport": {"type": "http", "host": [], "path": "", "method": "", "headers": {}, "idle_timeout": "15s", "ping_timeout": "15s"}, }, "httpupgrade": { - "transport": {"type": "httpupgrade", "host": "", "path": "", "headers": {}} + "transport": {"type": "httpupgrade", "host": "", "path": "", "headers": {}}, }, - } + }, }; const Map> possibleValues = { @@ -284,7 +281,7 @@ const Map> possibleValues = { "block", "socks", "http", - ] + ], }; /// Edit your JSON object with this Widget. Create, edit and format objects @@ -417,7 +414,7 @@ class _JsonEditorState extends State { Map getExpandedParents() { final map = {}; - for (var key in widget.expandedObjects) { + for (final key in widget.expandedObjects) { if (key is List) { final newExpandList = ["config", ...key]; for (int i = newExpandList.length - 1; i > 0; i--) { @@ -457,9 +454,9 @@ class _JsonEditorState extends State { }); } - void copyData() async { + Future copyData() async { await Clipboard.setData( - ClipboardData(text: JsonEncoder.withIndent(' ').convert(_data)), + ClipboardData(text: const JsonEncoder.withIndent(' ').convert(_data)), ); } @@ -478,7 +475,7 @@ class _JsonEditorState extends State { void findMatchingKeys(data, String text, List nestedParents) { if (data is Map) { final keys = data.keys.toList(); - for (var key in keys) { + for (final key in keys) { final keyName = key.toString(); if (keyName.toLowerCase().contains(text) || (data[key] is String && data[key].toString().toLowerCase().contains(text))) { _results = _results! + 1; @@ -533,7 +530,7 @@ class _JsonEditorState extends State { void calculateOffset(data, List parents, List toFind) { if (keyFound) return; if (data is Map) { - for (var entry in data.entries) { + for (final entry in data.entries) { if (keyFound) return; offset++; final newList = [...parents, entry.key]; @@ -600,7 +597,7 @@ class _JsonEditorState extends State { void expandAllObjects(data, List expandedList) { if (data is Map) { - for (var entry in data.entries) { + for (final entry in data.entries) { if (entry.value is Map || entry.value is List) { final newList = [...expandedList, entry.key]; _expandedObjects[newList.toString()] = true; @@ -664,7 +661,7 @@ class _JsonEditorState extends State { ? const Border( bottom: BorderSide(color: Colors.red, width: 2), ) - : null), + : null,), child: Padding( padding: const EdgeInsets.symmetric( vertical: 6, @@ -802,7 +799,6 @@ class _JsonEditorState extends State { controller: _controller, onChanged: parseData, maxLines: null, - minLines: null, expands: true, textAlignVertical: TextAlignVertical.top, decoration: const InputDecoration( @@ -853,7 +849,7 @@ class _Holder extends StatefulWidget { // ? '.${parentObject['type']}' // : ''; - return '$basePath'; + return basePath; } @override @@ -885,9 +881,9 @@ class _HolderState extends State<_Holder> { widget.setState(() {}); } else if (selectedItem == "map") { if (widget.data is Map) { - widget.data[_newKey] = Map(); + widget.data[_newKey] = {}; } else { - widget.data.add(Map()); + widget.data.add({}); } setState(() {}); @@ -951,7 +947,7 @@ class _HolderState extends State<_Holder> { var res = "{"; if (data is Map) { if (widget.expandedObjects[widget.allParents.toString()] ?? false) return ""; - final content = data as Map; + final content = data; //res += "${data.length}"; if (content["type"] != null) { res += "${content["type"]}"; @@ -963,10 +959,10 @@ class _HolderState extends State<_Holder> { res += " [${d.substring(0, min(20, d.length))}...]"; } } else if (data is List) { - final content = data as List; + final content = data; res += "${content.length}"; } - return res + "}"; + return "$res}"; } @override @@ -975,7 +971,7 @@ class _HolderState extends State<_Holder> { final mapWidget = []; final widgetData = widget.data as Map; final List keys = widgetData.keys.toList(); - for (var key in keys) { + for (final key in keys) { mapWidget.add(_Holder( key: Key(key), data: widget.data[key], @@ -987,7 +983,7 @@ class _HolderState extends State<_Holder> { matchedKeys: widget.matchedKeys, allParents: [...widget.allParents, key], expandedObjects: widget.expandedObjects, - )); + ),); } return Column( @@ -1065,7 +1061,7 @@ class _HolderState extends State<_Holder> { matchedKeys: widget.matchedKeys, allParents: [...widget.allParents, i], expandedObjects: widget.expandedObjects, - )); + ),); } return Column( @@ -1280,8 +1276,7 @@ class _ReplaceTextWithFieldState extends State<_ReplaceTextWithField> { child: DropdownButton( hint: Text('Select ${widget.keyPath.replaceAll("config.outbounds", "")}'), value: _text, - icon: Icon(Icons.arrow_downward), - iconSize: 24, + icon: const Icon(Icons.arrow_downward), elevation: 16, underline: Container( height: 2, @@ -1384,7 +1379,7 @@ class _Options extends StatelessWidget { SizedBox(width: 10), Text("Insert", style: TextStyle(fontSize: 14)), ], - )), + ),), if (keyPath != "config" && T == List) const _PopupMenuWidget(Row( mainAxisSize: MainAxisSize.min, @@ -1394,20 +1389,20 @@ class _Options extends StatelessWidget { SizedBox(width: 10), Text("Append", style: TextStyle(fontSize: 14)), ], - )), + ),), if (keyPath != "config" && (T == Map || T == List)) ...[ if (keyPath == "config.outbounds" && T == List) ...[ for (final String key in protocolSchemaValues.keys) ...{ PopupMenuItem<_OptionItems>( height: _popupMenuHeight, - padding: EdgeInsets.only(left: _popupMenuItemPadding), + padding: const EdgeInsets.only(left: _popupMenuItemPadding), value: key, child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.data_object), - SizedBox(width: 10), - Text(key, style: TextStyle(fontSize: 14)), + const Icon(Icons.data_object), + const SizedBox(width: 10), + Text(key, style: const TextStyle(fontSize: 14)), ], ), ), @@ -1420,14 +1415,14 @@ class _Options extends StatelessWidget { for (final String key2 in exampleSchemaValues[key]!.keys) ...{ PopupMenuItem<_OptionItems>( height: _popupMenuHeight, - padding: EdgeInsets.only(left: _popupMenuItemPadding), - value: key + "___" + key2, + padding: const EdgeInsets.only(left: _popupMenuItemPadding), + value: "${key}___$key2", child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.data_object), - SizedBox(width: 10), - Text(key2, style: TextStyle(fontSize: 14)), + const Icon(Icons.data_object), + const SizedBox(width: 10), + Text(key2, style: const TextStyle(fontSize: 14)), ], ), ), @@ -1573,12 +1568,12 @@ class _SearchField extends StatelessWidget { decoration: InputDecoration( hintText: "Search", hintStyle: Theme.of(context).textTheme.bodySmall, - constraints: BoxConstraints(maxWidth: 100), + constraints: const BoxConstraints(maxWidth: 100), border: InputBorder.none, // fillColor: Colors.transparent, // filled: true, isDense: true, - contentPadding: EdgeInsets.all(3), + contentPadding: const EdgeInsets.all(3), focusedBorder: InputBorder.none, // hoverColor: Colors.transparent, ), diff --git a/lib/features/profile/details/profile_details_notifier.dart b/lib/features/profile/details/profile_details_notifier.dart index ca168a07..75aee249 100644 --- a/lib/features/profile/details/profile_details_notifier.dart +++ b/lib/features/profile/details/profile_details_notifier.dart @@ -54,9 +54,9 @@ class ProfileDetailsNotifier extends _$ProfileDetailsNotifier with AppLogger { if (configContent.isNotEmpty) { try { final jsonObject = jsonDecode(configContent); - List> res = []; + final List> res = []; if (jsonObject is Map && jsonObject['outbounds'] is List) { - for (var outbound in jsonObject['outbounds'] as List) { + for (final outbound in jsonObject['outbounds'] as List) { if (outbound is Map && outbound['type'] != null && !['selector', 'urltest', 'dns', 'block'].contains(outbound['type']) && !['direct', 'bypass', 'direct-fragment'].contains(outbound['tag'])) { res.add(outbound); } diff --git a/lib/features/profile/details/profile_details_page.dart b/lib/features/profile/details/profile_details_page.dart index ba1d8976..9fca35f9 100644 --- a/lib/features/profile/details/profile_details_page.dart +++ b/lib/features/profile/details/profile_details_page.dart @@ -279,7 +279,7 @@ class ProfileDetailsPage extends HookConsumerWidget with PresLogger { expandedObjects: const ["outbounds"], onChanged: (value) { if (value == null) return; - const encoder = const JsonEncoder.withIndent(' '); + const encoder = JsonEncoder.withIndent(' '); notifier.setField(configContent: encoder.convert(value)); }, diff --git a/lib/features/profile/notifier/profile_notifier.dart b/lib/features/profile/notifier/profile_notifier.dart index 9a6304ee..cef2a400 100644 --- a/lib/features/profile/notifier/profile_notifier.dart +++ b/lib/features/profile/notifier/profile_notifier.dart @@ -76,7 +76,7 @@ class AddProfile extends _$AddProfile with AppLogger { } else if (LinkParser.protocol(rawInput) case (final parsed)?) { loggy.debug("adding profile, content"); var name = parsed.name; - var oldItem = await _profilesRepo.getByName(name); + final oldItem = await _profilesRepo.getByName(name); if (name == "Hiddify WARP" && oldItem != null) { _profilesRepo.deleteById(oldItem.id).run(); } @@ -111,10 +111,10 @@ class AddProfile extends _$AddProfile with AppLogger { Future check4Warp(String rawInput) async { for (final line in rawInput.split("\n")) { if (line.toLowerCase().startsWith("warp://")) { - final _prefs = ref.read(sharedPreferencesProvider).requireValue; - final _warp = ref.read(warpOptionNotifierProvider.notifier); + final prefs = ref.read(sharedPreferencesProvider).requireValue; + final warp = ref.read(warpOptionNotifierProvider.notifier); - final consent = false && (_prefs.getBool(WarpOptionNotifier.warpConsentGiven) ?? false); + final consent = false && (prefs.getBool(WarpOptionNotifier.warpConsentGiven) ?? false); final t = ref.read(translationsProvider); final notification = ref.read(inAppNotificationControllerProvider); @@ -126,24 +126,24 @@ class AddProfile extends _$AddProfile with AppLogger { ); if (agreed ?? false) { - await _prefs.setBool(WarpOptionNotifier.warpConsentGiven, true); + await prefs.setBool(WarpOptionNotifier.warpConsentGiven, true); final toast = notification.showInfoToast(t.profile.add.addingWarpMsg, duration: const Duration(milliseconds: 100)); toast?.pause(); - await _warp.generateWarpConfig(); + await warp.generateWarpConfig(); toast?.start(); } else { return; } } - final accountId = _prefs.getString("warp2-account-id"); - final accessToken = _prefs.getString("warp2-access-token"); + final accountId = prefs.getString("warp2-account-id"); + final accessToken = prefs.getString("warp2-access-token"); final hasWarp2Config = accountId != null && accessToken != null; if (!hasWarp2Config || true) { final toast = notification.showInfoToast(t.profile.add.addingWarpMsg, duration: const Duration(milliseconds: 100)); toast?.pause(); - await _warp.generateWarp2Config(); + await warp.generateWarp2Config(); toast?.start(); } } diff --git a/lib/features/profile/overview/profiles_overview_page.dart b/lib/features/profile/overview/profiles_overview_page.dart index ef542626..f1188523 100644 --- a/lib/features/profile/overview/profiles_overview_page.dart +++ b/lib/features/profile/overview/profiles_overview_page.dart @@ -80,6 +80,13 @@ class ProfilesOverviewModal extends HookConsumerWidget { onPressed: () { const AddProfileRoute().push(context); }, + style: FilledButton.styleFrom( + backgroundColor: Theme.of(context).brightness == Brightness.dark + ? const Color(0xFF37474F) // Светлее для темной темы + : const Color(0xFF263238), // Темнее для светлой темы + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + ), icon: const Icon(FluentIcons.add_24_filled), label: Text(t.profile.add.shortBtnTxt), ), @@ -92,6 +99,13 @@ class ProfilesOverviewModal extends HookConsumerWidget { }, ); }, + style: FilledButton.styleFrom( + backgroundColor: Theme.of(context).brightness == Brightness.dark + ? const Color(0xFF37474F) // Светлее для темной темы + : const Color(0xFF263238), // Темнее для светлой темы + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + ), icon: const Icon(FluentIcons.arrow_sort_24_filled), label: Text(t.general.sort), ), @@ -103,6 +117,13 @@ class ProfilesOverviewModal extends HookConsumerWidget { ) .trigger(); }, + style: FilledButton.styleFrom( + backgroundColor: Theme.of(context).brightness == Brightness.dark + ? const Color(0xFF37474F) // Светлее для темной темы + : const Color(0xFF263238), // Темнее для светлой темы + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + ), icon: const Icon(FluentIcons.arrow_sync_24_filled), label: Text(t.profile.update.updateSubscriptions), ), @@ -123,8 +144,7 @@ class ProfilesSortModal extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); - final sortNotifier = - ref.watch(profilesOverviewSortNotifierProvider.notifier); + final sortNotifier = ref.watch(profilesOverviewSortNotifierProvider.notifier); return AlertDialog( title: Text(t.general.sortBy), @@ -137,8 +157,7 @@ class ProfilesSortModal extends HookConsumerWidget { ...ProfilesSort.values.map( (e) { final selected = sort.by == e; - final double arrowTurn = - sort.mode == SortMode.ascending ? 0 : 0.5; + final double arrowTurn = sort.mode == SortMode.ascending ? 0 : 0.5; return ListTile( title: Text(e.present(t)), diff --git a/lib/features/profile/widget/profile_tile.dart b/lib/features/profile/widget/profile_tile.dart index 324d524f..588d75df 100644 --- a/lib/features/profile/widget/profile_tile.dart +++ b/lib/features/profile/widget/profile_tile.dart @@ -63,7 +63,6 @@ class ProfileTile extends HookConsumerWidget { ), shadowColor: Colors.transparent, child: Row( - crossAxisAlignment: CrossAxisAlignment.center, children: [ if (profile is RemoteProfileEntity || !isMain) ...[ SizedBox( diff --git a/lib/features/proxy/active/active_proxy_delay_indicator.dart b/lib/features/proxy/active/active_proxy_delay_indicator.dart index db070750..fe1fe9d7 100644 --- a/lib/features/proxy/active/active_proxy_delay_indicator.dart +++ b/lib/features/proxy/active/active_proxy_delay_indicator.dart @@ -1,4 +1,3 @@ -import 'dart:io'; import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; @@ -6,11 +5,8 @@ import 'package:gap/gap.dart'; import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/widget/animated_visibility.dart'; import 'package:hiddify/core/widget/shimmer_skeleton.dart'; -import 'package:hiddify/features/connection/model/connection_status.dart'; import 'package:hiddify/features/proxy/active/active_proxy_notifier.dart'; -import 'package:hiddify/features/system_tray/notifier/system_tray_notifier.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:tray_manager/tray_manager.dart'; class ActiveProxyDelayIndicator extends HookConsumerWidget { const ActiveProxyDelayIndicator({super.key}); diff --git a/lib/features/proxy/active/active_proxy_notifier.dart b/lib/features/proxy/active/active_proxy_notifier.dart index 75d4c138..01896be5 100644 --- a/lib/features/proxy/active/active_proxy_notifier.dart +++ b/lib/features/proxy/active/active_proxy_notifier.dart @@ -95,7 +95,7 @@ class ActiveProxyNotifier extends _$ActiveProxyNotifier with AppLogger { final _urlTestThrottler = Throttler(const Duration(seconds: 2)); Future urlTest(String groupTag_) async { - var groupTag = groupTag_; + final groupTag = groupTag_; _urlTestThrottler( () async { if (state case AsyncData()) { diff --git a/lib/features/proxy/active/ip_widget.dart b/lib/features/proxy/active/ip_widget.dart index ef775308..daa87c23 100644 --- a/lib/features/proxy/active/ip_widget.dart +++ b/lib/features/proxy/active/ip_widget.dart @@ -133,7 +133,7 @@ class IPCountryFlag extends HookConsumerWidget { padding: const EdgeInsets.all(2), child: Center( child: CircleFlag( - countryCode.toLowerCase() == "ir" ? "ir-shir" : countryCode), + countryCode.toLowerCase() == "ir" ? "ir-shir" : countryCode,), ), ), ); diff --git a/lib/features/proxy/model/proxy_entity.dart b/lib/features/proxy/model/proxy_entity.dart index 8b6c8472..815d55d9 100644 --- a/lib/features/proxy/model/proxy_entity.dart +++ b/lib/features/proxy/model/proxy_entity.dart @@ -29,10 +29,33 @@ class ProxyItemEntity with _$ProxyItemEntity { }) = _ProxyItemEntity; String get name => _sanitizedTag(tag); - String? get selectedName => - selectedTag == null ? null : _sanitizedTag(selectedTag!); + String? get selectedName => selectedTag == null ? null : _sanitizedTag(selectedTag!); bool get isVisible => !tag.contains("§hide§"); + + /// Извлекает код страны из названия прокси (emoji флаг или текст) + String get countryCode { + // Парсинг emoji флагов (🇩🇪 = U+1F1E6-1F1FF региональные индикаторы) + final runes = name.runes.toList(); + if (runes.length >= 2) { + final first = runes[0]; + final second = runes[1]; + // Проверка Regional Indicator Symbols + if (first >= 0x1F1E6 && first <= 0x1F1FF && + second >= 0x1F1E6 && second <= 0x1F1FF) { + final cc1 = String.fromCharCode(first - 0x1F1E6 + 65); // A-Z + final cc2 = String.fromCharCode(second - 0x1F1E6 + 65); + return cc1 + cc2; + } + } + + // Fallback: текстовые коды в начале или конце названия + final match = RegExp(r'^([A-Z]{2})[-_\s]|[-_\s]([A-Z]{2})$|^\[([A-Z]{2})\]').firstMatch(name); + if (match != null) { + return match.group(1) ?? match.group(2) ?? match.group(3) ?? 'XX'; + } + + return 'XX'; // Неизвестная страна + } } -String _sanitizedTag(String tag) => - tag.replaceFirst(RegExp(r"\§[^]*"), "").trimRight(); +String _sanitizedTag(String tag) => tag.replaceFirst(RegExp(r"\§[^]*"), "").trimRight(); diff --git a/lib/features/proxy/overview/proxies_overview_notifier.dart b/lib/features/proxy/overview/proxies_overview_notifier.dart index b9282341..be44516a 100644 --- a/lib/features/proxy/overview/proxies_overview_notifier.dart +++ b/lib/features/proxy/overview/proxies_overview_notifier.dart @@ -139,6 +139,10 @@ class ProxiesOverviewNotifier extends _$ProxiesOverviewNotifier with AppLogger { ), ], ).copyWithPrevious(state); + + // Автоматически запускаем URL test после переключения + // чтобы обновить задержки для всех прокси + await urlTest(groupTag); } } diff --git a/lib/features/proxy/overview/proxies_overview_page.dart b/lib/features/proxy/overview/proxies_overview_page.dart index dfa49942..1ed00b3e 100644 --- a/lib/features/proxy/overview/proxies_overview_page.dart +++ b/lib/features/proxy/overview/proxies_overview_page.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/model/failures.dart'; import 'package:hiddify/features/common/nested_app_bar.dart'; +import 'package:hiddify/features/proxy/model/proxy_entity.dart'; import 'package:hiddify/features/proxy/overview/proxies_overview_notifier.dart'; import 'package:hiddify/features/proxy/widget/proxy_tile.dart'; import 'package:hiddify/utils/utils.dart'; @@ -65,65 +66,78 @@ class ProxiesOverviewPage extends HookConsumerWidget with PresLogger { ); } - final group = groups.first; + // Находим группы "select" и "auto" + final selectGroup = groups.firstWhere( + (g) => g.type.name == 'selector', + orElse: () => groups.first, + ); + final autoGroup = groups.firstWhere( + (g) => g.type.name == 'urltest', + orElse: () => groups.last, + ); + + // Текущий выбор из группы "select" + final currentSelection = selectGroup.selected; + + // Все прокси берём из группы "auto" + final allProxies = autoGroup.items; + + // Группировка прокси по странам + final proxiesByCountry = >{}; + for (final proxy in allProxies) { + final country = proxy.countryCode; + proxiesByCountry.putIfAbsent(country, () => []).add(proxy); + } + + // Сортировка стран по средней задержке + final sortedCountries = proxiesByCountry.keys.toList() + ..sort((a, b) { + final avgA = _averageDelay(proxiesByCountry[a]!); + final avgB = _averageDelay(proxiesByCountry[b]!); + return avgA.compareTo(avgB); + }); return Scaffold( body: CustomScrollView( slivers: [ appBar, - SliverLayoutBuilder( - builder: (context, constraints) { - final width = constraints.crossAxisExtent; - if (!PlatformUtils.isDesktop && width < 648) { - return SliverPadding( - padding: const EdgeInsets.only(bottom: 86), - sliver: SliverList.builder( - itemBuilder: (_, index) { - final proxy = group.items[index]; - return ProxyTile( - proxy, - selected: group.selected == proxy.tag, - onSelect: () async { - if (selectActiveProxyMutation.state.isInProgress) { - return; - } - selectActiveProxyMutation.setFuture( - notifier.changeProxy(group.tag, proxy.tag), - ); - }, - ); - }, - itemCount: group.items.length, - ), - ); - } - - return SliverGrid.builder( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: (width / 268).floor(), - mainAxisExtent: 68, - ), - itemBuilder: (context, index) { - final proxy = group.items[index]; - return ProxyTile( - proxy, - selected: group.selected == proxy.tag, - onSelect: () async { - if (selectActiveProxyMutation.state.isInProgress) { - return; - } - selectActiveProxyMutation.setFuture( - notifier.changeProxy( - group.tag, - proxy.tag, - ), - ); - }, + SliverPadding( + padding: const EdgeInsets.only(bottom: 86, left: 12, right: 12, top: 8), + sliver: SliverList.builder( + // +1 для глобальной карточки "Авто по всем" + itemCount: 1 + sortedCountries.length, + itemBuilder: (context, index) { + // Первая карточка - "Авто по всем локациям" + if (index == 0) { + return _GlobalAutoCard( + currentSelection: currentSelection, + avgDelay: autoGroup.items.firstOrNull?.urlTestDelay.toDouble() ?? 999999.0, + selectGroup: selectGroup, + selectActiveProxyMutation: selectActiveProxyMutation, + notifier: notifier, + t: t, ); - }, - itemCount: group.items.length, - ); - }, + } + + // Остальные карточки - страны + final countryIndex = index - 1; + final countryCode = sortedCountries[countryIndex]; + final proxies = proxiesByCountry[countryCode]!; + final avgDelay = _averageDelay(proxies); + + return _CountryGroupCard( + countryCode: countryCode, + proxies: proxies, + avgDelay: avgDelay, + selectGroup: selectGroup, + currentSelection: currentSelection, + selectActiveProxyMutation: selectActiveProxyMutation, + notifier: notifier, + countryFlag: _countryFlag(countryCode), + delayColor: _delayColor(context, avgDelay), + ); + }, + ), ), ], ), @@ -150,7 +164,7 @@ class ProxiesOverviewPage extends HookConsumerWidget with PresLogger { ], ), child: FloatingActionButton( - onPressed: () async => notifier.urlTest(group.tag), + onPressed: () async => notifier.urlTest(selectGroup.tag), tooltip: t.proxies.delayTestTooltip, backgroundColor: Colors.transparent, elevation: 0, @@ -189,4 +203,316 @@ class ProxiesOverviewPage extends HookConsumerWidget with PresLogger { return const Scaffold(); } } + + // Вычисление средней задержки + double _averageDelay(List proxies) { + if (proxies.isEmpty) return 999999.0; + final total = proxies.fold(0, (sum, p) => sum + p.urlTestDelay); + return total / proxies.length; + } + + // Генерация emoji флага из кода страны + String _countryFlag(String countryCode) { + if (countryCode == 'XX' || countryCode.length != 2) { + return '❓'; // Неизвестная страна + } + final first = 0x1F1E6 + (countryCode.codeUnitAt(0) - 65); + final second = 0x1F1E6 + (countryCode.codeUnitAt(1) - 65); + return String.fromCharCodes([first, second]); + } + + // Цвет задержки + Color _delayColor(BuildContext context, double delay) { + return _getDelayColor(context, delay); + } +} + +// Глобальная функция для определения цвета задержки +Color _getDelayColor(BuildContext context, double delay) { + if (delay >= 999999) return Colors.grey; + + if (Theme.of(context).brightness == Brightness.dark) { + return switch (delay) { + < 800 => Colors.lightGreen, + < 1500 => Colors.orange, + _ => Colors.redAccent, + }; + } + + return switch (delay) { + < 800 => Colors.green, + < 1500 => Colors.deepOrangeAccent, + _ => Colors.red, + }; +} + +// Виджет глобальной карточки "Авто по всем локациям" +class _GlobalAutoCard extends StatelessWidget { + final String currentSelection; + final double avgDelay; + final ProxyGroupEntity selectGroup; + final ({ + AsyncMutation state, + ValueChanged> setFuture, + ValueChanged setOnFailure, + }) selectActiveProxyMutation; + final ProxiesOverviewNotifier notifier; + final Translations t; + + const _GlobalAutoCard({ + required this.currentSelection, + required this.avgDelay, + required this.selectGroup, + required this.selectActiveProxyMutation, + required this.notifier, + required this.t, + }); + + @override + Widget build(BuildContext context) { + final isSelected = currentSelection.toLowerCase() == 'auto'; + + return Card( + margin: const EdgeInsets.only(bottom: 8), + color: isSelected ? Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3) : null, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: isSelected + ? BorderSide( + color: Theme.of(context).colorScheme.primary, + width: 2, + ) + : BorderSide.none, + ), + child: InkWell( + onTap: () async { + if (selectActiveProxyMutation.state.isInProgress) return; + // Выбираем "auto" - автовыбор по всем странам + selectActiveProxyMutation.setFuture( + notifier.changeProxy(selectGroup.tag, 'auto'), + ); + }, + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + // Иконка глобуса + const Text('🌍', style: TextStyle(fontSize: 32)), + const SizedBox(width: 16), + // Название и описание + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + t.proxies.globalAuto, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + const SizedBox(width: 8), + if (isSelected) + Icon( + Icons.check_circle, + color: Theme.of(context).colorScheme.primary, + size: 20, + ), + ], + ), + const SizedBox(height: 4), + Text( + t.proxies.globalAutoDesc, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 12, + ), + ), + ], + ), + ), + // Задержка + Text( + avgDelay >= 999999 ? 'timeout' : '${avgDelay.toInt()}ms', + style: TextStyle( + color: _getDelayColor(context, avgDelay), + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ), + ], + ), + ), + ), + ); + } +} + +// Виджет карточки группы по стране с раскрывающимся списком +class _CountryGroupCard extends StatefulWidget { + final String countryCode; + final List proxies; + final double avgDelay; + final ProxyGroupEntity selectGroup; + final String currentSelection; + final ({ + AsyncMutation state, + ValueChanged> setFuture, + ValueChanged setOnFailure, + }) selectActiveProxyMutation; + final ProxiesOverviewNotifier notifier; + final String countryFlag; + final Color delayColor; + + const _CountryGroupCard({ + required this.countryCode, + required this.proxies, + required this.avgDelay, + required this.selectGroup, + required this.currentSelection, + required this.selectActiveProxyMutation, + required this.notifier, + required this.countryFlag, + required this.delayColor, + }); + + @override + State<_CountryGroupCard> createState() => _CountryGroupCardState(); +} + +class _CountryGroupCardState extends State<_CountryGroupCard> { + bool _isExpanded = false; + + @override + Widget build(BuildContext context) { + // Проверяем выбран ли один из прокси этой страны + final selectedProxyInCountry = widget.proxies.any( + (p) => p.tag == widget.currentSelection, + ); + + return Card( + margin: const EdgeInsets.only(bottom: 8), + color: selectedProxyInCountry ? Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3) : null, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: selectedProxyInCountry + ? BorderSide( + color: Theme.of(context).colorScheme.primary, + width: 2, + ) + : BorderSide.none, + ), + child: Column( + children: [ + // Основная карточка - клик выбирает первый прокси из этой страны + InkWell( + onTap: () async { + if (widget.selectActiveProxyMutation.state.isInProgress) return; + + // Выбираем первый прокси из этой страны (обычно лучший) + final firstProxy = widget.proxies.firstOrNull; + if (firstProxy != null) { + widget.selectActiveProxyMutation.setFuture( + widget.notifier.changeProxy(widget.selectGroup.tag, firstProxy.tag), + ); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + // Флаг + Text( + widget.countryFlag, + style: const TextStyle(fontSize: 32), + ), + const SizedBox(width: 16), + + // Название и количество + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + widget.countryCode == 'XX' ? 'Other' : widget.countryCode, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + const SizedBox(width: 8), + Text( + '(${widget.proxies.length})', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 14, + ), + ), + ], + ), + if (widget.countryCode == 'XX') + Text( + 'Автовыбор из всех локаций', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 12, + ), + ), + ], + ), + ), + + // Задержка + Text( + widget.avgDelay >= 999999 ? 'timeout' : '${widget.avgDelay.toInt()}ms', + style: TextStyle( + color: widget.delayColor, + fontWeight: FontWeight.w600, + ), + ), + + // Кнопка раскрытия (треугольник) - только если не XX + if (widget.countryCode != 'XX') + IconButton( + icon: Icon( + _isExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, + ), + onPressed: () { + setState(() { + _isExpanded = !_isExpanded; + }); + }, + ), + ], + ), + ), + ), + + // Раскрывающийся список прокси + if (_isExpanded) ...[ + // Все прокси из этой страны + ...widget.proxies.map((proxy) { + return ProxyTile( + proxy, + selected: widget.currentSelection == proxy.tag, + onSelect: () async { + if (widget.selectActiveProxyMutation.state.isInProgress) { + return; + } + widget.selectActiveProxyMutation.setFuture( + widget.notifier.changeProxy(widget.selectGroup.tag, proxy.tag), + ); + }, + ); + }), + ], + ], + ), + ); + } } diff --git a/lib/features/proxy/widget/proxy_tile.dart b/lib/features/proxy/widget/proxy_tile.dart index 7a55b951..6789f0f3 100644 --- a/lib/features/proxy/widget/proxy_tile.dart +++ b/lib/features/proxy/widget/proxy_tile.dart @@ -30,7 +30,7 @@ class ProxyTile extends HookConsumerWidget with PresLogger { title: Text( proxy.name, overflow: TextOverflow.ellipsis, - style: TextStyle(fontFamily: FontFamily.emoji), + style: const TextStyle(fontFamily: FontFamily.emoji), ), leading: Padding( padding: const EdgeInsets.symmetric(vertical: 8), diff --git a/lib/features/settings/about/about_page.dart b/lib/features/settings/about/about_page.dart index 144896a4..e7ed6bd4 100644 --- a/lib/features/settings/about/about_page.dart +++ b/lib/features/settings/about/about_page.dart @@ -5,13 +5,14 @@ import 'package:gap/gap.dart'; import 'package:hiddify/core/app_info/app_info_provider.dart'; import 'package:hiddify/core/directories/directories_provider.dart'; import 'package:hiddify/core/localization/translations.dart'; -import 'package:hiddify/core/model/constants.dart'; import 'package:hiddify/core/model/failures.dart'; import 'package:hiddify/core/widget/adaptive_icon.dart'; import 'package:hiddify/features/app_update/notifier/app_update_notifier.dart'; import 'package:hiddify/features/app_update/notifier/app_update_state.dart'; import 'package:hiddify/features/app_update/widget/new_version_dialog.dart'; import 'package:hiddify/features/common/nested_app_bar.dart'; +import 'package:hiddify/features/settings/about/privacy_policy_screen.dart'; +import 'package:hiddify/features/settings/about/terms_and_conditions_screen.dart'; import 'package:hiddify/gen/assets.gen.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -30,8 +31,7 @@ class AboutPage extends HookConsumerWidget { (_, next) async { if (!context.mounted) return; switch (next) { - case AppUpdateStateAvailable(:final versionInfo) || - AppUpdateStateIgnored(:final versionInfo): + case AppUpdateStateAvailable(:final versionInfo) || AppUpdateStateIgnored(:final versionInfo): return NewVersionDialog( appInfo.presentVersion, versionInfo, @@ -40,35 +40,19 @@ class AboutPage extends HookConsumerWidget { case AppUpdateStateError(:final error): return CustomToast.error(t.presentShortError(error)).show(context); case AppUpdateStateNotAvailable(): - return CustomToast.success(t.appUpdate.notAvailableMsg) - .show(context); + return CustomToast.success(t.appUpdate.notAvailableMsg).show(context); } }, ); final conditionalTiles = [ - if (appInfo.release.allowCustomUpdateChecker) - ListTile( - title: Text(t.about.checkForUpdate), - trailing: switch (appUpdate) { - AppUpdateStateChecking() => const SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator(), - ), - _ => const Icon(FluentIcons.arrow_sync_24_regular), - }, - onTap: () async { - await ref.read(appUpdateNotifierProvider.notifier).check(); - }, - ), + // UMBRIX: Отключили проверку обновлений - используем свой сервер if (PlatformUtils.isDesktop) ListTile( title: Text(t.settings.general.openWorkingDir), trailing: const Icon(FluentIcons.open_folder_24_regular), onTap: () async { - final path = - ref.watch(appDirectoriesProvider).requireValue.workingDir.uri; + final path = ref.watch(appDirectoriesProvider).requireValue.workingDir.uri; await UriUtils.tryLaunch(path); }, ), @@ -103,7 +87,8 @@ class AboutPage extends HookConsumerWidget { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Assets.images.logo.svg(width: 64, height: 64), + // UMBRIX: Используем наш логотип + Assets.images.umbrixLogo.image(width: 64, height: 64), const Gap(16), Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -127,39 +112,45 @@ class AboutPage extends HookConsumerWidget { [ ...conditionalTiles, if (conditionalTiles.isNotEmpty) const Divider(), - ListTile( - title: Text(t.about.sourceCode), - trailing: const Icon(FluentIcons.open_24_regular), - onTap: () async { - await UriUtils.tryLaunch( - Uri.parse(Constants.githubUrl), - ); - }, - ), - ListTile( - title: Text(t.about.telegramChannel), - trailing: const Icon(FluentIcons.open_24_regular), - onTap: () async { - await UriUtils.tryLaunch( - Uri.parse(Constants.telegramChannelUrl), - ); - }, - ), + // UMBRIX: Убрали "Исходный код" - наш приватный форк + // ListTile( + // title: Text(t.about.sourceCode), + // trailing: const Icon(FluentIcons.open_24_regular), + // onTap: () async { + // await UriUtils.tryLaunch( + // Uri.parse(Constants.githubUrl), + // ); + // }, + // ), + // UMBRIX: Telegram канал пока закомментирован + // ListTile( + // title: Text(t.about.telegramChannel), + // trailing: const Icon(FluentIcons.open_24_regular), + // onTap: () async { + // await UriUtils.tryLaunch( + // Uri.parse(Constants.telegramChannelUrl), + // ); + // }, + // ), ListTile( title: Text(t.about.termsAndConditions), - trailing: const Icon(FluentIcons.open_24_regular), - onTap: () async { - await UriUtils.tryLaunch( - Uri.parse(Constants.termsAndConditionsUrl), + trailing: const Icon(FluentIcons.chevron_right_20_regular), + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const TermsAndConditionsScreen(), + ), ); }, ), ListTile( title: Text(t.about.privacyPolicy), - trailing: const Icon(FluentIcons.open_24_regular), - onTap: () async { - await UriUtils.tryLaunch( - Uri.parse(Constants.privacyPolicyUrl), + trailing: const Icon(FluentIcons.chevron_right_20_regular), + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const PrivacyPolicyScreen(), + ), ); }, ), diff --git a/lib/features/settings/about/licenses_screen.dart b/lib/features/settings/about/licenses_screen.dart new file mode 100644 index 00000000..f7027ec7 --- /dev/null +++ b/lib/features/settings/about/licenses_screen.dart @@ -0,0 +1,92 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/gen/assets.gen.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class LicensesScreen extends HookConsumerWidget { + const LicensesScreen({super.key}); + + static Future _loadAllLicenses() async { + final licenses = await LicenseRegistry.licenses.toList(); + final buffer = StringBuffer(); + + for (final license in licenses) { + for (final package in license.packages) { + buffer.writeln(package); + } + buffer.writeln(); + + final paragraphs = license.paragraphs.toList(); + for (final paragraph in paragraphs) { + buffer.writeln(paragraph.text); + } + + buffer.writeln(); + buffer.writeln('=' * 80); + buffer.writeln(); + } + + return buffer.toString(); + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + return Scaffold( + appBar: AppBar( + title: Text(t.about.licenses), + ), + body: Column( + children: [ + // Заголовок с логотипом + Container( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + Assets.images.umbrixLogo.image( + width: 64, + height: 64, + fit: BoxFit.contain, + ), + const SizedBox(height: 12), + const Text( + '0.1.0', + style: TextStyle(fontSize: 16), + ), + ], + ), + ), + const Divider(height: 1), + // Весь текст лицензий + Expanded( + child: FutureBuilder( + future: _loadAllLicenses(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + + if (snapshot.hasError) { + return Center(child: Text('Ошибка: ${snapshot.error}')); + } + + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: SelectableText( + snapshot.data ?? '', + style: const TextStyle( + fontSize: 5.5, + fontFamily: 'monospace', + height: 1.4, + ), + ), + ); + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/settings/about/privacy_policy_screen.dart b/lib/features/settings/about/privacy_policy_screen.dart new file mode 100644 index 00000000..0be92544 --- /dev/null +++ b/lib/features/settings/about/privacy_policy_screen.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class PrivacyPolicyScreen extends ConsumerWidget { + const PrivacyPolicyScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + + return Scaffold( + appBar: AppBar( + title: Text(t.about.privacyPolicy), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + t.about.privacyPolicy.toUpperCase(), + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + t.privacyPolicy.lastUpdated, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 24), + _buildSection( + context, + '1. ${t.privacyPolicy.section1Title}', + t.privacyPolicy.section1Content, + ), + _buildSection( + context, + '2. ${t.privacyPolicy.section2Title}', + t.privacyPolicy.section2Content, + ), + _buildSection( + context, + '3. ${t.privacyPolicy.section3Title}', + t.privacyPolicy.section3Content, + ), + _buildSection( + context, + '4. ${t.privacyPolicy.section4Title}', + t.privacyPolicy.section4Content, + ), + _buildSection( + context, + '5. ${t.privacyPolicy.section5Title}', + t.privacyPolicy.section5Content, + ), + _buildSection( + context, + '6. ${t.privacyPolicy.section6Title}', + t.privacyPolicy.section6Content, + ), + _buildSection( + context, + '7. ${t.privacyPolicy.section7Title}', + t.privacyPolicy.section7Content, + ), + const SizedBox(height: 24), + ], + ), + ), + ); + } + + Widget _buildSection(BuildContext context, String title, String content) { + return Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + content, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + height: 1.6, + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/settings/about/terms_and_conditions_screen.dart b/lib/features/settings/about/terms_and_conditions_screen.dart new file mode 100644 index 00000000..b91845f3 --- /dev/null +++ b/lib/features/settings/about/terms_and_conditions_screen.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class TermsAndConditionsScreen extends ConsumerWidget { + const TermsAndConditionsScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + + return Scaffold( + appBar: AppBar( + title: Text(t.about.termsAndConditions), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + t.about.termsAndConditions.toUpperCase(), + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + t.termsAndConditions.lastUpdated, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 24), + _buildSection( + context, + '1. ${t.termsAndConditions.section1Title}', + t.termsAndConditions.section1Content, + ), + _buildSection( + context, + '2. ${t.termsAndConditions.section2Title}', + t.termsAndConditions.section2Content, + ), + _buildSection( + context, + '3. ${t.termsAndConditions.section3Title}', + t.termsAndConditions.section3Content, + ), + _buildSection( + context, + '4. ${t.termsAndConditions.section4Title}', + t.termsAndConditions.section4Content, + ), + _buildSection( + context, + '5. ${t.termsAndConditions.section5Title}', + t.termsAndConditions.section5Content, + ), + _buildSection( + context, + '6. ${t.termsAndConditions.section6Title}', + t.termsAndConditions.section6Content, + ), + _buildSection( + context, + '7. ${t.termsAndConditions.section7Title}', + t.termsAndConditions.section7Content, + ), + const SizedBox(height: 24), + ], + ), + ), + ); + } + + Widget _buildSection(BuildContext context, String title, String content) { + return Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + content, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + height: 1.6, + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/settings/experimental_features_page.dart b/lib/features/settings/experimental_features_page.dart new file mode 100644 index 00000000..8ff1bfe2 --- /dev/null +++ b/lib/features/settings/experimental_features_page.dart @@ -0,0 +1,149 @@ +import 'package:flutter/material.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/optional_range.dart'; +import 'package:hiddify/features/config_option/data/config_option_repository.dart'; +import 'package:hiddify/features/config_option/overview/warp_options_widgets.dart'; +import 'package:hiddify/features/config_option/widget/preference_tile.dart'; +import 'package:hiddify/features/settings/widgets/sections_widgets.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class ExperimentalFeaturesPage extends HookConsumerWidget { + const ExperimentalFeaturesPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + + return Scaffold( + body: CustomScrollView( + slivers: [ + SliverAppBar.large( + title: Text( + t.settings.experimental, + style: const TextStyle(fontSize: 20), + ), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => Navigator.of(context).pop(), + ), + ), + SliverToBoxAdapter( + child: Container( + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.errorContainer.withOpacity(0.3), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Theme.of(context).colorScheme.error.withOpacity(0.3), + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.warning_amber_rounded, + color: Theme.of(context).colorScheme.error, + size: 24, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + t.config.bypassLanWarning.title, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.error, + ), + ), + const SizedBox(height: 4), + Text( + t.settings.experimentalMsg, + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ), + ], + ), + ), + ), + SliverList.list( + children: [ + // Разрешить подключения из LAN + const SettingsDivider(), + SettingsSection(t.config.section.route), + SwitchListTile( + title: Text(t.config.allowConnectionFromLan), + value: ref.watch(ConfigOptions.allowConnectionFromLan), + onChanged: ref.watch(ConfigOptions.allowConnectionFromLan.notifier).update, + ), + + // TLS Tricks + const SettingsDivider(), + SettingsSection(t.config.section.tlsTricks), + SwitchListTile( + title: Text(t.config.enableTlsFragment), + value: ref.watch(ConfigOptions.enableTlsFragment), + onChanged: ref.watch(ConfigOptions.enableTlsFragment.notifier).update, + ), + ValuePreferenceWidget( + value: ref.watch(ConfigOptions.tlsFragmentSize), + preferences: ref.watch(ConfigOptions.tlsFragmentSize.notifier), + title: t.config.tlsFragmentSize, + inputToValue: OptionalRange.tryParse, + presentValue: (value) => value.present(t), + formatInputValue: (value) => value.format(), + ), + ValuePreferenceWidget( + value: ref.watch(ConfigOptions.tlsFragmentSleep), + preferences: ref.watch(ConfigOptions.tlsFragmentSleep.notifier), + title: t.config.tlsFragmentSleep, + inputToValue: OptionalRange.tryParse, + presentValue: (value) => value.present(t), + formatInputValue: (value) => value.format(), + ), + SwitchListTile( + title: Text(t.config.enableTlsMixedSniCase), + value: ref.watch(ConfigOptions.enableTlsMixedSniCase), + onChanged: ref.watch(ConfigOptions.enableTlsMixedSniCase.notifier).update, + ), + SwitchListTile( + title: Text(t.config.enableTlsPadding), + value: ref.watch(ConfigOptions.enableTlsPadding), + onChanged: ref.watch(ConfigOptions.enableTlsPadding.notifier).update, + ), + ValuePreferenceWidget( + value: ref.watch(ConfigOptions.tlsPaddingSize), + preferences: ref.watch(ConfigOptions.tlsPaddingSize.notifier), + title: t.config.tlsPaddingSize, + inputToValue: OptionalRange.tryParse, + presentValue: (value) => value.format(), + formatInputValue: (value) => value.format(), + ), + + // WARP + const SettingsDivider(), + SettingsSection(t.config.section.warp), + const WarpOptionsTiles(), + + // Использовать Xray Core + const SettingsDivider(), + SettingsSection(t.config.section.misc), + SwitchListTile( + title: Text(t.config.useXrayCoreWhenPossible.Label), + subtitle: Text(t.config.useXrayCoreWhenPossible.Description), + value: ref.watch(ConfigOptions.useXrayCoreWhenPossible), + onChanged: ref.watch(ConfigOptions.useXrayCoreWhenPossible.notifier).update, + ), + + const SizedBox(height: 24), + ], + ), + ], + ), + ); + } +} diff --git a/lib/features/settings/overview/settings_overview_page.dart b/lib/features/settings/overview/settings_overview_page.dart index 3fee58b2..a68d1994 100644 --- a/lib/features/settings/overview/settings_overview_page.dart +++ b/lib/features/settings/overview/settings_overview_page.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:gap/gap.dart'; import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/router/routes.dart'; import 'package:hiddify/features/common/nested_app_bar.dart'; +import 'package:hiddify/features/settings/experimental_features_page.dart'; import 'package:hiddify/features/settings/widgets/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -24,8 +27,57 @@ class SettingsOverviewPage extends HookConsumerWidget { const GeneralSettingTiles(), const PlatformSettingsTiles(), const SettingsDivider(), - SettingsSection(t.settings.advanced.sectionTitle), - const AdvancedSettingTiles(), + // Расширенные - раскрывающаяся секция + Theme( + data: Theme.of(context).copyWith(dividerColor: Colors.transparent), + child: ExpansionTile( + leading: const Icon(Icons.tune), + title: Text(t.settings.advanced.sectionTitle), + initiallyExpanded: false, + children: const [ + AdvancedSettingTiles(), + ], + ), + ), + const SettingsDivider(), + // Экспериментальные - обычная кнопка + ListTile( + leading: const Icon(Icons.science_outlined), + title: Text(t.settings.experimental), + subtitle: Text(t.settings.experimentalMsg), + trailing: const Icon(Icons.arrow_forward_ios), + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const ExperimentalFeaturesPage(), + ), + ); + }, + ), + const SettingsDivider(), + // Параметры конфигурации - обычная кнопка + ListTile( + leading: const Icon(FluentIcons.box_edit_20_filled), + title: Text(t.config.pageTitle), + subtitle: Text(t.config.allOptions), + trailing: const Icon(Icons.arrow_forward_ios), + onTap: () { + const ConfigOptionsRoute().push(context); + }, + ), + const SettingsDivider(), + // Логи - раскрывающаяся секция + Theme( + data: Theme.of(context).copyWith(dividerColor: Colors.transparent), + child: ExpansionTile( + leading: const Icon(FluentIcons.document_text_20_filled), + title: Text(t.logs.pageTitle), + initiallyExpanded: false, + children: const [ + LogsSettingTiles(), + ], + ), + ), const Gap(16), ], ), diff --git a/lib/features/settings/widgets/advanced_setting_tiles.dart b/lib/features/settings/widgets/advanced_setting_tiles.dart index cf8ffaf1..ab520f9a 100644 --- a/lib/features/settings/widgets/advanced_setting_tiles.dart +++ b/lib/features/settings/widgets/advanced_setting_tiles.dart @@ -4,9 +4,6 @@ import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/preferences/general_preferences.dart'; -import 'package:hiddify/core/router/router.dart'; -import 'package:hiddify/features/common/general_pref_tiles.dart'; -import 'package:hiddify/features/per_app_proxy/model/per_app_proxy_mode.dart'; import 'package:hiddify/features/settings/notifier/platform_settings_notifier.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; diff --git a/lib/features/settings/widgets/general_setting_tiles.dart b/lib/features/settings/widgets/general_setting_tiles.dart index 92047d80..3c9440c7 100644 --- a/lib/features/settings/widgets/general_setting_tiles.dart +++ b/lib/features/settings/widgets/general_setting_tiles.dart @@ -7,6 +7,7 @@ import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/preferences/general_preferences.dart'; import 'package:hiddify/features/auto_start/notifier/auto_start_notifier.dart'; import 'package:hiddify/features/common/general_pref_tiles.dart'; +import 'package:hiddify/features/config_option/data/config_option_repository.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -19,9 +20,93 @@ class GeneralSettingTiles extends HookConsumerWidget { return Column( children: [ - const LocalePrefTile(), - const ThemeModePrefTile(), + // UMBRIX: Язык и тема перенесены в левое меню const EnableAnalyticsPrefTile(), + SwitchListTile( + title: Text(t.config.blockAds), + subtitle: Text( + t.config.blockAdsWarning.subtitle, + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + secondary: const Icon(FluentIcons.shield_prohibited_24_regular), + value: ref.watch(ConfigOptions.blockAds), + onChanged: (value) async { + if (value) { + // Показываем предупреждение при включении + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + icon: const Icon(Icons.warning_amber_rounded, size: 48), + title: Text(t.config.bypassLanWarning.title), + content: Text( + t.config.blockAdsWarning.message, + style: const TextStyle(fontSize: 14), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(t.config.bypassLanWarning.cancel), + ), + FilledButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text(t.config.bypassLanWarning.enable), + ), + ], + ), + ); + if (confirmed == true) { + ref.read(ConfigOptions.blockAds.notifier).update(true); + } + } else { + ref.read(ConfigOptions.blockAds.notifier).update(false); + } + }, + ), + SwitchListTile( + title: Text(t.config.bypassLan), + subtitle: Text( + t.config.bypassLanWarning.subtitle, + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + secondary: const Icon(FluentIcons.home_24_regular), + value: ref.watch(ConfigOptions.bypassLan), + onChanged: (value) async { + if (value) { + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + icon: const Icon(Icons.warning_amber_rounded, size: 48), + title: Text(t.config.bypassLanWarning.title), + content: Text( + t.config.bypassLanWarning.message, + style: const TextStyle(fontSize: 14), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(t.config.bypassLanWarning.cancel), + ), + FilledButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text(t.config.bypassLanWarning.enable), + ), + ], + ), + ); + if (confirmed == true) { + ref.read(ConfigOptions.bypassLan.notifier).update(true); + } + } else { + ref.read(ConfigOptions.bypassLan.notifier).update(false); + } + }, + ), SwitchListTile( title: Text(t.settings.general.autoIpCheck), secondary: const Icon(FluentIcons.globe_search_24_regular), diff --git a/lib/features/settings/widgets/logs_setting_tiles.dart b/lib/features/settings/widgets/logs_setting_tiles.dart new file mode 100644 index 00000000..865b0ebd --- /dev/null +++ b/lib/features/settings/widgets/logs_setting_tiles.dart @@ -0,0 +1,30 @@ +import 'package:fluentui_system_icons/fluentui_system_icons.dart'; +import 'package:flutter/material.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/router/router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +/// Секция "Логи и отладка" в настройках +class LogsSettingTiles extends HookConsumerWidget { + const LogsSettingTiles({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + + return Column( + children: [ + // Переход на страницу логов + ListTile( + title: Text(t.logs.pageTitle), + subtitle: const Text('Просмотр логов приложения и ядра'), + leading: const Icon(FluentIcons.document_text_20_regular), + trailing: const Icon(FluentIcons.chevron_right_20_regular), + onTap: () { + const LogsOverviewRoute().push(context); + }, + ), + ], + ); + } +} diff --git a/lib/features/settings/widgets/settings_input_dialog.dart b/lib/features/settings/widgets/settings_input_dialog.dart index cdade2a4..7dd9c701 100644 --- a/lib/features/settings/widgets/settings_input_dialog.dart +++ b/lib/features/settings/widgets/settings_input_dialog.dart @@ -23,7 +23,6 @@ class SettingsInputDialog extends HookConsumerWidget with PresLogger { Future show(BuildContext context) async { return showDialog( context: context, - useRootNavigator: true, builder: (context) => this, ); } @@ -86,7 +85,7 @@ class SettingsInputDialog extends HookConsumerWidget with PresLogger { onSelected: (suggestion) { // Handle the selected suggestion print('Selected: $suggestion'); - textController.text = suggestion.toString(); + textController.text = suggestion; }, ) else @@ -120,7 +119,7 @@ class SettingsInputDialog extends HookConsumerWidget with PresLogger { child: TextButton( onPressed: () async { onReset!(); - await Navigator.of(context).maybePop(null); + await Navigator.of(context).maybePop(); }, child: Text(t.general.reset.toUpperCase()), ), @@ -139,7 +138,7 @@ class SettingsInputDialog extends HookConsumerWidget with PresLogger { child: TextButton( onPressed: () async { if (validator?.call(textController.value.text) == false) { - await Navigator.of(context).maybePop(null); + await Navigator.of(context).maybePop(); } else if (mapTo != null) { await Navigator.of(context).maybePop(mapTo!.call(textController.value.text)); } else { @@ -164,7 +163,7 @@ class AutocompleteField extends StatelessWidget { Widget build(BuildContext context) { return Autocomplete( initialValue: TextEditingValue( - text: this.initialValue, selection: TextSelection(baseOffset: 0, extentOffset: this.initialValue.length), // Selects the entire text + text: initialValue, selection: TextSelection(baseOffset: 0, extentOffset: initialValue.length), // Selects the entire text ), optionsBuilder: (TextEditingValue textEditingValue) { // if (textEditingValue.text == '') { @@ -200,7 +199,6 @@ class SettingsPickerDialog extends HookConsumerWidget with PresLogger { Future show(BuildContext context) async { return showDialog( context: context, - useRootNavigator: true, builder: (context) => this, ); } @@ -231,7 +229,7 @@ class SettingsPickerDialog extends HookConsumerWidget with PresLogger { TextButton( onPressed: () async { onReset!(); - await Navigator.of(context).maybePop(null); + await Navigator.of(context).maybePop(); }, child: Text(t.general.reset.toUpperCase()), ), @@ -270,7 +268,6 @@ class SettingsSliderDialog extends HookConsumerWidget with PresLogger { Future show(BuildContext context) async { return showDialog( context: context, - useRootNavigator: true, builder: (context) => this, ); } @@ -299,7 +296,7 @@ class SettingsSliderDialog extends HookConsumerWidget with PresLogger { TextButton( onPressed: () async { onReset!(); - await Navigator.of(context).maybePop(null); + await Navigator.of(context).maybePop(); }, child: Text(t.general.reset.toUpperCase()), ), diff --git a/lib/features/settings/widgets/widgets.dart b/lib/features/settings/widgets/widgets.dart index e0a96295..fab8efad 100644 --- a/lib/features/settings/widgets/widgets.dart +++ b/lib/features/settings/widgets/widgets.dart @@ -1,5 +1,6 @@ export 'advanced_setting_tiles.dart'; export 'general_setting_tiles.dart'; +export 'logs_setting_tiles.dart'; export 'platform_settings_tiles.dart'; export 'sections_widgets.dart'; export 'settings_input_dialog.dart'; diff --git a/lib/features/system_tray/notifier/system_tray_notifier.dart b/lib/features/system_tray/notifier/system_tray_notifier.dart index 06031d24..2c9cbd77 100644 --- a/lib/features/system_tray/notifier/system_tray_notifier.dart +++ b/lib/features/system_tray/notifier/system_tray_notifier.dart @@ -1,6 +1,5 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/model/constants.dart'; @@ -25,7 +24,7 @@ class SystemTrayNotifier extends _$SystemTrayNotifier with AppLogger { Future build() async { if (!PlatformUtils.isDesktop) return; - final activeProxy = await ref.watch(activeProxyNotifierProvider); + final activeProxy = ref.watch(activeProxyNotifierProvider); final delay = activeProxy.value?.urlTestDelay ?? 0; final newConnectionStatus = delay > 0 && delay < 65000; ConnectionStatus connection; @@ -40,7 +39,7 @@ class SystemTrayNotifier extends _$SystemTrayNotifier with AppLogger { var tooltip = Constants.appName; final serviceMode = ref.watch(ConfigOptions.serviceMode); - if (connection == Disconnected()) { + if (connection == const Disconnected()) { setIcon(connection); } else if (newConnectionStatus) { setIcon(const Connected()); @@ -172,7 +171,7 @@ class SystemTrayNotifier extends _$SystemTrayNotifier with AppLogger { } } } - final isDarkMode = false; + const isDarkMode = false; switch (status) { case Connected(): return Assets.images.trayIconConnectedPng.path; diff --git a/lib/features/window/notifier/window_notifier.dart b/lib/features/window/notifier/window_notifier.dart index 8b379155..bf2e36c4 100644 --- a/lib/features/window/notifier/window_notifier.dart +++ b/lib/features/window/notifier/window_notifier.dart @@ -2,7 +2,6 @@ import 'dart:io'; import 'dart:ui'; import 'package:flutter/material.dart'; -import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/features/connection/notifier/connection_notifier.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; diff --git a/lib/singbox/generated/core.pbgrpc.dart b/lib/singbox/generated/core.pbgrpc.dart index ea886f1f..3988aab5 100644 --- a/lib/singbox/generated/core.pbgrpc.dart +++ b/lib/singbox/generated/core.pbgrpc.dart @@ -13,10 +13,9 @@ import 'dart:async' as $async; import 'dart:core' as $core; import 'package:grpc/service_api.dart' as $grpc; +import 'package:hiddify/singbox/generated/core.pb.dart' as $0; import 'package:protobuf/protobuf.dart' as $pb; -import 'core.pb.dart' as $0; - export 'core.pb.dart'; @$pb.GrpcServiceName('ConfigOptions.CoreService') @@ -24,17 +23,17 @@ class CoreServiceClient extends $grpc.Client { static final _$parseConfig = $grpc.ClientMethod<$0.ParseConfigRequest, $0.ParseConfigResponse>( '/ConfigOptions.CoreService/ParseConfig', ($0.ParseConfigRequest value) => value.writeToBuffer(), - ($core.List<$core.int> value) => $0.ParseConfigResponse.fromBuffer(value)); + ($core.List<$core.int> value) => $0.ParseConfigResponse.fromBuffer(value),); static final _$generateFullConfig = $grpc.ClientMethod<$0.GenerateConfigRequest, $0.GenerateConfigResponse>( '/ConfigOptions.CoreService/GenerateFullConfig', ($0.GenerateConfigRequest value) => value.writeToBuffer(), - ($core.List<$core.int> value) => $0.GenerateConfigResponse.fromBuffer(value)); + ($core.List<$core.int> value) => $0.GenerateConfigResponse.fromBuffer(value),); CoreServiceClient($grpc.ClientChannel channel, {$grpc.CallOptions? options, - $core.Iterable<$grpc.ClientInterceptor>? interceptors}) + $core.Iterable<$grpc.ClientInterceptor>? interceptors,}) : super(channel, options: options, - interceptors: interceptors); + interceptors: interceptors,); $grpc.ResponseFuture<$0.ParseConfigResponse> parseConfig($0.ParseConfigRequest request, {$grpc.CallOptions? options}) { return $createUnaryCall(_$parseConfig, request, options: options); @@ -56,14 +55,14 @@ abstract class CoreServiceBase extends $grpc.Service { false, false, ($core.List<$core.int> value) => $0.ParseConfigRequest.fromBuffer(value), - ($0.ParseConfigResponse value) => value.writeToBuffer())); + ($0.ParseConfigResponse value) => value.writeToBuffer(),),); $addMethod($grpc.ServiceMethod<$0.GenerateConfigRequest, $0.GenerateConfigResponse>( 'GenerateFullConfig', generateFullConfig_Pre, false, false, ($core.List<$core.int> value) => $0.GenerateConfigRequest.fromBuffer(value), - ($0.GenerateConfigResponse value) => value.writeToBuffer())); + ($0.GenerateConfigResponse value) => value.writeToBuffer(),),); } $async.Future<$0.ParseConfigResponse> parseConfig_Pre($grpc.ServiceCall call, $async.Future<$0.ParseConfigRequest> request) async { diff --git a/lib/singbox/model/singbox_outbound.dart b/lib/singbox/model/singbox_outbound.dart index 54bdc56e..7a9384d5 100644 --- a/lib/singbox/model/singbox_outbound.dart +++ b/lib/singbox/model/singbox_outbound.dart @@ -1,4 +1,3 @@ -import 'package:dartx/dartx.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hiddify/singbox/model/singbox_proxy_type.dart'; diff --git a/lib/singbox/service/platform_singbox_service.dart b/lib/singbox/service/platform_singbox_service.dart index e65db621..7f8e6367 100644 --- a/lib/singbox/service/platform_singbox_service.dart +++ b/lib/singbox/service/platform_singbox_service.dart @@ -186,7 +186,10 @@ class PlatformSingboxService with InfraLogger implements SingboxService { return activeGroupsChannel.receiveBroadcastStream().map( (event) { if (event case String _) { - return (jsonDecode(event) as List).map((e) { + final decoded = jsonDecode(event) as List; + loggy.info("🔍 DECODED JSON: ${decoded.length} groups"); + return decoded.map((e) { + loggy.info("🔍 GROUP DATA: $e"); return SingboxOutboundGroup.fromJson(e as Map); }).toList(); } diff --git a/lib/utils/alerts.dart b/lib/utils/alerts.dart index 9c47a844..c45ae256 100644 --- a/lib/utils/alerts.dart +++ b/lib/utils/alerts.dart @@ -21,7 +21,6 @@ class CustomAlertDialog extends StatelessWidget { Future show(BuildContext context) async { await showDialog( context: context, - useRootNavigator: true, builder: (context) => this, ); } diff --git a/logo/all_generated_icons/android/ic_launcher_playstore.png b/logo/all_generated_icons/android/ic_launcher_playstore.png new file mode 100644 index 00000000..64ca0389 Binary files /dev/null and b/logo/all_generated_icons/android/ic_launcher_playstore.png differ diff --git a/logo/all_generated_icons/android/mipmap-hdpi/ic_launcher.png b/logo/all_generated_icons/android/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..48616430 Binary files /dev/null and b/logo/all_generated_icons/android/mipmap-hdpi/ic_launcher.png differ diff --git a/logo/all_generated_icons/android/mipmap-ldpi/ic_launcher.png b/logo/all_generated_icons/android/mipmap-ldpi/ic_launcher.png new file mode 100644 index 00000000..942bdaa6 Binary files /dev/null and b/logo/all_generated_icons/android/mipmap-ldpi/ic_launcher.png differ diff --git a/logo/all_generated_icons/android/mipmap-mdpi/ic_launcher.png b/logo/all_generated_icons/android/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..a971234b Binary files /dev/null and b/logo/all_generated_icons/android/mipmap-mdpi/ic_launcher.png differ diff --git a/logo/all_generated_icons/android/mipmap-xhdpi/ic_launcher.png b/logo/all_generated_icons/android/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..0d619247 Binary files /dev/null and b/logo/all_generated_icons/android/mipmap-xhdpi/ic_launcher.png differ diff --git a/logo/all_generated_icons/android/mipmap-xxhdpi/ic_launcher.png b/logo/all_generated_icons/android/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..4138c741 Binary files /dev/null and b/logo/all_generated_icons/android/mipmap-xxhdpi/ic_launcher.png differ diff --git a/logo/all_generated_icons/android/mipmap-xxxhdpi/ic_launcher.png b/logo/all_generated_icons/android/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..db7ab4a7 Binary files /dev/null and b/logo/all_generated_icons/android/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/logo/all_generated_icons/ios/Contents.json b/logo/all_generated_icons/ios/Contents.json new file mode 100644 index 00000000..41ee8477 --- /dev/null +++ b/logo/all_generated_icons/ios/Contents.json @@ -0,0 +1,122 @@ +{ + "images": [ + { + "size": "60x60", + "idiom": "iphone", + "scale": "2x", + "filename": "Icon-App-60x60@2x.png" + }, + { + "size": "60x60", + "idiom": "iphone", + "scale": "3x", + "filename": "Icon-App-60x60@3x.png" + }, + { + "size": "29x29", + "idiom": "iphone", + "scale": "1x", + "filename": "Icon-App-29x29@1x.png" + }, + { + "size": "29x29", + "idiom": "iphone", + "scale": "2x", + "filename": "Icon-App-29x29@2x.png" + }, + { + "size": "29x29", + "idiom": "iphone", + "scale": "3x", + "filename": "Icon-App-29x29@3x.png" + }, + { + "size": "40x40", + "idiom": "iphone", + "scale": "2x", + "filename": "Icon-App-40x40@2x.png" + }, + { + "size": "40x40", + "idiom": "iphone", + "scale": "3x", + "filename": "Icon-App-40x40@3x.png" + }, + { + "size": "20x20", + "idiom": "iphone", + "scale": "2x", + "filename": "Icon-App-20x20@2x.png" + }, + { + "size": "20x20", + "idiom": "iphone", + "scale": "3x", + "filename": "Icon-App-20x20@3x.png" + }, + { + "size": "76x76", + "idiom": "ipad", + "scale": "1x", + "filename": "Icon-App-76x76@1x.png" + }, + { + "size": "76x76", + "idiom": "ipad", + "scale": "2x", + "filename": "Icon-App-76x76@2x.png" + }, + { + "size": "83.5x83.5", + "idiom": "ipad", + "scale": "2x", + "filename": "Icon-App-83.5x83.5@2x.png" + }, + { + "size": "29x29", + "idiom": "ipad", + "scale": "1x", + "filename": "Icon-App-29x29@1x.png" + }, + { + "size": "29x29", + "idiom": "ipad", + "scale": "2x", + "filename": "Icon-App-29x29@2x.png" + }, + { + "size": "40x40", + "idiom": "ipad", + "scale": "1x", + "filename": "Icon-App-40x40@1x.png" + }, + { + "size": "40x40", + "idiom": "ipad", + "scale": "2x", + "filename": "Icon-App-40x40@2x.png" + }, + { + "size": "20x20", + "idiom": "ipad", + "scale": "1x", + "filename": "Icon-App-20x20@1x.png" + }, + { + "size": "20x20", + "idiom": "ipad", + "scale": "2x", + "filename": "Icon-App-20x20@2x.png" + }, + { + "size": "1024x1024", + "idiom": "ios-marketing", + "scale": "1x", + "filename": "Icon-App-1024x1024@1x.png" + } + ], + "info": { + "author": "convertany", + "version": 1 + } +} \ No newline at end of file diff --git a/logo/all_generated_icons/ios/Icon-App-1024x1024@1x.png b/logo/all_generated_icons/ios/Icon-App-1024x1024@1x.png new file mode 100644 index 00000000..65794a3b Binary files /dev/null and b/logo/all_generated_icons/ios/Icon-App-1024x1024@1x.png differ diff --git a/logo/all_generated_icons/ios/Icon-App-20x20@1x.png b/logo/all_generated_icons/ios/Icon-App-20x20@1x.png new file mode 100644 index 00000000..303d2f9e Binary files /dev/null and b/logo/all_generated_icons/ios/Icon-App-20x20@1x.png differ diff --git a/logo/all_generated_icons/ios/Icon-App-20x20@2x.png b/logo/all_generated_icons/ios/Icon-App-20x20@2x.png new file mode 100644 index 00000000..111253b6 Binary files /dev/null and b/logo/all_generated_icons/ios/Icon-App-20x20@2x.png differ diff --git a/logo/all_generated_icons/ios/Icon-App-20x20@3x.png b/logo/all_generated_icons/ios/Icon-App-20x20@3x.png new file mode 100644 index 00000000..c33c148e Binary files /dev/null and b/logo/all_generated_icons/ios/Icon-App-20x20@3x.png differ diff --git a/logo/all_generated_icons/ios/Icon-App-29x29@1x.png b/logo/all_generated_icons/ios/Icon-App-29x29@1x.png new file mode 100644 index 00000000..6c7ab656 Binary files /dev/null and b/logo/all_generated_icons/ios/Icon-App-29x29@1x.png differ diff --git a/logo/all_generated_icons/ios/Icon-App-29x29@2x.png b/logo/all_generated_icons/ios/Icon-App-29x29@2x.png new file mode 100644 index 00000000..51c2ae7f Binary files /dev/null and b/logo/all_generated_icons/ios/Icon-App-29x29@2x.png differ diff --git a/logo/all_generated_icons/ios/Icon-App-29x29@3x.png b/logo/all_generated_icons/ios/Icon-App-29x29@3x.png new file mode 100644 index 00000000..74615088 Binary files /dev/null and b/logo/all_generated_icons/ios/Icon-App-29x29@3x.png differ diff --git a/logo/all_generated_icons/ios/Icon-App-40x40@1x.png b/logo/all_generated_icons/ios/Icon-App-40x40@1x.png new file mode 100644 index 00000000..111253b6 Binary files /dev/null and b/logo/all_generated_icons/ios/Icon-App-40x40@1x.png differ diff --git a/logo/all_generated_icons/ios/Icon-App-40x40@2x.png b/logo/all_generated_icons/ios/Icon-App-40x40@2x.png new file mode 100644 index 00000000..6c0f8029 Binary files /dev/null and b/logo/all_generated_icons/ios/Icon-App-40x40@2x.png differ diff --git a/logo/all_generated_icons/ios/Icon-App-40x40@3x.png b/logo/all_generated_icons/ios/Icon-App-40x40@3x.png new file mode 100644 index 00000000..0cace29f Binary files /dev/null and b/logo/all_generated_icons/ios/Icon-App-40x40@3x.png differ diff --git a/logo/all_generated_icons/ios/Icon-App-60x60@2x.png b/logo/all_generated_icons/ios/Icon-App-60x60@2x.png new file mode 100644 index 00000000..0cace29f Binary files /dev/null and b/logo/all_generated_icons/ios/Icon-App-60x60@2x.png differ diff --git a/logo/all_generated_icons/ios/Icon-App-60x60@3x.png b/logo/all_generated_icons/ios/Icon-App-60x60@3x.png new file mode 100644 index 00000000..86cedd27 Binary files /dev/null and b/logo/all_generated_icons/ios/Icon-App-60x60@3x.png differ diff --git a/logo/all_generated_icons/ios/Icon-App-76x76@1x.png b/logo/all_generated_icons/ios/Icon-App-76x76@1x.png new file mode 100644 index 00000000..fb9b2093 Binary files /dev/null and b/logo/all_generated_icons/ios/Icon-App-76x76@1x.png differ diff --git a/logo/all_generated_icons/ios/Icon-App-76x76@2x.png b/logo/all_generated_icons/ios/Icon-App-76x76@2x.png new file mode 100644 index 00000000..70aa115e Binary files /dev/null and b/logo/all_generated_icons/ios/Icon-App-76x76@2x.png differ diff --git a/logo/all_generated_icons/ios/Icon-App-83.5x83.5@2x.png b/logo/all_generated_icons/ios/Icon-App-83.5x83.5@2x.png new file mode 100644 index 00000000..02f04b05 Binary files /dev/null and b/logo/all_generated_icons/ios/Icon-App-83.5x83.5@2x.png differ diff --git a/logo/all_generated_icons/macos/Contents.json b/logo/all_generated_icons/macos/Contents.json new file mode 100644 index 00000000..8fdfdb2e --- /dev/null +++ b/logo/all_generated_icons/macos/Contents.json @@ -0,0 +1,68 @@ +{ + "images": [ + { + "size": "16x16", + "idiom": "mac", + "scale": "1x", + "filename": "icon_16x16.png" + }, + { + "size": "16x16", + "idiom": "mac", + "scale": "2x", + "filename": "icon_16x16@2x.png" + }, + { + "size": "32x32", + "idiom": "mac", + "scale": "1x", + "filename": "icon_32x32.png" + }, + { + "size": "32x32", + "idiom": "mac", + "scale": "2x", + "filename": "icon_32x32@2x.png" + }, + { + "size": "128x128", + "idiom": "mac", + "scale": "1x", + "filename": "icon_128x128.png" + }, + { + "size": "128x128", + "idiom": "mac", + "scale": "2x", + "filename": "icon_128x128@2x.png" + }, + { + "size": "256x256", + "idiom": "mac", + "scale": "1x", + "filename": "icon_256x256.png" + }, + { + "size": "256x256", + "idiom": "mac", + "scale": "2x", + "filename": "icon_256x256@2x.png" + }, + { + "size": "512x512", + "idiom": "mac", + "scale": "1x", + "filename": "icon_512x512.png" + }, + { + "size": "512x512", + "idiom": "mac", + "scale": "2x", + "filename": "icon_512x512@2x.png" + } + ], + "info": { + "author": "convertany", + "version": 1 + } +} \ No newline at end of file diff --git a/logo/all_generated_icons/macos/icon_128x128.png b/logo/all_generated_icons/macos/icon_128x128.png new file mode 100644 index 00000000..57662e18 Binary files /dev/null and b/logo/all_generated_icons/macos/icon_128x128.png differ diff --git a/logo/all_generated_icons/macos/icon_128x128@2x.png b/logo/all_generated_icons/macos/icon_128x128@2x.png new file mode 100644 index 00000000..bbbdb1aa Binary files /dev/null and b/logo/all_generated_icons/macos/icon_128x128@2x.png differ diff --git a/logo/all_generated_icons/macos/icon_16x16.png b/logo/all_generated_icons/macos/icon_16x16.png new file mode 100644 index 00000000..b83fe6a6 Binary files /dev/null and b/logo/all_generated_icons/macos/icon_16x16.png differ diff --git a/logo/all_generated_icons/macos/icon_16x16@2x.png b/logo/all_generated_icons/macos/icon_16x16@2x.png new file mode 100644 index 00000000..c85f2e8b Binary files /dev/null and b/logo/all_generated_icons/macos/icon_16x16@2x.png differ diff --git a/logo/all_generated_icons/macos/icon_256x256.png b/logo/all_generated_icons/macos/icon_256x256.png new file mode 100644 index 00000000..bbbdb1aa Binary files /dev/null and b/logo/all_generated_icons/macos/icon_256x256.png differ diff --git a/logo/all_generated_icons/macos/icon_256x256@2x.png b/logo/all_generated_icons/macos/icon_256x256@2x.png new file mode 100644 index 00000000..64ca0389 Binary files /dev/null and b/logo/all_generated_icons/macos/icon_256x256@2x.png differ diff --git a/logo/all_generated_icons/macos/icon_32x32.png b/logo/all_generated_icons/macos/icon_32x32.png new file mode 100644 index 00000000..c85f2e8b Binary files /dev/null and b/logo/all_generated_icons/macos/icon_32x32.png differ diff --git a/logo/all_generated_icons/macos/icon_32x32@2x.png b/logo/all_generated_icons/macos/icon_32x32@2x.png new file mode 100644 index 00000000..9a11fdee Binary files /dev/null and b/logo/all_generated_icons/macos/icon_32x32@2x.png differ diff --git a/logo/all_generated_icons/macos/icon_512x512.png b/logo/all_generated_icons/macos/icon_512x512.png new file mode 100644 index 00000000..64ca0389 Binary files /dev/null and b/logo/all_generated_icons/macos/icon_512x512.png differ diff --git a/logo/all_generated_icons/macos/icon_512x512@2x.png b/logo/all_generated_icons/macos/icon_512x512@2x.png new file mode 100644 index 00000000..65794a3b Binary files /dev/null and b/logo/all_generated_icons/macos/icon_512x512@2x.png differ diff --git a/logo/cf1f3970-f3ba-43b8-9a66-d96fddd46795.webp.144x144.png b/logo/cf1f3970-f3ba-43b8-9a66-d96fddd46795.webp.144x144.png new file mode 100644 index 00000000..727068e7 Binary files /dev/null and b/logo/cf1f3970-f3ba-43b8-9a66-d96fddd46795.webp.144x144.png differ diff --git a/logo/cf1f3970-f3ba-43b8-9a66-d96fddd46795.webp.192x192.png b/logo/cf1f3970-f3ba-43b8-9a66-d96fddd46795.webp.192x192.png new file mode 100644 index 00000000..039e94b4 Binary files /dev/null and b/logo/cf1f3970-f3ba-43b8-9a66-d96fddd46795.webp.192x192.png differ diff --git a/logo/cf1f3970-f3ba-43b8-9a66-d96fddd46795.webp.48x48.png b/logo/cf1f3970-f3ba-43b8-9a66-d96fddd46795.webp.48x48.png new file mode 100644 index 00000000..ff99e320 Binary files /dev/null and b/logo/cf1f3970-f3ba-43b8-9a66-d96fddd46795.webp.48x48.png differ diff --git a/logo/cf1f3970-f3ba-43b8-9a66-d96fddd46795.webp.72x72.png b/logo/cf1f3970-f3ba-43b8-9a66-d96fddd46795.webp.72x72.png new file mode 100644 index 00000000..c84865ce Binary files /dev/null and b/logo/cf1f3970-f3ba-43b8-9a66-d96fddd46795.webp.72x72.png differ diff --git a/logo/cf1f3970-f3ba-43b8-9a66-d96fddd46795.webp.96x96.png b/logo/cf1f3970-f3ba-43b8-9a66-d96fddd46795.webp.96x96.png new file mode 100644 index 00000000..ddba2e14 Binary files /dev/null and b/logo/cf1f3970-f3ba-43b8-9a66-d96fddd46795.webp.96x96.png differ diff --git a/logo/ic_launcher_playstore.png b/logo/ic_launcher_playstore.png new file mode 100644 index 00000000..64ca0389 Binary files /dev/null and b/logo/ic_launcher_playstore.png differ diff --git a/logo/ic_launcher_round.png b/logo/ic_launcher_round.png new file mode 100644 index 00000000..0427c95c Binary files /dev/null and b/logo/ic_launcher_round.png differ diff --git a/pubspec.yaml b/pubspec.yaml index 124dfb06..6876bc95 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: hiddify description: Cross Platform Multi Protocol Proxy Frontend. publish_to: "none" -version: 2.5.7+20507 +version: 0.1.0+100 environment: sdk: ">=3.3.0 <4.0.0" @@ -124,6 +124,7 @@ flutter: # - assets/core/geoip.db # - assets/core/geosite.db - assets/images/logo.svg + - assets/images/logo_splash.png - assets/images/umbrix_logo.png - assets/images/tray_icon.ico - assets/images/tray_icon.png