From 597d9f59aed7c310840010f83a76ed739c82527d Mon Sep 17 00:00:00 2001 From: Umbrix Developer Date: Tue, 20 Jan 2026 10:54:47 +0300 Subject: [PATCH] feat: Add portable ZIP update for Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../model/remote_version_entity.dart | 44 ++++++--- .../app_update/widget/new_version_dialog.dart | 90 ++++++++++++++++++- 2 files changed, 118 insertions(+), 16 deletions(-) diff --git a/lib/features/app_update/model/remote_version_entity.dart b/lib/features/app_update/model/remote_version_entity.dart index ad9a1b30..0f019b7d 100644 --- a/lib/features/app_update/model/remote_version_entity.dart +++ b/lib/features/app_update/model/remote_version_entity.dart @@ -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; diff --git a/lib/features/app_update/widget/new_version_dialog.dart b/lib/features/app_update/widget/new_version_dialog.dart index 21a0be96..a854af82 100644 --- a/lib/features/app_update/widget/new_version_dialog.dart +++ b/lib/features/app_update/widget/new_version_dialog.dart @@ -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);