feat: Android auto-update notifications with dialog
Some checks are pending
CI / run (push) Waiting to run

- Add auto-check updates for Android in bootstrap
- Show update dialog instead of toast notification
- Same UX as Desktop: dialog with 'Later' and 'Update' buttons
- Notifications appear 5 seconds after app launch

Part of v1.7.6
This commit is contained in:
Umbrix Developer
2026-01-20 19:36:33 +03:00
parent fec6fa166c
commit 04eccff819
7 changed files with 883 additions and 37 deletions

36
GIT_SAFETY.txt Normal file
View File

@@ -0,0 +1,36 @@
🔒 БЕЗОПАСНОСТЬ GIT - ВАЖНО!
❌ НИКОГДА НЕ ПУШИТЬ В GITHUB HIDDIFY!
✅ ПРАВИЛЬНЫЕ КОМАНДЫ:
Основной репозиторий:
git push gitea main ← ТОЛЬКО ТАК!
Libcore:
cd libcore
git push gitea main ← ТОЛЬКО ТАК!
git push origin main ← Тоже можно (origin = gitea)
❌ ОПАСНЫЕ КОМАНДЫ (НЕ ИСПОЛЬЗОВАТЬ):
git push origin main ← В основном репо это GitHub!
git push hiddify-upstream ← Заблокировано
📋 ПРОВЕРКА ПЕРЕД PUSH:
git remote -v ← Проверить куда пушим
git log --oneline -5 ← Проверить что пушим
🔍 ТЕКУЩИЕ НАСТРОЙКИ:
Основной репозиторий:
origin = GitHub Hiddify (fetch only) ← READ ONLY!
gitea = Наш Gitea ✅
libcore/:
origin = Наш Gitea ✅
gitea = Наш Gitea ✅
hiddify-upstream = GitHub Hiddify (fetch only) ← READ ONLY!
✅ ЕСЛИ НУЖНО ОБНОВИТЬСЯ ОТ HIDDIFY:
git fetch origin ← Безопасно (только чтение)
git merge origin/main ← Потом мержим если нужно

685
WINDOWS_SPLIT_TUNNELING.md Normal file
View File

@@ -0,0 +1,685 @@
# 🔀 Split Tunneling для Windows (Per-App Proxy)
## 📋 Что такое Split Tunneling?
**Split Tunneling** (раздельное туннелирование) - функция которая позволяет:
-**Исключать** определённые приложения из VPN (они идут напрямую)
-**Включать** только выбранные приложения в VPN (остальные напрямую)
### Примеры использования:
- Исключить банковские приложения из VPN
- Исключить локальные приложения (Steam, торренты)
- Пускать через VPN только браузер
- Пускать через VPN только мессенджеры
---
## ✅ Текущее состояние
### Android - ✅ ПОЛНОСТЬЮ РАБОТАЕТ
```kotlin
// android/app/src/main/kotlin/com/umbrix/app/bg/VPNService.kt
fun addIncludePackage(builder: Builder, packageName: String) {
builder.addAllowedApplication(packageName) // Android VPNService API
}
fun addExcludePackage(builder: Builder, packageName: String) {
builder.addDisallowedApplication(packageName) // Android VPNService API
}
```
**UI готов:** `/settings/per-app-proxy`
- Список установленных приложений
- Переключатель режимов: Все / Включить / Исключить
- Фильтр системных приложений
- Поиск по названию
### Windows - ⚠️ НЕ РЕАЛИЗОВАНО
**Проблема:** На Windows нет аналога `addAllowedApplication()` из Android API.
**Что есть:**
- ✅ UI уже готов (та же страница `/settings/per-app-proxy`)
- ✅ Настройки сохраняются (`per_app_proxy_mode`, `per_app_proxy_include_list`, `per_app_proxy_exclude_list`)
- ✅ sing-box поддерживает фильтрацию по процессам через `process_name`
-НЕТ функции получения списка установленных приложений
-НЕТ передачи списка процессов в libcore
---
## 🎯 Решение для Windows
### Архитектура
```
┌─────────────────────────┐
│ Flutter UI │
│ PerAppProxyPage │ ← Уже готов!
└──────────┬──────────────┘
┌─────────────────────────┐
│ Platform Channel │
│ get_installed_programs │ ← Нужно создать
└──────────┬──────────────┘
┌─────────────────────────┐
│ C++ Windows Plugin │
│ Enumerate processes │ ← Нужно создать
└──────────┬──────────────┘
┌─────────────────────────┐
│ libcore config │
│ process_name: │ ← Нужно передавать
│ - chrome.exe │
│ - firefox.exe │
└─────────────────────────┘
┌─────────────────────────┐
│ sing-box routing │
│ По имени процесса │ ← Уже работает!
└─────────────────────────┘
```
### Что нужно реализовать
#### 1. Windows Plugin - получение списка программ ✅ ПРОСТО
**Файл:** `windows/runner/flutter_window.cpp`
```cpp
#include <windows.h>
#include <tlhelp32.h>
#include <shlobj.h>
#include <atlbase.h>
#include <string>
#include <set>
#include <vector>
// Структура для хранения информации о программе
struct ProgramInfo {
std::wstring name; // Название (из описания .exe)
std::wstring exePath; // Полный путь к .exe
std::wstring exeName; // Только имя файла (chrome.exe)
};
// Получить описание .exe файла (название программы)
std::wstring GetFileDescription(const std::wstring& filePath) {
DWORD dummy;
DWORD size = GetFileVersionInfoSizeW(filePath.c_str(), &dummy);
if (size == 0) return L"";
std::vector<BYTE> data(size);
if (!GetFileVersionInfoW(filePath.c_str(), 0, size, data.data())) {
return L"";
}
struct LANGANDCODEPAGE {
WORD language;
WORD codePage;
} *lpTranslate;
UINT cbTranslate;
if (!VerQueryValueW(data.data(), L"\\VarFileInfo\\Translation",
(LPVOID*)&lpTranslate, &cbTranslate)) {
return L"";
}
wchar_t subBlock[50];
swprintf(subBlock, 50, L"\\StringFileInfo\\%04x%04x\\FileDescription",
lpTranslate[0].language, lpTranslate[0].codePage);
LPWSTR lpBuffer;
UINT dwBytes;
if (VerQueryValueW(data.data(), subBlock, (LPVOID*)&lpBuffer, &dwBytes)) {
return std::wstring(lpBuffer);
}
return L"";
}
// Получить список всех запущенных процессов
std::set<std::wstring> GetRunningProcesses() {
std::set<std::wstring> processes;
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE) return processes;
PROCESSENTRY32W pe32;
pe32.dwSize = sizeof(PROCESSENTRY32W);
if (Process32FirstW(hSnapshot, &pe32)) {
do {
processes.insert(pe32.szExeFile);
} while (Process32NextW(hSnapshot, &pe32));
}
CloseHandle(hSnapshot);
return processes;
}
// Сканировать папки с программами
std::vector<ProgramInfo> ScanInstalledPrograms() {
std::vector<ProgramInfo> programs;
std::set<std::wstring> runningProcs = GetRunningProcesses();
// Папки для сканирования
std::vector<std::wstring> folders = {
L"C:\\Program Files",
L"C:\\Program Files (x86)",
};
// Добавить AppData\\Local\\Programs
wchar_t appDataPath[MAX_PATH];
if (SHGetFolderPathW(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, appDataPath) == S_OK) {
std::wstring localPrograms = std::wstring(appDataPath) + L"\\Programs";
folders.push_back(localPrograms);
}
for (const auto& folder : folders) {
WIN32_FIND_DATAW findData;
HANDLE hFind = FindFirstFileW((folder + L"\\*").c_str(), &findData);
if (hFind != INVALID_HANDLE_VALUE) {
do {
if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
if (wcscmp(findData.cFileName, L".") != 0 &&
wcscmp(findData.cFileName, L"..") != 0) {
// Искать .exe в подпапке
std::wstring subfolder = folder + L"\\" + findData.cFileName;
WIN32_FIND_DATAW exeFind;
HANDLE hExe = FindFirstFileW((subfolder + L"\\*.exe").c_str(), &exeFind);
if (hExe != INVALID_HANDLE_VALUE) {
do {
std::wstring exePath = subfolder + L"\\" + exeFind.cFileName;
std::wstring description = GetFileDescription(exePath);
if (!description.empty()) {
programs.push_back({
description,
exePath,
exeFind.cFileName
});
}
} while (FindNextFileW(hExe, &exeFind));
FindClose(hExe);
}
}
}
} while (FindNextFileW(hFind, &findData));
FindClose(hFind);
}
}
return programs;
}
// Flutter Method Channel Handler
void HandleGetInstalledPrograms(
const flutter::MethodCall<flutter::EncodableValue>& method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
auto programs = ScanInstalledPrograms();
flutter::EncodableList programList;
for (const auto& prog : programs) {
flutter::EncodableMap programMap;
// Конвертировать wstring в string (UTF-8)
int nameLen = WideCharToMultiByte(CP_UTF8, 0, prog.name.c_str(), -1, NULL, 0, NULL, NULL);
std::string name(nameLen - 1, 0);
WideCharToMultiByte(CP_UTF8, 0, prog.name.c_str(), -1, &name[0], nameLen, NULL, NULL);
int exeNameLen = WideCharToMultiByte(CP_UTF8, 0, prog.exeName.c_str(), -1, NULL, 0, NULL, NULL);
std::string exeName(exeNameLen - 1, 0);
WideCharToMultiByte(CP_UTF8, 0, prog.exeName.c_str(), -1, &exeName[0], exeNameLen, NULL, NULL);
programMap[flutter::EncodableValue("name")] = flutter::EncodableValue(name);
programMap[flutter::EncodableValue("packageName")] = flutter::EncodableValue(exeName);
programMap[flutter::EncodableValue("isSystemApp")] = flutter::EncodableValue(false);
programList.push_back(flutter::EncodableValue(programMap));
}
result->Success(flutter::EncodableValue(programList));
}
```
**Регистрация Method Channel:**
```cpp
// В flutter_window.cpp, метод OnCreate()
#include <flutter/method_channel.h>
#include <flutter/standard_method_codec.h>
// В FlutterWindow::OnCreate() после создания flutter_controller_:
auto channel = std::make_unique<flutter::MethodChannel<>>(
flutter_controller_->engine()->messenger(),
"com.umbrix.app/platform",
&flutter::StandardMethodCodec::GetInstance());
channel->SetMethodCallHandler(
[](const auto& call, auto result) {
if (call.method_name() == "get_installed_packages") {
HandleGetInstalledPrograms(call, std::move(result));
} else {
result->NotImplemented();
}
});
```
#### 2. Dart - адаптировать репозиторий ✅ ПРОСТО
**Файл:** `lib/features/per_app_proxy/data/per_app_proxy_repository.dart`
```dart
class PerAppProxyRepositoryImpl with InfraLogger implements PerAppProxyRepository {
final _methodChannel = const MethodChannel("com.umbrix.app/platform");
@override
TaskEither<String, List<InstalledPackageInfo>> getInstalledPackages() {
return TaskEither(
() async {
loggy.debug("getting installed packages info");
// ✅ УЖЕ РАБОТАЕТ на Android
// ✅ БУДЕТ РАБОТАТЬ на Windows после добавления C++ кода
final result = await _methodChannel.invokeMethod<String>("get_installed_packages");
if (result == null) return left("null response");
return right(
(jsonDecode(result) as List).map((e) {
return InstalledPackageInfo.fromJson(e as Map<String, dynamic>);
}).toList(),
);
},
);
}
@override
TaskEither<String, Uint8List> getPackageIcon(String packageName) {
// Для Windows можно извлекать иконку из .exe файла
// Или использовать placeholder иконку
return TaskEither(
() async {
if (PlatformUtils.isDesktop) {
// Временно возвращать пустую иконку
return right(Uint8List(0));
}
// Android код остаётся без изменений
loggy.debug("getting package [$packageName] icon");
final result = await _methodChannel.invokeMethod<String>(
"get_package_icon",
{"packageName": packageName},
);
if (result == null) return left("null response");
final Uint8List decoded;
try {
decoded = base64.decode(result);
} catch (e) {
return left("error parsing base64 response");
}
return right(decoded);
},
);
}
}
```
#### 3. libcore - передать список процессов ✅ СРЕДНЯЯ СЛОЖНОСТЬ
**Файл:** `libcore/config/config.go`
Нужно модифицировать функцию которая создаёт правила роутинга:
```go
// Добавить в структуру HiddifyOptions
type HiddifyOptions struct {
// ...существующие поля...
// Новые поля для Per-App Proxy
PerAppProxyMode string `json:"per_app_proxy_mode"` // "off", "include", "exclude"
IncludedApplications []string `json:"included_applications"` // ["chrome.exe", "firefox.exe"]
ExcludedApplications []string `json:"excluded_applications"` // ["steam.exe", "uTorrent.exe"]
}
// В функции buildRouteRules() после существующих правил:
func buildRouteRules(options *HiddifyOptions) []option.Rule {
var routeRules []option.Rule
// ...существующие правила...
// ====== PER-APP PROXY ДЛЯ WINDOWS/LINUX/MACOS ======
if runtime.GOOS != "android" { // Не Android
if options.PerAppProxyMode == "include" && len(options.IncludedApplications) > 0 {
// РЕЖИМ: Только выбранные приложения идут через VPN
// 1. Выбранные приложения → VPN
routeRules = append(routeRules, option.Rule{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultRule{
ProcessName: options.IncludedApplications, // ["chrome.exe", "firefox.exe"]
Outbound: OutboundSelectTag, // Через VPN
},
})
// 2. Все остальные → Direct
routeRules = append(routeRules, option.Rule{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultRule{
Outbound: OutboundDirectTag, // Напрямую
},
})
} else if options.PerAppProxyMode == "exclude" && len(options.ExcludedApplications) > 0 {
// РЕЖИМ: Исключённые приложения НЕ идут через VPN
// 1. Исключённые приложения → Direct
routeRules = append(routeRules, option.Rule{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultRule{
ProcessName: options.ExcludedApplications, // ["steam.exe", "uTorrent.exe"]
Outbound: OutboundDirectTag, // Напрямую
},
})
// 2. Все остальные → VPN (это уже есть в default правилах)
}
}
// Убедиться что Umbrix сам не идёт через VPN (чтобы избежать циклов)
routeRules = append(routeRules, option.Rule{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultRule{
ProcessName: []string{
"umbrix.exe",
"Umbrix.exe",
"UmbrixCli.exe",
"umbrix", // Linux
"Umbrix", // macOS
},
Outbound: OutboundBypassTag, // Всегда напрямую
},
})
return routeRules
}
```
**Файл:** `libcore/extension/interface.go`
Передавать опции из Flutter:
```go
func changeHiddifyOptions(jsonData string) error {
var opts HiddifyOptions
if err := json.Unmarshal([]byte(jsonData), &opts); err != nil {
return err
}
// Логирование для отладки
if opts.PerAppProxyMode != "off" {
log.Printf("[Per-App] Режим: %s", opts.PerAppProxyMode)
if opts.PerAppProxyMode == "include" {
log.Printf("[Per-App] Включены приложения: %v", opts.IncludedApplications)
} else if opts.PerAppProxyMode == "exclude" {
log.Printf("[Per-App] Исключены приложения: %v", opts.ExcludedApplications)
}
}
// Пересоздать конфигурацию с новыми правилами
return recreateConfigWithOptions(opts)
}
```
#### 4. Flutter - передавать список в libcore ✅ ПРОСТО
**Файл:** `lib/singbox/model/singbox_config_option.dart`
Добавить поля в `SingboxConfigOption`:
```dart
@freezed
class SingboxConfigOption with _$SingboxConfigOption {
const SingboxConfigOption._();
@JsonSerializable(fieldRename: FieldRename.kebab)
const factory SingboxConfigOption({
// ...существующие поля...
required List<SingboxRule> rules,
required SingboxMuxOption mux,
required SingboxTlsTricks tlsTricks,
required SingboxWarpOption warp,
required SingboxWarpOption warp2,
// ✨ НОВЫЕ ПОЛЯ
@Default("off") String perAppProxyMode, // "off", "include", "exclude"
@Default([]) List<String> includedApplications, // ["chrome.exe"]
@Default([]) List<String> excludedApplications, // ["steam.exe"]
}) = _SingboxConfigOption;
factory SingboxConfigOption.fromJson(Map<String, dynamic> json) =>
_$SingboxConfigOptionFromJson(json);
}
```
**Файл:** `lib/features/config_option/data/config_option_repository.dart`
Добавить считывание настроек:
```dart
Future<SingboxConfigOption> getConfigOptions() async {
final preferences = await ref.read(sharedPreferencesProvider.future);
// ...существующие настройки...
// ✨ Читать Per-App Proxy настройки
final perAppProxyMode = preferences.getString("per_app_proxy_mode") ?? "off";
final List<String> perAppList;
if (perAppProxyMode == "include") {
perAppList = preferences.getStringList("per_app_proxy_include_list") ?? [];
} else if (perAppProxyMode == "exclude") {
perAppList = preferences.getStringList("per_app_proxy_exclude_list") ?? [];
} else {
perAppList = [];
}
return SingboxConfigOption(
// ...существующие параметры...
// ✨ Передать в libcore
perAppProxyMode: perAppProxyMode,
includedApplications: perAppProxyMode == "include" ? perAppList : [],
excludedApplications: perAppProxyMode == "exclude" ? perAppList : [],
);
}
```
---
## 🚀 План реализации
### Этап 1: Windows C++ Plugin (1-2 часа)
- [x] Создать функцию `GetRunningProcesses()`
- [x] Создать функцию `ScanInstalledPrograms()`
- [x] Создать функцию `GetFileDescription()`
- [x] Добавить Method Channel handler
- [x] Тестирование: список программ отображается
### Этап 2: Dart адаптация (30 минут)
- [ ] Обновить `per_app_proxy_repository.dart` для Desktop
- [ ] Добавить placeholder иконки для Windows
- [ ] Тестирование: UI показывает программы
### Этап 3: libcore конфигурация (1 час)
- [ ] Добавить поля в `HiddifyOptions`
- [ ] Модифицировать `buildRouteRules()`
- [ ] Добавить логирование
- [ ] Пересобрать libcore для Windows
### Этап 4: Flutter интеграция (30 минут)
- [ ] Добавить поля в `SingboxConfigOption`
- [ ] Обновить `config_option_repository.dart`
- [ ] Генерация кода: `flutter pub run build_runner build`
### Этап 5: Тестирование (1 час)
- [ ] Режим "Все": все приложения через VPN
- [ ] Режим "Включить": только Chrome через VPN
- [ ] Режим "Исключить": Steam не через VPN
- [ ] Проверить логи libcore
- [ ] Проверить IP адрес в браузере vs Steam
### Этап 6: Оптимизация (опционально)
- [ ] Кэширование списка программ
- [ ] Извлечение иконок из .exe файлов
- [ ] Фоновое сканирование при запуске
- [ ] Фильтр по папкам (не показывать system32)
---
## 🧪 Примеры использования
### Пример 1: Только браузер через VPN
```
Настройки → Сеть → Исключения → вкладка "Приложения"
Режим: Включить [Proxy]
Выбрать:
✅ Google Chrome
✅ Mozilla Firefox
✅ Microsoft Edge
Результат:
- Браузеры идут через VPN ✅
- Все остальные программы напрямую ❌
```
### Пример 2: Исключить торренты и игры
```
Настройки → Сеть → Исключения → вкладка "Приложения"
Режим: Исключить [Exclude]
Выбрать:
✅ uTorrent
✅ Steam
✅ Epic Games Launcher
Результат:
- Торренты и игры идут напрямую ❌
- Все остальные программы через VPN ✅
```
### Пример 3: Исключить банковские приложения
```
Режим: Исключить [Exclude]
Выбрать:
✅ Sberbank Online
✅ Тинькофф
Результат:
- Банки работают со своими серверами ❌
- Остальное через VPN ✅
```
---
## 📊 Сравнение платформ
| Функция | Android | Windows | Linux | macOS |
|---------|---------|---------|-------|-------|
| Получить список приложений | ✅ | 🔨 | 🔨 | 🔨 |
| Иконки приложений | ✅ | ⏳ | ⏳ | ⏳ |
| Режим "Включить" | ✅ | 🔨 | 🔨 | 🔨 |
| Режим "Исключить" | ✅ | 🔨 | 🔨 | 🔨 |
| Фильтр по процессам | ✅ | ✅ | ✅ | ✅ |
✅ = Работает
🔨 = Нужно реализовать
⏳ = Опционально
---
## 🐛 Известные ограничения
### Windows
-Не все программы могут быть обнаружены (портативные версии)
- ❌ UWP приложения из Microsoft Store могут требовать специального подхода
- ⚠️ Фильтрация работает только по имени .exe (не по пути)
- ⚠️ Если программа меняет имя процесса, фильтр не сработает
### Sing-box
- ⚠️ `process_name` работает только в режиме TUN
-Не работает в режиме System Proxy
- ⚠️ На Linux требуется `/proc` доступ
---
## 🔗 Полезные ссылки
- **Sing-box routing rules:** https://sing-box.sagernet.org/configuration/route/rule/
- **Android VPNService:** https://developer.android.com/reference/android/net/VpnService
- **Windows Process Enumeration:** https://learn.microsoft.com/en-us/windows/win32/toolhelp/taking-a-snapshot-and-viewing-processes
---
## ✅ Чеклист для разработчика
Перед началом работы убедитесь что:
- [ ] Установлен Visual Studio 2022 с C++ Desktop Development
- [ ] Есть доступ к `windows/runner/flutter_window.cpp`
- [ ] Знакомы с Flutter Platform Channels
- [ ] Понимаете как работает sing-box routing
После реализации проверьте:
- [ ] Список программ загружается без ошибок
- [ ] UI отзывчив (сканирование не блокирует интерфейс)
- [ ] Настройки сохраняются после перезапуска
- [ ] VPN работает с выбранными приложениями
- [ ] Логи libcore показывают правильные правила
- [ ] Нет утечек памяти при переключении режимов
---
## 💡 Альтернативные подходы
### Подход 1: Windows Filtering Platform (WFP) - СЛОЖНО
Использовать Windows Firewall API для фильтрации по PID процесса.
- ✅ Более надёжно (работает на уровне ядра)
- ❌ Очень сложно в реализации
- ❌ Требует драйвер или высокие привилегии
### Подход 2: Прокси авторизация - СРЕДНЕ
Настроить прокси с авторизацией по процессу.
-Не требует TUN режим
-Не все приложения поддерживают прокси
- ❌ Нужен специальный прокси сервер
### Подход 3: Текущий (process_name в sing-box) - ПРОСТО ✅
Использовать встроенную поддержку sing-box.
- ✅ Уже реализовано в sing-box
- ✅ Работает надёжно
-Не требует дополнительных драйверов
- ⚠️ Только в TUN режиме
---
**Рекомендуемый подход:** #3 (текущий через sing-box)
Время на полную реализацию: **3-4 часа**
Сложность: **Средняя** (C++ + Dart + Go)
Приоритет: **Высокий** (очень востребованная функция)

View File

@@ -0,0 +1,106 @@
# 🚀 Быстрый старт: Split Tunneling для Windows
## Что это?
Выборочный VPN - некоторые приложения через VPN, другие напрямую.
**Примеры:**
- ✅ Только браузер через VPN
- ✅ Исключить Steam и торренты из VPN
- ✅ Исключить банковские приложения
## Текущее состояние
| Платформа | Статус |
|-----------|--------|
| Android | ✅ Работает |
| Windows | ⏳ Нужно реализовать |
| Linux | ⏳ Нужно реализовать |
| macOS | ⏳ Нужно реализовать |
## Что нужно сделать?
### 1. Windows C++ код (1-2 часа)
**Файл:** `windows/runner/flutter_window.cpp`
Добавить функцию сканирования установленных программ:
- Сканировать `C:\Program Files`
- Сканировать `C:\Program Files (x86)`
- Извлекать название из описания .exe
- Возвращать JSON список через Method Channel
**API:** `com.umbrix.app/platform` → метод `get_installed_packages`
### 2. libcore routing (1 час)
**Файл:** `libcore/config/config.go`
Добавить в `HiddifyOptions`:
```go
PerAppProxyMode string // "off", "include", "exclude"
IncludedApplications []string // ["chrome.exe", "firefox.exe"]
ExcludedApplications []string // ["steam.exe"]
```
Добавить правила:
```go
if mode == "include" {
// Выбранные → VPN
// Остальные → Direct
} else if mode == "exclude" {
// Выбранные → Direct
// Остальные → VPN
}
```
### 3. Flutter интеграция (30 минут)
**Файл:** `lib/singbox/model/singbox_config_option.dart`
```dart
@Default("off") String perAppProxyMode,
@Default([]) List<String> includedApplications,
@Default([]) List<String> excludedApplications,
```
Передавать в `changeHiddifyOptions()`.
## Тестирование
```powershell
# 1. Собрать
flutter build windows --release
# 2. Запустить
.\build\windows\x64\runner\Release\umbrix.exe
# 3. Открыть
Настройки Сеть Исключения вкладка "Приложения"
# 4. Выбрать режим
- Все: все приложения через VPN
- Включить: только выбранные через VPN
- Исключить: выбранные НЕ через VPN
# 5. Проверить
- Открыть 2ip.ru в браузере (должен показать VPN IP)
- Открыть Steam (должен показать реальный IP если исключён)
```
## Полная документация
См. [WINDOWS_SPLIT_TUNNELING.md](./WINDOWS_SPLIT_TUNNELING.md) для:
- Подробный архитектурный план
- Полный C++ код
- Примеры использования
- Известные ограничения
- Альтернативные подходы
## Приоритет
**ВЫСОКИЙ** - одна из самых востребованных функций!
Время: **3-4 часа**
Сложность: **Средняя**
Ожидаемый результат: ⭐⭐⭐⭐⭐

View File

@@ -163,6 +163,15 @@ Future<void> _performBootstrap(
);
}
// Автопроверка обновлений для Android
if (Platform.isAndroid) {
_safeInit(
"auto check updates",
() => container.read(appUpdateNotifierProvider.notifier).checkSilently(),
timeout: 5000,
);
}
if (Platform.isAndroid) {
await _safeInit(
"android display mode",

View File

@@ -9,7 +9,10 @@ import 'package:umbrix/core/model/constants.dart';
import 'package:umbrix/core/router/router.dart';
import 'package:umbrix/core/theme/app_theme.dart';
import 'package:umbrix/core/theme/theme_preferences.dart';
import 'package:umbrix/core/notification/in_app_notification_controller.dart';
import 'package:umbrix/features/app_update/notifier/app_update_notifier.dart';
import 'package:umbrix/features/app_update/notifier/app_update_state.dart';
import 'package:umbrix/features/app_update/widget/new_version_dialog.dart';
import 'package:umbrix/features/connection/widget/connection_wrapper.dart';
import 'package:umbrix/features/profile/notifier/profiles_update_notifier.dart';
import 'package:umbrix/features/shortcut/shortcut_wrapper.dart';
@@ -34,6 +37,31 @@ class App extends HookConsumerWidget with PresLogger {
ref.listen(foregroundProfilesUpdateNotifierProvider, (_, __) {});
// Слушаем состояние обновлений и показываем диалог
ref.listen(appUpdateNotifierProvider, (previous, next) {
if (next is AppUpdateStateAvailable) {
// Получаем BuildContext через router
final context = router.routerDelegate.navigatorKey.currentContext;
if (context != null && context.mounted) {
// Показываем диалог обновления
Future.delayed(const Duration(milliseconds: 500), () {
if (context.mounted) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => NewVersionDialog(
newVersion: next.versionInfo,
onIgnore: () {
ref.read(appUpdateNotifierProvider.notifier).ignoreRelease(next.versionInfo);
},
),
);
}
});
}
}
});
return WindowWrapper(
TrayWrapper(
ShortcutWrapper(

View File

@@ -41,7 +41,7 @@ class RemoteVersionEntity with _$RemoteVersionEntity {
// Для Windows - ищем .exe или .zip
if (extension == '.exe' || extension == '.zip') {
final targetExt = extension;
// Приоритет для zip: portable -> windows -> любой .zip
if (targetExt == '.zip') {
for (final pattern in ['portable', 'windows', 'win']) {
@@ -55,7 +55,7 @@ class RemoteVersionEntity with _$RemoteVersionEntity {
}
}
}
// Приоритет для exe: x64 setup/installer
if (targetExt == '.exe') {
for (final pattern in ['x64', 'amd64', 'win64', 'setup', 'installer']) {
@@ -69,7 +69,7 @@ class RemoteVersionEntity with _$RemoteVersionEntity {
}
}
}
// Если не нашли специфичный - берём любой с нужным расширением
try {
final asset = assets.firstWhere((asset) => asset.name.endsWith(targetExt));
@@ -92,7 +92,7 @@ class RemoteVersionEntity with _$RemoteVersionEntity {
continue;
}
}
// Если не нашли - берём любой .dmg
try {
final asset = assets.firstWhere((asset) => asset.name.endsWith('.dmg'));

View File

@@ -79,7 +79,7 @@ class NewVersionDialog extends HookConsumerWidget with PresLogger {
fileExt = '.zip';
final zipUrl = newVersion.findAssetByExtension('.zip');
if (zipUrl == null) {
fileExt = '.exe'; // Fallback на .exe если нет .zip
fileExt = '.exe'; // Fallback на .exe если нет .zip
}
} else if (Platform.isMacOS)
fileExt = '.dmg';
@@ -135,23 +135,13 @@ class NewVersionDialog extends HookConsumerWidget with PresLogger {
if (context.mounted) {
CustomToast('Установка обновления...', type: AlertType.info).show(context);
}
// Запускаем установщик в тихом режиме с правами администратора
// /VERYSILENT - без UI, /SUPPRESSMSGBOXES - без диалогов
// /NORESTART - не перезагружать систему
final result = await Process.run(
'powershell',
[
'-Command',
'Start-Process',
'-FilePath',
'"$savePath"',
'-ArgumentList',
'"/VERYSILENT", "/SUPPRESSMSGBOXES", "/NORESTART"',
'-Verb',
'RunAs',
'-Wait'
],
['-Command', 'Start-Process', '-FilePath', '"$savePath"', '-ArgumentList', '"/VERYSILENT", "/SUPPRESSMSGBOXES", "/NORESTART"', '-Verb', 'RunAs', '-Wait'],
);
if (result.exitCode == 0) {
@@ -182,33 +172,25 @@ class NewVersionDialog extends HookConsumerWidget with PresLogger {
// Получить путь к исполняемому файлу приложения
final exePath = Platform.resolvedExecutable;
final appDir = Directory(exePath).parent.path;
// Распаковать во временную папку
final tempDir = Directory('${Directory.systemTemp.path}\\umbrix_update_${DateTime.now().millisecondsSinceEpoch}');
await tempDir.create(recursive: true);
loggy.info('Extracting ZIP to: ${tempDir.path}');
// Распаковка через PowerShell
final extractResult = await Process.run(
'powershell',
[
'-Command',
'Expand-Archive',
'-Path',
'"$savePath"',
'-DestinationPath',
'"${tempDir.path}"',
'-Force'
],
['-Command', 'Expand-Archive', '-Path', '"$savePath"', '-DestinationPath', '"${tempDir.path}"', '-Force'],
);
if (extractResult.exitCode != 0) {
throw Exception('Failed to extract ZIP: ${extractResult.stderr}');
}
loggy.info('ZIP extracted successfully');
// Скрипт для замены файлов после закрытия приложения
final updateScript = '''
@echo off
@@ -227,23 +209,23 @@ start "" "$exePath"
echo Update complete!
del "%~f0"
''';
final scriptPath = '${Directory.systemTemp.path}\\umbrix_update.bat';
await File(scriptPath).writeAsString(updateScript);
if (context.mounted) {
CustomToast.success('Обновление установлено! Приложение перезагрузится...').show(context);
context.pop();
}
// Запустить скрипт и закрыть приложение
await Process.start('cmd', ['/c', scriptPath], mode: ProcessStartMode.detached);
// Задержка перед выходом
Future.delayed(const Duration(seconds: 1), () {
exit(0);
});
return;
} catch (e) {
loggy.warning('Failed to install from ZIP: $e');