feat: Android auto-update notifications with dialog
Some checks failed
Upload store MSIX to release / upload-store-msix-to-release (push) Has been cancelled
CI / run (push) Has been cancelled

- 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

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');