diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 7277238..e93a31e 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -2,6 +2,7 @@
xmlns:tools="http://schemas.android.com/tools">
+
diff --git a/lib/core/utils/update_checker.dart b/lib/core/utils/update_checker.dart
new file mode 100644
index 0000000..cdf3dd4
--- /dev/null
+++ b/lib/core/utils/update_checker.dart
@@ -0,0 +1,208 @@
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:package_info_plus/package_info_plus.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+class AppUpdateInfo {
+ final String version;
+ final int? build;
+ final String tag;
+ final String url;
+ final String notes;
+ final Map assets;
+
+ const AppUpdateInfo({
+ required this.version,
+ required this.build,
+ required this.tag,
+ required this.url,
+ required this.notes,
+ required this.assets,
+ });
+}
+
+abstract class UpdateChecker {
+ static const String _owner = 'KometTeam';
+ static const String _repo = 'Komet';
+ static const String _userAgent = 'KometUpdateChecker';
+
+ static const String _lastCheckKey = 'update_last_check_ms';
+ static const String _skippedTagKey = 'update_skipped_tag';
+ static const Duration _checkInterval = Duration(hours: 6);
+ static const Duration _timeout = Duration(seconds: 15);
+
+ static Future fetchLatest() async {
+ final info = await PackageInfo.fromPlatform();
+ final currentBase = info.version;
+ final currentBuild = _normalizeBuild(int.tryParse(info.buildNumber));
+
+ final release = await _fetchLatestRelease();
+ if (release == null) return null;
+
+ final tag = (release['tag_name'] as String?)?.trim();
+ if (tag == null || tag.isEmpty) return null;
+
+ final remoteBase = _baseVersion(tag);
+ final remoteBuild = _buildNumber(tag);
+
+ if (!_isNewer(
+ currentBase: currentBase,
+ currentBuild: currentBuild,
+ remoteBase: remoteBase,
+ remoteBuild: remoteBuild,
+ )) {
+ return null;
+ }
+
+ return AppUpdateInfo(
+ version: remoteBase,
+ build: remoteBuild,
+ tag: tag,
+ url: (release['html_url'] as String?) ?? _releasesPage,
+ notes: (release['body'] as String?)?.trim() ?? '',
+ assets: _parseAssets(release['assets']),
+ );
+ }
+
+ static Future check({bool force = false}) async {
+ final prefs = await SharedPreferences.getInstance();
+
+ if (!force) {
+ final last = prefs.getInt(_lastCheckKey);
+ if (last != null) {
+ final elapsed = DateTime.now().millisecondsSinceEpoch - last;
+ if (elapsed >= 0 && elapsed < _checkInterval.inMilliseconds) {
+ return null;
+ }
+ }
+ }
+
+ AppUpdateInfo? update;
+ try {
+ update = await fetchLatest();
+ } catch (_) {
+ return null;
+ }
+
+ await prefs.setInt(_lastCheckKey, DateTime.now().millisecondsSinceEpoch);
+
+ if (update == null) return null;
+
+ if (!force && prefs.getString(_skippedTagKey) == update.tag) {
+ return null;
+ }
+
+ return update;
+ }
+
+ static Future skip(String tag) async {
+ final prefs = await SharedPreferences.getInstance();
+ await prefs.setString(_skippedTagKey, tag);
+ }
+
+ static String get _releasesPage =>
+ 'https://github.com/$_owner/$_repo/releases';
+
+ static Future