feat: Windows support - auto-update system + proper app icon
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:
Umbrix Developer
2026-01-19 17:48:21 +03:00
parent 95383d09fc
commit 796c223d44
8 changed files with 368 additions and 14 deletions

View File

@@ -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,

View File

@@ -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;

View File

@@ -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}");
}
}

View File

@@ -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) {