feat(settings): add manual update check

This commit is contained in:
klockky
2026-07-14 19:55:02 +03:00
parent 511351c015
commit 93efd524cd
7 changed files with 136 additions and 6 deletions
+38 -4
View File
@@ -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<UpdateCheckResult> 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<void> 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<void>();
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<String, dynamic>();
}
return null;
} catch (_) {
return null;
} finally {
client.close(force: true);
}
@@ -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<SettingsTab> {
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<SettingsTab> {
});
}
Future<void> _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<void> _openCloudStorage(BuildContext context) async {
final cs = Theme.of(context).colorScheme;
final ok = await showInfoActionSheet(
@@ -221,6 +251,7 @@ class _SettingsTabState extends State<SettingsTab> {
@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<SettingsTab> {
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',
+5 -1
View File
@@ -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"
}
+24
View File
@@ -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
+13
View File
@@ -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';
}
+13
View File
@@ -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 =>
'Не удалось проверить обновления. Повторите позже';
}
+5 -1
View File
@@ -685,5 +685,9 @@
"updateLater": "Позже",
"updateSkip": "Пропустить",
"updateDownloading": "Загрузка обновления…",
"updateDownloadFailed": "Не удалось скачать обновление"
"updateDownloadFailed": "Не удалось скачать обновление",
"updateCheck": "Проверить обновление",
"updateChecking": "Проверяем обновления…",
"updateUpToDate": "Установлена актуальная версия",
"updateCheckFailed": "Не удалось проверить обновления. Повторите позже"
}