mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-08 01:20:27 +00:00
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.
This commit is contained in:
1 parent
694b43e3f9
commit
172f4a3650
5 files changed
+81
-58
No files matched your search
@@ -49,6 +49,7 @@ class ExtensionsState {
|
||||
this.loadingMoreStore = false,
|
||||
this.busyExtensionIds = const {},
|
||||
this.devMode = false,
|
||||
this.updateCheckFailed = false,
|
||||
});
|
||||
|
||||
final List<Extension> installedExtensions;
|
||||
@@ -63,6 +64,7 @@ class ExtensionsState {
|
||||
final bool loadingMoreStore;
|
||||
final Set<String> busyExtensionIds;
|
||||
final bool devMode;
|
||||
final bool updateCheckFailed;
|
||||
|
||||
UnmodifiableMapView<String, Extension> get installedMap =>
|
||||
UnmodifiableMapView({for (final ext in installedExtensions) ext.identity: ext});
|
||||
@@ -100,6 +102,7 @@ class ExtensionsState {
|
||||
bool? loadingMoreStore,
|
||||
Set<String>? 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<ExtensionsState> {
|
||||
}
|
||||
|
||||
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<void> installFromStore(StoreExtension extension) async {
|
||||
@@ -257,15 +260,23 @@ class ExtensionsController extends AsyncNotifier<ExtensionsState> {
|
||||
|
||||
Future<void> checkUpdate() async {
|
||||
final flags = <String, String>{};
|
||||
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<ExtensionsState> {
|
||||
}());
|
||||
}
|
||||
|
||||
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<int> _toVersionNumbers(String version) {
|
||||
return version
|
||||
.split(RegExp(r'[^0-9]+'))
|
||||
.where((part) => part.isNotEmpty)
|
||||
.map((part) => int.tryParse(part) ?? 0)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -106,6 +106,12 @@ class _ExtensionsPageState extends ConsumerState<ExtensionsPage> {
|
||||
});
|
||||
}
|
||||
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(
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -106,6 +106,8 @@
|
||||
"extensionInstallSuccess": "安装成功",
|
||||
"extensionUpdateConfirm": "确定将“{name}”更新到最新版本吗?",
|
||||
"extensionUpdateSuccess": "更新成功",
|
||||
"extensionUpdateAction": "更新",
|
||||
"extensionUpdateCheckFailed": "检查更新失败,请检查网络后重试",
|
||||
"extensionDelete": "删除扩展",
|
||||
"extensionAlreadyLatest": "已经是最新版本",
|
||||
"extensionFind": "获取扩展",
|
||||
|
||||
Reference in new issue
Block a user