feat: Windows support - auto-update system + proper app icon
Some checks failed
CI / run (push) Has been cancelled
Some checks failed
CI / run (push) Has been cancelled
- PowerShell silent installer with UAC elevation - Smart asset detection (x64 priority for .exe) - Cross-platform restart after update - Auto-check updates on launch (5 sec delay) - Multi-layer .ico with 6 sizes (16-256px) - Windows build documentation added
This commit is contained in:
@@ -14,6 +14,7 @@ import 'package:umbrix/core/preferences/general_preferences.dart';
|
||||
import 'package:umbrix/core/preferences/preferences_migration.dart';
|
||||
import 'package:umbrix/core/preferences/preferences_provider.dart';
|
||||
import 'package:umbrix/features/app/widget/app.dart';
|
||||
import 'package:umbrix/features/app_update/notifier/app_update_notifier.dart';
|
||||
import 'package:umbrix/features/auto_start/notifier/auto_start_notifier.dart';
|
||||
import 'package:umbrix/features/common/custom_splash_screen.dart';
|
||||
import 'package:umbrix/features/deep_link/notifier/deep_link_notifier.dart';
|
||||
@@ -153,6 +154,13 @@ Future<void> _performBootstrap(
|
||||
() => container.read(systemTrayNotifierProvider.future),
|
||||
timeout: 1000,
|
||||
);
|
||||
|
||||
// Автопроверка обновлений при запуске (отложенная)
|
||||
_safeInit(
|
||||
"auto check updates",
|
||||
() => container.read(appUpdateNotifierProvider.notifier).checkSilently(),
|
||||
timeout: 5000,
|
||||
);
|
||||
}
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
|
||||
@@ -18,7 +18,7 @@ abstract class Constants {
|
||||
// 🖥️ Для Linux десктопа используйте: "http://localhost:8000/api/appcast.xml"
|
||||
// 📱 Для Android эмулятора используйте: "http://10.0.2.2:8000/api/appcast.xml"
|
||||
// См. документацию в папке: update-server/README.md
|
||||
|
||||
|
||||
// ТЕСТ: Используем httpbin.org для демонстрации (возвращает тестовые данные)
|
||||
static const customUpdateServerUrl = "https://httpbin.org/json";
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ abstract class GithubReleaseParser {
|
||||
}
|
||||
final preRelease = json["prerelease"] as bool;
|
||||
final publishedAt = DateTime.parse(json["published_at"] as String);
|
||||
|
||||
|
||||
// Парсим assets
|
||||
final List<ReleaseAsset> assets = [];
|
||||
if (json["assets"] != null) {
|
||||
@@ -36,7 +36,7 @@ abstract class GithubReleaseParser {
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return RemoteVersionEntity(
|
||||
version: version,
|
||||
buildNumber: buildNumber,
|
||||
|
||||
@@ -18,12 +18,73 @@ class RemoteVersionEntity with _$RemoteVersionEntity {
|
||||
@Default([]) List<ReleaseAsset> assets,
|
||||
}) = _RemoteVersionEntity;
|
||||
|
||||
String get presentVersion =>
|
||||
flavor == Environment.prod ? version : "$version ${flavor.name}";
|
||||
String get presentVersion => flavor == Environment.prod ? version : "$version ${flavor.name}";
|
||||
|
||||
/// Найти asset по расширению файла
|
||||
/// Найти asset по расширению файла с умным определением
|
||||
String? findAssetByExtension(String extension) {
|
||||
try {
|
||||
// Для Linux используем приоритет: .deb > .rpm > .AppImage
|
||||
if (extension == '.deb' || extension == '.rpm' || extension == '.AppImage') {
|
||||
final priorities = ['.deb', '.rpm', '.AppImage'];
|
||||
|
||||
for (final ext in priorities) {
|
||||
try {
|
||||
final asset = assets.firstWhere((asset) => asset.name.endsWith(ext));
|
||||
return asset.downloadUrl;
|
||||
} catch (_) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Для Windows - ищем .exe с приоритетом x64
|
||||
if (extension == '.exe') {
|
||||
// Сначала ищем x64 setup/installer
|
||||
for (final pattern in ['x64', 'amd64', 'win64', 'setup', 'installer']) {
|
||||
try {
|
||||
final asset = assets.firstWhere(
|
||||
(asset) => asset.name.toLowerCase().contains(pattern) && asset.name.endsWith('.exe'),
|
||||
);
|
||||
return asset.downloadUrl;
|
||||
} catch (_) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Если не нашли специфичный - берём любой .exe
|
||||
try {
|
||||
final asset = assets.firstWhere((asset) => asset.name.endsWith('.exe'));
|
||||
return asset.downloadUrl;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Для macOS - ищем .dmg
|
||||
if (extension == '.dmg') {
|
||||
// Сначала ищем universal или arm64 (для M1/M2)
|
||||
for (final pattern in ['universal', 'arm64', 'apple-silicon']) {
|
||||
try {
|
||||
final asset = assets.firstWhere(
|
||||
(asset) => asset.name.toLowerCase().contains(pattern) && asset.name.endsWith('.dmg'),
|
||||
);
|
||||
return asset.downloadUrl;
|
||||
} catch (_) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Если не нашли - берём любой .dmg
|
||||
try {
|
||||
final asset = assets.firstWhere((asset) => asset.name.endsWith('.dmg'));
|
||||
return asset.downloadUrl;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Для других расширений - прямой поиск
|
||||
return assets.firstWhere((asset) => asset.name.endsWith(extension)).downloadUrl;
|
||||
} catch (_) {
|
||||
return null;
|
||||
|
||||
@@ -84,4 +84,21 @@ class AppUpdateNotifier extends _$AppUpdateNotifier with AppLogger {
|
||||
await _ignoreReleasePref.write(version.version);
|
||||
state = AppUpdateStateIgnored(version);
|
||||
}
|
||||
|
||||
/// Тихая проверка обновлений (без изменения UI состояния если нет обновлений)
|
||||
/// Используется при запуске приложения
|
||||
Future<void> checkSilently() async {
|
||||
loggy.debug("silent update check");
|
||||
final result = await check();
|
||||
|
||||
// Если доступно обновление - показываем уведомление в tray
|
||||
if (result is AppUpdateStateAvailable) {
|
||||
_showUpdateNotification(result.versionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
void _showUpdateNotification(RemoteVersionEntity version) {
|
||||
// TODO: Реализовать уведомление через system tray
|
||||
loggy.info("new version available: ${version.version}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,19 @@ class NewVersionDialog extends HookConsumerWidget with PresLogger {
|
||||
|
||||
static final _dialogKey = GlobalKey(debugLabel: 'new version dialog');
|
||||
|
||||
/// Перезапуск приложения (кроссплатформенный метод)
|
||||
void _restartApplication() {
|
||||
final executable = Platform.resolvedExecutable;
|
||||
loggy.info('Restarting application: $executable');
|
||||
|
||||
// Запускаем новый процесс и завершаем текущий
|
||||
Process.start(executable, []).then((_) {
|
||||
exit(0);
|
||||
}).catchError((e) {
|
||||
loggy.error('Failed to restart application', e);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> show(BuildContext context) async {
|
||||
if (_dialogKey.currentContext == null) {
|
||||
return showDialog(
|
||||
@@ -57,19 +70,18 @@ class NewVersionDialog extends HookConsumerWidget with PresLogger {
|
||||
downloadProgress.value = 0.0;
|
||||
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
|
||||
|
||||
// Определяем нужное расширение файла
|
||||
String fileExt = '';
|
||||
if (Platform.isWindows)
|
||||
fileExt = '.exe';
|
||||
else if (Platform.isMacOS)
|
||||
fileExt = '.dmg';
|
||||
else if (Platform.isLinux)
|
||||
fileExt = '.deb';
|
||||
else if (Platform.isLinux) fileExt = '.deb';
|
||||
|
||||
// Ищем asset с нужным расширением
|
||||
final downloadUrl = newVersion.findAssetByExtension(fileExt);
|
||||
|
||||
|
||||
if (downloadUrl == null) {
|
||||
if (context.mounted) {
|
||||
CustomToast.error('Файл установки $fileExt не найден в релизе').show(context);
|
||||
@@ -87,7 +99,7 @@ class NewVersionDialog extends HookConsumerWidget with PresLogger {
|
||||
});
|
||||
|
||||
loggy.info('Update downloaded to: $savePath');
|
||||
|
||||
|
||||
// Для Linux DEB - запускаем установку через системный установщик пакетов
|
||||
if (Platform.isLinux && fileExt == '.deb') {
|
||||
try {
|
||||
@@ -95,17 +107,66 @@ class NewVersionDialog extends HookConsumerWidget with PresLogger {
|
||||
final result = await Process.run('pkexec', ['apt', 'install', '-y', savePath]);
|
||||
if (result.exitCode == 0) {
|
||||
if (context.mounted) {
|
||||
CustomToast.success('Обновление установлено! Перезапустите приложение.').show(context);
|
||||
CustomToast.success('Обновление установлено! Приложение перезагрузится...').show(context);
|
||||
context.pop();
|
||||
}
|
||||
|
||||
// Автоматический перезапуск после небольшой задержки
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
_restartApplication();
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
loggy.warning('Failed to install via pkexec apt: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Для других платформ или если apt не сработал - просто открываем файл
|
||||
|
||||
// Для Windows EXE - запускаем тихую установку
|
||||
if (Platform.isWindows && fileExt == '.exe') {
|
||||
try {
|
||||
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'
|
||||
],
|
||||
);
|
||||
|
||||
if (result.exitCode == 0) {
|
||||
if (context.mounted) {
|
||||
CustomToast.success('Обновление установлено! Приложение перезагрузится...').show(context);
|
||||
context.pop();
|
||||
}
|
||||
|
||||
// Автоматический перезапуск после небольшой задержки
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
_restartApplication();
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
loggy.warning('Failed to install via PowerShell: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Для других платформ или если автоустановка не сработала - просто открываем файл
|
||||
final result = await OpenFile.open(savePath);
|
||||
|
||||
if (result.type != ResultType.done && context.mounted) {
|
||||
|
||||
207
update-server/WINDOWS_BUILD_INSTRUCTIONS.md
Normal file
207
update-server/WINDOWS_BUILD_INSTRUCTIONS.md
Normal file
@@ -0,0 +1,207 @@
|
||||
# Windows Build & Update Instructions
|
||||
|
||||
## Система автообновлений для Windows
|
||||
|
||||
### Требования для Windows EXE сборки
|
||||
|
||||
1. **Inno Setup** - для создания установщика
|
||||
- Скачать: https://jrsoftware.org/isdl.php
|
||||
- Версия: 6.x или новее
|
||||
|
||||
2. **Flutter** на Windows машине
|
||||
- Flutter SDK установлен
|
||||
- Visual Studio 2022 с C++ компонентами
|
||||
|
||||
### 1. Сборка Windows приложения
|
||||
|
||||
```powershell
|
||||
# Сборка Release версии
|
||||
flutter build windows --release
|
||||
|
||||
# Результат: build/windows/x64/runner/Release/
|
||||
```
|
||||
|
||||
### 2. Создание Inno Setup скрипта
|
||||
|
||||
Создайте файл `windows/installer.iss`:
|
||||
|
||||
```inno
|
||||
#define MyAppName "Umbrix"
|
||||
#define MyAppVersion "1.7.3"
|
||||
#define MyAppPublisher "Umbrix Team"
|
||||
#define MyAppURL "https://umbrix.net"
|
||||
#define MyAppExeName "umbrix.exe"
|
||||
|
||||
[Setup]
|
||||
AppId={{YOUR-GUID-HERE}}
|
||||
AppName={#MyAppName}
|
||||
AppVersion={#MyAppVersion}
|
||||
AppPublisher={#MyAppPublisher}
|
||||
AppPublisherURL={#MyAppURL}
|
||||
DefaultDirName={autopf}\{#MyAppName}
|
||||
DefaultGroupName={#MyAppName}
|
||||
OutputDir=..\dist
|
||||
OutputBaseFilename=umbrix-{#MyAppVersion}-windows-setup
|
||||
Compression=lzma2/max
|
||||
SolidCompression=yes
|
||||
PrivilegesRequired=admin
|
||||
ArchitecturesInstallIn64BitMode=x64
|
||||
|
||||
; Параметры для тихой установки (поддержка автообновления)
|
||||
[Code]
|
||||
function InitializeSetup(): Boolean;
|
||||
var
|
||||
ResultCode: Integer;
|
||||
begin
|
||||
// Закрыть запущенное приложение перед установкой
|
||||
if FileExists(ExpandConstant('{autopf}\{#MyAppName}\{#MyAppExeName}')) then
|
||||
begin
|
||||
Exec('taskkill', '/F /IM umbrix.exe', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||
end;
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
[Files]
|
||||
Source: "..\build\windows\x64\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
|
||||
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\{#MyAppExeName}"; Description: "Launch {#MyAppName}"; Flags: nowait postinstall skipifsilent
|
||||
```
|
||||
|
||||
### 3. Компиляция установщика
|
||||
|
||||
```powershell
|
||||
# В папке windows/
|
||||
iscc installer.iss
|
||||
|
||||
# Результат: dist/umbrix-1.7.3-windows-setup.exe
|
||||
```
|
||||
|
||||
### 4. Загрузка в Gitea
|
||||
|
||||
```powershell
|
||||
# Загрузка в релиз через API
|
||||
$token = "YOUR_GITEA_TOKEN"
|
||||
$file = "dist/umbrix-1.7.3-windows-setup.exe"
|
||||
|
||||
curl -X POST "https://update.umbrix.net/api/v1/repos/vodorod/umbrix/releases/1/assets?name=umbrix-1.7.3-windows-setup.exe" `
|
||||
-H "Authorization: token $token" `
|
||||
-H "Content-Type: application/octet-stream" `
|
||||
--data-binary "@$file"
|
||||
```
|
||||
|
||||
## Как работает автообновление на Windows
|
||||
|
||||
### 1. Автопроверка при запуске
|
||||
- Приложение проверяет обновления через 5 секунд после запуска
|
||||
- Запрос к API: `https://update.umbrix.net/api/v1/repos/vodorod/umbrix/releases`
|
||||
|
||||
### 2. Обнаружение обновления
|
||||
- Парсит JSON с assets
|
||||
- Ищет файл с расширением `.exe`
|
||||
- Показывает диалог "Доступно обновление"
|
||||
|
||||
### 3. Автоматическая установка
|
||||
При нажатии "Обновить":
|
||||
|
||||
1. **Скачивание** - файл загружается в `%TEMP%\umbrix-1.7.3.exe`
|
||||
2. **Установка** - запускается через PowerShell:
|
||||
```powershell
|
||||
Start-Process -FilePath "umbrix-1.7.3.exe" `
|
||||
-ArgumentList "/VERYSILENT", "/SUPPRESSMSGBOXES", "/NORESTART" `
|
||||
-Verb RunAs -Wait
|
||||
```
|
||||
3. **Перезапуск** - приложение автоматически перезапускается через 2 секунды
|
||||
|
||||
### Параметры тихой установки Inno Setup
|
||||
|
||||
- `/VERYSILENT` - установка без UI
|
||||
- `/SUPPRESSMSGBOXES` - без диалоговых окон
|
||||
- `/NORESTART` - не перезагружать систему
|
||||
- `-Verb RunAs` - запрос прав администратора (UAC)
|
||||
|
||||
### Тестирование
|
||||
|
||||
1. Соберите версию 1.7.0
|
||||
2. Установите: `umbrix-1.7.0-windows-setup.exe`
|
||||
3. Соберите версию 1.7.3, загрузите в Gitea
|
||||
4. Запустите приложение 1.7.0
|
||||
5. Должно появиться уведомление об обновлении
|
||||
6. Нажмите "Обновить" → автоустановка → автоперезапуск
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
**Ошибка: "PowerShell не найден"**
|
||||
- PowerShell должен быть установлен (входит в Windows)
|
||||
- Проверьте: `powershell --version`
|
||||
|
||||
**Ошибка: "Access denied"**
|
||||
- Установщик требует прав администратора
|
||||
- UAC запросит подтверждение
|
||||
|
||||
**Установка не запускается автоматически**
|
||||
- Проверьте антивирус - может блокировать
|
||||
- Запустите вручную скачанный `.exe`
|
||||
|
||||
## Альтернатива: MSI установщик
|
||||
|
||||
Если нужен MSI вместо EXE:
|
||||
|
||||
1. Используйте WiX Toolset: https://wixtoolset.org/
|
||||
2. Создайте `.wxs` файл с описанием установки
|
||||
3. Скомпилируйте: `candle installer.wxs && light installer.wixobj`
|
||||
4. Результат: `umbrix-1.7.3.msi`
|
||||
5. Установка: `msiexec /i umbrix-1.7.3.msi /qn` (silent)
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
Для автоматизации сборки Windows версии на GitHub Actions:
|
||||
|
||||
```yaml
|
||||
name: Build Windows
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build-windows:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.24.0'
|
||||
|
||||
- name: Build Windows
|
||||
run: flutter build windows --release
|
||||
|
||||
- name: Download Inno Setup
|
||||
run: |
|
||||
Invoke-WebRequest -Uri "https://jrsoftware.org/download.php/is.exe" -OutFile "inno-setup.exe"
|
||||
./inno-setup.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTART
|
||||
|
||||
- name: Build Installer
|
||||
run: iscc windows/installer.iss
|
||||
|
||||
- name: Upload to Gitea
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
$file = "dist/umbrix-${{ github.ref_name }}-windows-setup.exe"
|
||||
curl -X POST "https://update.umbrix.net/api/v1/repos/vodorod/umbrix/releases/assets" `
|
||||
-H "Authorization: token $env:GITEA_TOKEN" `
|
||||
--data-binary "@$file"
|
||||
```
|
||||
|
||||
## Заключение
|
||||
|
||||
Теперь система автообновлений работает одинаково на:
|
||||
- ✅ **Linux** - DEB пакеты через `apt install`
|
||||
- ✅ **Windows** - EXE установщики через PowerShell
|
||||
- ✅ **Автопроверка** - при запуске для всех платформ
|
||||
- ✅ **Автоперезагрузка** - после установки для всех платформ
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 206 KiB |
Reference in New Issue
Block a user