feat(settings): add manual update check
This commit is contained in:
@@ -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 {
|
abstract class UpdateChecker {
|
||||||
static const String _owner = 'KometTeam';
|
static const String _owner = 'KometTeam';
|
||||||
static const String _repo = 'Komet';
|
static const String _repo = 'Komet';
|
||||||
@@ -96,6 +112,21 @@ abstract class UpdateChecker {
|
|||||||
return update;
|
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 {
|
static Future<void> skip(String tag) async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setString(_skippedTagKey, tag);
|
await prefs.setString(_skippedTagKey, tag);
|
||||||
@@ -117,22 +148,25 @@ abstract class UpdateChecker {
|
|||||||
final resp = await req.close().timeout(_timeout);
|
final resp = await req.close().timeout(_timeout);
|
||||||
if (resp.statusCode != HttpStatus.ok) {
|
if (resp.statusCode != HttpStatus.ok) {
|
||||||
await resp.drain<void>();
|
await resp.drain<void>();
|
||||||
return null;
|
throw HttpException(
|
||||||
|
'GitHub returned HTTP ${resp.statusCode}',
|
||||||
|
uri: uri,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
final body = await resp
|
final body = await resp
|
||||||
.transform(const Utf8Decoder())
|
.transform(const Utf8Decoder())
|
||||||
.join()
|
.join()
|
||||||
.timeout(_timeout);
|
.timeout(_timeout);
|
||||||
final decoded = jsonDecode(body);
|
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) {
|
for (final entry in decoded) {
|
||||||
if (entry is! Map) continue;
|
if (entry is! Map) continue;
|
||||||
if (entry['draft'] == true) continue;
|
if (entry['draft'] == true) continue;
|
||||||
return entry.cast<String, dynamic>();
|
return entry.cast<String, dynamic>();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
} catch (_) {
|
|
||||||
return null;
|
|
||||||
} finally {
|
} finally {
|
||||||
client.close(force: true);
|
client.close(force: true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import '../../../core/config/komet_settings.dart';
|
|||||||
import '../../../core/config/app_show_extra_info.dart';
|
import '../../../core/config/app_show_extra_info.dart';
|
||||||
import '../../../core/storage/app_database.dart';
|
import '../../../core/storage/app_database.dart';
|
||||||
import '../../../core/utils/format.dart';
|
import '../../../core/utils/format.dart';
|
||||||
|
import '../../../core/utils/update_checker.dart';
|
||||||
import '../../../l10n/app_localizations.dart';
|
import '../../../l10n/app_localizations.dart';
|
||||||
import '../../../main.dart';
|
import '../../../main.dart';
|
||||||
import '../../widgets/avatar_history_screen.dart';
|
import '../../widgets/avatar_history_screen.dart';
|
||||||
@@ -19,6 +20,7 @@ import '../../widgets/settings_card.dart';
|
|||||||
import '../../widgets/sheet_helpers.dart';
|
import '../../widgets/sheet_helpers.dart';
|
||||||
import '../../widgets/small_spinner.dart';
|
import '../../widgets/small_spinner.dart';
|
||||||
import '../../widgets/custom_notification.dart';
|
import '../../widgets/custom_notification.dart';
|
||||||
|
import '../../widgets/update_dialog.dart';
|
||||||
import '../auth/login_screen.dart';
|
import '../auth/login_screen.dart';
|
||||||
import '../auth/proxy_settings_sheet.dart';
|
import '../auth/proxy_settings_sheet.dart';
|
||||||
import '../../../core/config/app_digital_id_mode.dart';
|
import '../../../core/config/app_digital_id_mode.dart';
|
||||||
@@ -49,6 +51,7 @@ class _SettingsTabState extends State<SettingsTab> {
|
|||||||
bool _isPhoneVisible = false;
|
bool _isPhoneVisible = false;
|
||||||
String? _appVersionLabel;
|
String? _appVersionLabel;
|
||||||
bool _debugMenuVisible = false;
|
bool _debugMenuVisible = false;
|
||||||
|
bool _isCheckingForUpdates = false;
|
||||||
int _versionSecretTapCount = 0;
|
int _versionSecretTapCount = 0;
|
||||||
Timer? _versionSecretTapResetTimer;
|
Timer? _versionSecretTapResetTimer;
|
||||||
StreamSubscription? _profileUpdateSub;
|
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 {
|
Future<void> _openCloudStorage(BuildContext context) async {
|
||||||
final cs = Theme.of(context).colorScheme;
|
final cs = Theme.of(context).colorScheme;
|
||||||
final ok = await showInfoActionSheet(
|
final ok = await showInfoActionSheet(
|
||||||
@@ -221,6 +251,7 @@ class _SettingsTabState extends State<SettingsTab> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final cs = Theme.of(context).colorScheme;
|
final cs = Theme.of(context).colorScheme;
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
if (_profile == null) {
|
if (_profile == null) {
|
||||||
return const Center(child: SmallSpinner(size: 36));
|
return const Center(child: SmallSpinner(size: 36));
|
||||||
@@ -444,6 +475,13 @@ class _SettingsTabState extends State<SettingsTab> {
|
|||||||
child: _buildSection(
|
child: _buildSection(
|
||||||
context,
|
context,
|
||||||
items: [
|
items: [
|
||||||
|
_SettingsItem(
|
||||||
|
icon: Symbols.system_update,
|
||||||
|
label: _isCheckingForUpdates
|
||||||
|
? l10n.updateChecking
|
||||||
|
: l10n.updateCheck,
|
||||||
|
onTap: _isCheckingForUpdates ? null : _checkForUpdates,
|
||||||
|
),
|
||||||
_SettingsItem(
|
_SettingsItem(
|
||||||
leading: Image.asset(
|
leading: Image.asset(
|
||||||
'assets/komet.png',
|
'assets/komet.png',
|
||||||
|
|||||||
+5
-1
@@ -903,5 +903,9 @@
|
|||||||
"updateLater": "Later",
|
"updateLater": "Later",
|
||||||
"updateSkip": "Skip",
|
"updateSkip": "Skip",
|
||||||
"updateDownloading": "Downloading update…",
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4021,6 +4021,30 @@ abstract class AppLocalizations {
|
|||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Failed to download the update'**
|
/// **'Failed to download the update'**
|
||||||
String get updateDownloadFailed;
|
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
|
class _AppLocalizationsDelegate
|
||||||
|
|||||||
@@ -2084,4 +2084,17 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get updateDownloadFailed => 'Failed to download the update';
|
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';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2095,4 +2095,17 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get updateDownloadFailed => 'Не удалось скачать обновление';
|
String get updateDownloadFailed => 'Не удалось скачать обновление';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get updateCheck => 'Проверить обновление';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get updateChecking => 'Проверяем обновления…';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get updateUpToDate => 'Установлена актуальная версия';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get updateCheckFailed =>
|
||||||
|
'Не удалось проверить обновления. Повторите позже';
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -685,5 +685,9 @@
|
|||||||
"updateLater": "Позже",
|
"updateLater": "Позже",
|
||||||
"updateSkip": "Пропустить",
|
"updateSkip": "Пропустить",
|
||||||
"updateDownloading": "Загрузка обновления…",
|
"updateDownloading": "Загрузка обновления…",
|
||||||
"updateDownloadFailed": "Не удалось скачать обновление"
|
"updateDownloadFailed": "Не удалось скачать обновление",
|
||||||
|
"updateCheck": "Проверить обновление",
|
||||||
|
"updateChecking": "Проверяем обновления…",
|
||||||
|
"updateUpToDate": "Установлена актуальная версия",
|
||||||
|
"updateCheckFailed": "Не удалось проверить обновления. Повторите позже"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user