From 93efd524cd23b69deae5bd4bf6ca9132931103de Mon Sep 17 00:00:00 2001 From: klockky Date: Tue, 14 Jul 2026 19:55:02 +0300 Subject: [PATCH] feat(settings): add manual update check --- lib/core/utils/update_checker.dart | 42 +++++++++++++++++-- .../screens/profile/settings_tab.dart | 38 +++++++++++++++++ lib/l10n/app_en.arb | 6 ++- lib/l10n/app_localizations.dart | 24 +++++++++++ lib/l10n/app_localizations_en.dart | 13 ++++++ lib/l10n/app_localizations_ru.dart | 13 ++++++ lib/l10n/app_ru.arb | 6 ++- 7 files changed, 136 insertions(+), 6 deletions(-) diff --git a/lib/core/utils/update_checker.dart b/lib/core/utils/update_checker.dart index cdf3dd4..6501979 100644 --- a/lib/core/utils/update_checker.dart +++ b/lib/core/utils/update_checker.dart @@ -22,6 +22,22 @@ class AppUpdateInfo { }); } +enum UpdateCheckStatus { updateAvailable, upToDate, failed } + +class UpdateCheckResult { + final UpdateCheckStatus status; + final AppUpdateInfo? update; + + const UpdateCheckResult._(this.status, [this.update]); + + const UpdateCheckResult.updateAvailable(AppUpdateInfo update) + : this._(UpdateCheckStatus.updateAvailable, update); + + const UpdateCheckResult.upToDate() : this._(UpdateCheckStatus.upToDate); + + const UpdateCheckResult.failed() : this._(UpdateCheckStatus.failed); +} + abstract class UpdateChecker { static const String _owner = 'KometTeam'; static const String _repo = 'Komet'; @@ -96,6 +112,21 @@ abstract class UpdateChecker { return update; } + /// Runs a user-initiated check without applying the automatic-check interval + /// or the "skip this version" preference. + static Future checkNow() async { + try { + final update = await fetchLatest(); + final prefs = await SharedPreferences.getInstance(); + await prefs.setInt(_lastCheckKey, DateTime.now().millisecondsSinceEpoch); + return update == null + ? const UpdateCheckResult.upToDate() + : UpdateCheckResult.updateAvailable(update); + } catch (_) { + return const UpdateCheckResult.failed(); + } + } + static Future skip(String tag) async { final prefs = await SharedPreferences.getInstance(); await prefs.setString(_skippedTagKey, tag); @@ -117,22 +148,25 @@ abstract class UpdateChecker { final resp = await req.close().timeout(_timeout); if (resp.statusCode != HttpStatus.ok) { await resp.drain(); - return null; + throw HttpException( + 'GitHub returned HTTP ${resp.statusCode}', + uri: uri, + ); } final body = await resp .transform(const Utf8Decoder()) .join() .timeout(_timeout); final decoded = jsonDecode(body); - if (decoded is! List) return null; + if (decoded is! List) { + throw const FormatException('Invalid GitHub releases response'); + } for (final entry in decoded) { if (entry is! Map) continue; if (entry['draft'] == true) continue; return entry.cast(); } return null; - } catch (_) { - return null; } finally { client.close(force: true); } diff --git a/lib/frontend/screens/profile/settings_tab.dart b/lib/frontend/screens/profile/settings_tab.dart index 7f8eb94..f3b3561 100644 --- a/lib/frontend/screens/profile/settings_tab.dart +++ b/lib/frontend/screens/profile/settings_tab.dart @@ -9,6 +9,7 @@ import '../../../core/config/komet_settings.dart'; import '../../../core/config/app_show_extra_info.dart'; import '../../../core/storage/app_database.dart'; import '../../../core/utils/format.dart'; +import '../../../core/utils/update_checker.dart'; import '../../../l10n/app_localizations.dart'; import '../../../main.dart'; import '../../widgets/avatar_history_screen.dart'; @@ -19,6 +20,7 @@ import '../../widgets/settings_card.dart'; import '../../widgets/sheet_helpers.dart'; import '../../widgets/small_spinner.dart'; import '../../widgets/custom_notification.dart'; +import '../../widgets/update_dialog.dart'; import '../auth/login_screen.dart'; import '../auth/proxy_settings_sheet.dart'; import '../../../core/config/app_digital_id_mode.dart'; @@ -49,6 +51,7 @@ class _SettingsTabState extends State { bool _isPhoneVisible = false; String? _appVersionLabel; bool _debugMenuVisible = false; + bool _isCheckingForUpdates = false; int _versionSecretTapCount = 0; Timer? _versionSecretTapResetTimer; StreamSubscription? _profileUpdateSub; @@ -105,6 +108,33 @@ class _SettingsTabState extends State { }); } + Future _checkForUpdates() async { + if (_isCheckingForUpdates) return; + setState(() => _isCheckingForUpdates = true); + + final result = await UpdateChecker.checkNow(); + if (!mounted) return; + setState(() => _isCheckingForUpdates = false); + + switch (result.status) { + case UpdateCheckStatus.updateAvailable: + await showUpdateDialog(context, result.update!); + return; + case UpdateCheckStatus.upToDate: + showCustomNotification( + context, + AppLocalizations.of(context)!.updateUpToDate, + ); + return; + case UpdateCheckStatus.failed: + showCustomNotification( + context, + AppLocalizations.of(context)!.updateCheckFailed, + ); + return; + } + } + Future _openCloudStorage(BuildContext context) async { final cs = Theme.of(context).colorScheme; final ok = await showInfoActionSheet( @@ -221,6 +251,7 @@ class _SettingsTabState extends State { @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; + final l10n = AppLocalizations.of(context)!; if (_profile == null) { return const Center(child: SmallSpinner(size: 36)); @@ -444,6 +475,13 @@ class _SettingsTabState extends State { child: _buildSection( context, items: [ + _SettingsItem( + icon: Symbols.system_update, + label: _isCheckingForUpdates + ? l10n.updateChecking + : l10n.updateCheck, + onTap: _isCheckingForUpdates ? null : _checkForUpdates, + ), _SettingsItem( leading: Image.asset( 'assets/komet.png', diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b4d6bb0..861cf4f 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -903,5 +903,9 @@ "updateLater": "Later", "updateSkip": "Skip", "updateDownloading": "Downloading update…", - "updateDownloadFailed": "Failed to download the update" + "updateDownloadFailed": "Failed to download the update", + "updateCheck": "Check for updates", + "updateChecking": "Checking for updates…", + "updateUpToDate": "You have the latest version", + "updateCheckFailed": "Couldn't check for updates. Try again later" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 2956c43..3069059 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4021,6 +4021,30 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Failed to download the update'** String get updateDownloadFailed; + + /// No description provided for @updateCheck. + /// + /// In en, this message translates to: + /// **'Check for updates'** + String get updateCheck; + + /// No description provided for @updateChecking. + /// + /// In en, this message translates to: + /// **'Checking for updates…'** + String get updateChecking; + + /// No description provided for @updateUpToDate. + /// + /// In en, this message translates to: + /// **'You have the latest version'** + String get updateUpToDate; + + /// No description provided for @updateCheckFailed. + /// + /// In en, this message translates to: + /// **'Couldn\'t check for updates. Try again later'** + String get updateCheckFailed; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 36a35aa..218904c 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2084,4 +2084,17 @@ class AppLocalizationsEn extends AppLocalizations { @override String get updateDownloadFailed => 'Failed to download the update'; + + @override + String get updateCheck => 'Check for updates'; + + @override + String get updateChecking => 'Checking for updates…'; + + @override + String get updateUpToDate => 'You have the latest version'; + + @override + String get updateCheckFailed => + 'Couldn\'t check for updates. Try again later'; } diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 98f6636..a81bbf5 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2095,4 +2095,17 @@ class AppLocalizationsRu extends AppLocalizations { @override String get updateDownloadFailed => 'Не удалось скачать обновление'; + + @override + String get updateCheck => 'Проверить обновление'; + + @override + String get updateChecking => 'Проверяем обновления…'; + + @override + String get updateUpToDate => 'Установлена актуальная версия'; + + @override + String get updateCheckFailed => + 'Не удалось проверить обновления. Повторите позже'; } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 2548d5c..0ace50d 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -685,5 +685,9 @@ "updateLater": "Позже", "updateSkip": "Пропустить", "updateDownloading": "Загрузка обновления…", - "updateDownloadFailed": "Не удалось скачать обновление" + "updateDownloadFailed": "Не удалось скачать обновление", + "updateCheck": "Проверить обновление", + "updateChecking": "Проверяем обновления…", + "updateUpToDate": "Установлена актуальная версия", + "updateCheckFailed": "Не удалось проверить обновления. Повторите позже" }