feat: Android auto-update notifications with dialog
- 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:
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user