feat: Add portable ZIP update for Windows
Some checks failed
CI / run (push) Has been cancelled

Two update methods now available:
1. EXE installer (requires UAC, shows SmartScreen without signature)

Changes:
- Added .zip asset detection with priority (portable/windows/win)
- ZIP auto-install: extract → replace files → restart
- Priority: .zip → .exe (portable first for better UX)
- Batch script handles file replacement after app closes
- No administrator rights needed for ZIP updates

Benefits of ZIP:
 No UAC prompts
 No SmartScreen warnings
 Fast updates (just file replacement)
 Perfect for testing without code signing
 Fallback to .exe if .zip not available
This commit is contained in:
Umbrix Developer
2026-01-20 10:54:47 +03:00
parent cd5b3493a2
commit 597d9f59ae
2 changed files with 118 additions and 16 deletions

View File

@@ -38,23 +38,41 @@ class RemoteVersionEntity with _$RemoteVersionEntity {
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;
// Для 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']) {
try {
final asset = assets.firstWhere(
(asset) => asset.name.toLowerCase().contains(pattern) && asset.name.endsWith('.zip'),
);
return asset.downloadUrl;
} catch (_) {
continue;
}
}
}
// Если не нашли специфичный - берём любой .exe
// Приоритет для exe: x64 setup/installer
if (targetExt == '.exe') {
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;
}
}
}
// Если не нашли специфичный - берём любой с нужным расширением
try {
final asset = assets.firstWhere((asset) => asset.name.endsWith('.exe'));
final asset = assets.firstWhere((asset) => asset.name.endsWith(targetExt));
return asset.downloadUrl;
} catch (_) {
return null;

View File

@@ -73,9 +73,15 @@ class NewVersionDialog extends HookConsumerWidget with PresLogger {
// Определяем нужное расширение файла
String fileExt = '';
if (Platform.isWindows)
fileExt = '.exe';
else if (Platform.isMacOS)
if (Platform.isWindows) {
// Для Windows приоритет: .zip (portable) → .exe (installer)
// ZIP не требует UAC и подписи кода!
fileExt = '.zip';
final zipUrl = newVersion.findAssetByExtension('.zip');
if (zipUrl == null) {
fileExt = '.exe'; // Fallback на .exe если нет .zip
}
} else if (Platform.isMacOS)
fileExt = '.dmg';
else if (Platform.isLinux) fileExt = '.deb';
@@ -166,6 +172,84 @@ class NewVersionDialog extends HookConsumerWidget with PresLogger {
}
}
// Windows portable ZIP update
if (Platform.isWindows && fileExt == '.zip') {
try {
if (context.mounted) {
CustomToast('Установка обновления из ZIP...', type: AlertType.info).show(context);
}
// Получить путь к исполняемому файлу приложения
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'
],
);
if (extractResult.exitCode != 0) {
throw Exception('Failed to extract ZIP: ${extractResult.stderr}');
}
loggy.info('ZIP extracted successfully');
// Скрипт для замены файлов после закрытия приложения
final updateScript = '''
@echo off
echo Waiting for application to close...
timeout /t 3 /nobreak > nul
echo Updating files...
xcopy /E /Y "${tempDir.path}\\*" "$appDir\\"
echo Cleanup...
rmdir /S /Q "${tempDir.path}"
echo Starting application...
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');
}
}
// Для других платформ или если автоустановка не сработала - просто открываем файл
final result = await OpenFile.open(savePath);