From 172f4a3650cc810514fe84ad63bba906bff114e1 Mon Sep 17 00:00:00 2001 From: Levi Date: Sat, 19 Sep 2026 17:32:44 +0800 Subject: [PATCH] fix(ui): unify extension update check and improve update affordance - Update availability is now decided solely by the backend live GitHub check (updateFlags); the store index may lag and previously masked new versions in the market view. - Replace the ambiguous refresh icon on extension cards with a labeled update button, and add a red dot badge on extension icons (card and detail view) when an update is available. - Surface a toast when the update check fails instead of failing silently. --- .../application/extensions_controller.dart | 47 +++++++--------- .../presentation/pages/extensions_page.dart | 54 +++++++++++-------- .../widgets/extension_detail_view.dart | 34 +++++++++--- ui/flutter/lib/l10n/app_en.arb | 2 + ui/flutter/lib/l10n/app_zh.arb | 2 + 5 files changed, 81 insertions(+), 58 deletions(-) diff --git a/ui/flutter/lib/features/extensions/application/extensions_controller.dart b/ui/flutter/lib/features/extensions/application/extensions_controller.dart index 74e439d3..b63b27f4 100644 --- a/ui/flutter/lib/features/extensions/application/extensions_controller.dart +++ b/ui/flutter/lib/features/extensions/application/extensions_controller.dart @@ -49,6 +49,7 @@ class ExtensionsState { this.loadingMoreStore = false, this.busyExtensionIds = const {}, this.devMode = false, + this.updateCheckFailed = false, }); final List installedExtensions; @@ -63,6 +64,7 @@ class ExtensionsState { final bool loadingMoreStore; final Set busyExtensionIds; final bool devMode; + final bool updateCheckFailed; UnmodifiableMapView get installedMap => UnmodifiableMapView({for (final ext in installedExtensions) ext.identity: ext}); @@ -100,6 +102,7 @@ class ExtensionsState { bool? loadingMoreStore, Set? busyExtensionIds, bool? devMode, + bool? updateCheckFailed, }) { return ExtensionsState( installedExtensions: installedExtensions ?? this.installedExtensions, @@ -114,6 +117,7 @@ class ExtensionsState { loadingMoreStore: loadingMoreStore ?? this.loadingMoreStore, busyExtensionIds: busyExtensionIds ?? this.busyExtensionIds, devMode: devMode ?? this.devMode, + updateCheckFailed: updateCheckFailed ?? this.updateCheckFailed, ); } } @@ -185,12 +189,11 @@ class ExtensionsController extends AsyncNotifier { } bool canUpdateItem(ExtensionListItem item) { - final current = _current; - if (item.installed == null) return false; - if (current.listFilter == ExtensionListFilter.market && item.store != null) { - return _compareVersion(item.store!.version, item.installed!.version) > 0; - } - return current.updateFlags.containsKey(item.installed!.identity); + final installed = item.installed; + if (installed == null) return false; + // The backend checks GitHub live for every installed extension; the store + // index may lag behind and must not decide update visibility. + return _current.updateFlags.containsKey(installed.identity); } Future installFromStore(StoreExtension extension) async { @@ -257,15 +260,23 @@ class ExtensionsController extends AsyncNotifier { Future checkUpdate() async { final flags = {}; + var failed = false; for (final ext in _current.installedExtensions) { try { final resp = await ref.read(gopeedServiceProvider).upgradeCheckExtension(ext.identity); if (resp.newVersion.isNotEmpty) { flags[ext.identity] = resp.newVersion; } - } catch (_) {} + } catch (_) { + failed = true; + } } - state = AsyncValue.data(_current.copyWith(updateFlags: flags)); + state = AsyncValue.data( + _current.copyWith( + updateFlags: flags, + updateCheckFailed: failed && _current.installedExtensions.isNotEmpty, + ), + ); } void tryOpenDevMode() { @@ -354,24 +365,4 @@ class ExtensionsController extends AsyncNotifier { }()); } - static int _compareVersion(String a, String b) { - final aNums = _toVersionNumbers(a); - final bNums = _toVersionNumbers(b); - final maxLen = aNums.length > bNums.length ? aNums.length : bNums.length; - for (var i = 0; i < maxLen; i++) { - final left = i < aNums.length ? aNums[i] : 0; - final right = i < bNums.length ? bNums[i] : 0; - if (left > right) return 1; - if (left < right) return -1; - } - return 0; - } - - static List _toVersionNumbers(String version) { - return version - .split(RegExp(r'[^0-9]+')) - .where((part) => part.isNotEmpty) - .map((part) => int.tryParse(part) ?? 0) - .toList(); - } } diff --git a/ui/flutter/lib/features/extensions/presentation/pages/extensions_page.dart b/ui/flutter/lib/features/extensions/presentation/pages/extensions_page.dart index f2bd73b9..7d4db1ee 100644 --- a/ui/flutter/lib/features/extensions/presentation/pages/extensions_page.dart +++ b/ui/flutter/lib/features/extensions/presentation/pages/extensions_page.dart @@ -106,6 +106,12 @@ class _ExtensionsPageState extends ConsumerState { }); } final stateAsync = ref.watch(extensionsControllerProvider); + ref.listen(extensionsControllerProvider, (previous, next) { + final failed = next.value?.updateCheckFailed ?? false; + if (failed && previous?.value?.updateCheckFailed != true) { + showAppToast(context, context.l10n.extensionUpdateCheckFailed); + } + }); final body = Stack( children: [ ColoredBox( @@ -485,25 +491,7 @@ class _Content extends StatelessWidget { } bool _canUpdate(ExtensionsState state, ExtensionListItem item) { - if (item.installed == null) return false; - if (state.listFilter == ExtensionListFilter.market && item.store != null) { - return _compareVersion(item.store!.version, item.installed!.version) > 0; - } - return state.updateFlags.containsKey(item.installed!.identity); - } - - int _compareVersion(String a, String b) { - final aNums = a.split(RegExp(r'[^0-9]+')).where((part) => part.isNotEmpty).map((part) => int.tryParse(part) ?? 0); - final bNums = b.split(RegExp(r'[^0-9]+')).where((part) => part.isNotEmpty).map((part) => int.tryParse(part) ?? 0); - final left = aNums.toList(); - final right = bNums.toList(); - final maxLen = left.length > right.length ? left.length : right.length; - for (var i = 0; i < maxLen; i++) { - final l = i < left.length ? left[i] : 0; - final r = i < right.length ? right[i] : 0; - if (l != r) return l.compareTo(r); - } - return 0; + return item.installed != null && state.updateFlags.containsKey(item.installed!.identity); } } @@ -988,7 +976,27 @@ class _ExtensionCard extends ConsumerWidget { children: [ Row( children: [ - ExtensionIcon(item: item, size: 40, borderRadius: BorderRadius.circular(6), fallbackFit: BoxFit.cover), + Stack( + clipBehavior: Clip.none, + children: [ + ExtensionIcon(item: item, size: 40, borderRadius: BorderRadius.circular(6), fallbackFit: BoxFit.cover), + if (canUpdate) + Positioned( + top: -3, + right: -3, + child: Container( + key: ValueKey('extension-card-update-dot-${item.id}'), + width: 11, + height: 11, + decoration: BoxDecoration( + color: palette.error, + shape: BoxShape.circle, + border: Border.all(color: palette.cardBg, width: 2), + ), + ), + ), + ], + ), const SizedBox(width: 10), Expanded( child: Column( @@ -1068,11 +1076,11 @@ class _ExtensionCard extends ConsumerWidget { mainAxisSize: MainAxisSize.min, children: [ if (canUpdate && installed != null) - shad.GhostButton( - density: shad.ButtonDensity.icon, + shad.PrimaryButton( key: ValueKey('update-extension-${installed.identity}'), + density: shad.ButtonDensity.dense, onPressed: busy ? null : () => showExtensionUpdateDialog(context, installed), - child: const Icon(Icons.refresh), + child: Text(context.l10n.extensionUpdateAction), ), if (installed == null && item.store != null) shad.GhostButton( diff --git a/ui/flutter/lib/features/extensions/presentation/widgets/extension_detail_view.dart b/ui/flutter/lib/features/extensions/presentation/widgets/extension_detail_view.dart index 87837560..26204eef 100644 --- a/ui/flutter/lib/features/extensions/presentation/widgets/extension_detail_view.dart +++ b/ui/flutter/lib/features/extensions/presentation/widgets/extension_detail_view.dart @@ -65,12 +65,32 @@ class ExtensionDetailView extends ConsumerWidget { key: const ValueKey('extension-details-hero'), crossAxisAlignment: CrossAxisAlignment.start, children: [ - ExtensionIcon( - item: current, - size: mobile ? 60 : 68, - borderRadius: BorderRadius.circular(14), - fallbackPadding: EdgeInsets.all((mobile ? 60 : 68) * 0.16), - fallbackBackgroundColor: palette.surfaceSoft, + Stack( + clipBehavior: Clip.none, + children: [ + ExtensionIcon( + item: current, + size: mobile ? 60 : 68, + borderRadius: BorderRadius.circular(14), + fallbackPadding: EdgeInsets.all((mobile ? 60 : 68) * 0.16), + fallbackBackgroundColor: palette.surfaceSoft, + ), + if (canUpdate) + Positioned( + top: -2, + right: -2, + child: Container( + key: const ValueKey('extension-details-update-dot'), + width: 13, + height: 13, + decoration: BoxDecoration( + color: palette.error, + shape: BoxShape.circle, + border: Border.all(color: palette.bg, width: 2.5), + ), + ), + ), + ], ), SizedBox(width: mobile ? 16 : 20), Expanded( @@ -135,7 +155,7 @@ class ExtensionDetailView extends ConsumerWidget { key: const ValueKey('extension-details-update'), loading: false, variant: AppLoadingButtonVariant.primary, - icon: const Icon(Icons.refresh, size: 16), + icon: const Icon(Icons.upgrade, size: 16), onPressed: busy ? null : () => showExtensionUpdateDialog(context, installed), child: Text(context.l10n.newVersionUpdate), ), diff --git a/ui/flutter/lib/l10n/app_en.arb b/ui/flutter/lib/l10n/app_en.arb index a241bbfd..97fb6f36 100644 --- a/ui/flutter/lib/l10n/app_en.arb +++ b/ui/flutter/lib/l10n/app_en.arb @@ -120,6 +120,8 @@ } }, "extensionUpdateSuccess": "Updated successfully", + "extensionUpdateAction": "Update", + "extensionUpdateCheckFailed": "Failed to check for updates. Please check your network", "extensionDelete": "Delete Extension", "extensionAlreadyLatest": "It's already the latest version", "extensionFind": "Find Extensions", diff --git a/ui/flutter/lib/l10n/app_zh.arb b/ui/flutter/lib/l10n/app_zh.arb index 0269904d..2cbd5aa7 100644 --- a/ui/flutter/lib/l10n/app_zh.arb +++ b/ui/flutter/lib/l10n/app_zh.arb @@ -106,6 +106,8 @@ "extensionInstallSuccess": "安装成功", "extensionUpdateConfirm": "确定将“{name}”更新到最新版本吗?", "extensionUpdateSuccess": "更新成功", + "extensionUpdateAction": "更新", + "extensionUpdateCheckFailed": "检查更新失败,请检查网络后重试", "extensionDelete": "删除扩展", "extensionAlreadyLatest": "已经是最新版本", "extensionFind": "获取扩展",