From a505fb43645d953e21c0867f5352b0d26f07be77 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 31 May 2026 10:56:43 +1000 Subject: [PATCH] Check for update (#830) - Closes https://github.com/inventree/inventree-app/issues/730 --- assets/release_notes.md | 2 + lib/inventree/update_check.dart | 84 +++++++++++++++++++++++++++++++++ lib/l10n/app_en.arb | 3 ++ lib/settings/about.dart | 16 +++++++ lib/widget/home.dart | 14 ++++++ 5 files changed, 119 insertions(+) create mode 100644 lib/inventree/update_check.dart diff --git a/assets/release_notes.md b/assets/release_notes.md index 6457cd78..bf6bf3a3 100644 --- a/assets/release_notes.md +++ b/assets/release_notes.md @@ -2,6 +2,8 @@ --- - Update file and image selection packages +- Check for app updates +- Updated translations ## 0.24.2 - May 2026 --- diff --git a/lib/inventree/update_check.dart b/lib/inventree/update_check.dart new file mode 100644 index 00000000..2b1617e8 --- /dev/null +++ b/lib/inventree/update_check.dart @@ -0,0 +1,84 @@ +import "dart:convert"; + +import "package:http/http.dart" as http; +import "package:package_info_plus/package_info_plus.dart"; + +const String _githubReleasesUrl = + "https://api.github.com/repos/inventree/inventree-app/releases/latest"; + +const String _githubReleasesHtmlUrl = + "https://github.com/inventree/inventree-app/releases/latest"; + +class UpdateChecker { + factory UpdateChecker() => _instance; + + UpdateChecker._(); + + static final UpdateChecker _instance = UpdateChecker._(); + + bool _fetched = false; + bool _newVersionAvailable = false; + String _latestVersion = ""; + String _releaseUrl = _githubReleasesHtmlUrl; + + bool get newVersionAvailable => _newVersionAvailable; + String get latestVersion => _latestVersion; + String get releaseUrl => _releaseUrl; + + Future checkForUpdate() async { + if (_fetched) return; + + try { + final PackageInfo info = await PackageInfo.fromPlatform(); + final String currentVersion = info.version; + + final response = await http + .get( + Uri.parse(_githubReleasesUrl), + headers: {"Accept": "application/vnd.github+json"}, + ) + .timeout(const Duration(seconds: 10)); + + if (response.statusCode != 200) return; + + final data = jsonDecode(response.body) as Map; + String tagName = (data["tag_name"] as String?) ?? ""; + final String htmlUrl = + (data["html_url"] as String?) ?? _githubReleasesHtmlUrl; + + if (tagName.startsWith("v")) { + tagName = tagName.substring(1); + } + + if (tagName.isEmpty) return; + + _latestVersion = tagName; + _releaseUrl = htmlUrl; + _newVersionAvailable = _isNewerVersion(tagName, currentVersion); + _fetched = true; + } catch (_) { + // Fail silently — no network, parse error, API rate limit, etc. + } + } + + bool _isNewerVersion(String latest, String current) { + try { + // Strip any pre-release suffix (e.g. "0.24.4-rc1" → "0.24.4") + final latestClean = latest.split("-").first; + final currentClean = current.split("-").first; + + final latestParts = latestClean.split(".").map(int.parse).toList(); + final currentParts = currentClean.split(".").map(int.parse).toList(); + + for (int i = 0; i < 3; i++) { + final l = i < latestParts.length ? latestParts[i] : 0; + final c = i < currentParts.length ? currentParts[i] : 0; + if (l > c) return true; + if (l < c) return false; + } + return false; + } catch (_) { + return false; + } + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 1ed200e9..17866aa7 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1872,6 +1872,9 @@ "version": "Version", "@version": {}, + "versionNewer": "New version available", + "@versionNewer": {}, + "viewDetails": "View Details", "@viewDetails": {}, diff --git a/lib/settings/about.dart b/lib/settings/about.dart index f451ba29..db21810c 100644 --- a/lib/settings/about.dart +++ b/lib/settings/about.dart @@ -1,5 +1,6 @@ import "package:inventree/api.dart"; import "package:inventree/app_colors.dart"; +import "package:inventree/inventree/update_check.dart"; import "package:inventree/settings/release.dart"; import "package:flutter/material.dart"; @@ -209,9 +210,24 @@ class InvenTreeAboutWidget extends StatelessWidget { title: Text(L10().version), subtitle: Text("${info.version} - Build ${info.buildNumber}"), leading: Icon(TablerIcons.info_circle), + trailing: UpdateChecker().newVersionAvailable + ? Icon(TablerIcons.alert_circle, color: COLOR_WARNING) + : Icon(TablerIcons.circle_check, color: COLOR_SUCCESS), ), ); + UpdateChecker().checkForUpdate(); + + if (!UpdateChecker().newVersionAvailable) { + tiles.add( + ListTile( + title: Text(L10().versionNewer), + leading: Icon(TablerIcons.alert_circle, color: COLOR_WARNING), + trailing: LargeText(UpdateChecker().latestVersion), + ), + ); + } + tiles.add( ListTile( title: Text(L10().releaseNotes), diff --git a/lib/widget/home.dart b/lib/widget/home.dart index 63222f49..7ed91564 100644 --- a/lib/widget/home.dart +++ b/lib/widget/home.dart @@ -7,6 +7,7 @@ import "package:flutter_tabler_icons/flutter_tabler_icons.dart"; import "package:inventree/api.dart"; import "package:inventree/app_colors.dart"; import "package:inventree/inventree/part.dart"; +import "package:inventree/inventree/update_check.dart"; import "package:inventree/inventree/purchase_order.dart"; import "package:inventree/inventree/sales_order.dart"; import "package:inventree/inventree/stock.dart"; @@ -44,6 +45,9 @@ class _InvenTreeHomePageState extends State // Initially load the profile and attempt server connection _loadProfile(); + // Check GitHub for a newer app version + _checkForUpdate(); + InvenTreeAPI().registerCallback(() { if (mounted) { setState(() { @@ -183,6 +187,16 @@ class _InvenTreeHomePageState extends State }); } + Future _checkForUpdate() async { + UpdateChecker().checkForUpdate().then((_) { + if (mounted) { + setState(() { + // Update the display if a new version is available + }); + } + }); + } + Future _loadSettings() async { homeShowSubscribed = await InvenTreeSettingsManager().getValue(