mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-09 01:50:30 +00:00
feat: confirm extension updates with progress and retry
This commit is contained in:
1 parent
4802802828
commit
2e8672273c
26 files changed
+209
-10
No files matched your search
@@ -24,6 +24,7 @@ import '../../application/extensions_controller.dart';
|
||||
import '../../application/pending_extension_install.dart';
|
||||
import '../widgets/extension_detail_view.dart';
|
||||
import '../widgets/extension_icon.dart';
|
||||
import '../widgets/extension_update_dialog.dart';
|
||||
|
||||
const _extensionCardMinWidth = 280.0;
|
||||
const _extensionGridSpacing = 10.0;
|
||||
@@ -1034,10 +1035,8 @@ class _ExtensionCard extends ConsumerWidget {
|
||||
if (canUpdate && installed != null)
|
||||
shad.GhostButton(
|
||||
density: shad.ButtonDensity.icon,
|
||||
onPressed: busy
|
||||
? null
|
||||
: () =>
|
||||
onAction(() => ref.read(extensionsControllerProvider.notifier).upgradeExtension(installed)),
|
||||
key: ValueKey('update-extension-${installed.identity}'),
|
||||
onPressed: busy ? null : () => showExtensionUpdateDialog(context, installed),
|
||||
child: const Icon(Icons.refresh),
|
||||
),
|
||||
if (installed == null && item.store != null)
|
||||
|
||||
@@ -19,6 +19,7 @@ import '../../../../shared/widgets/detail/app_detail_surface.dart';
|
||||
import '../../../../util/util.dart';
|
||||
import '../../application/extensions_controller.dart';
|
||||
import 'extension_icon.dart';
|
||||
import 'extension_update_dialog.dart';
|
||||
|
||||
class ExtensionDetailDrawer extends StatelessWidget {
|
||||
const ExtensionDetailDrawer({super.key, required this.item, required this.onClose});
|
||||
@@ -131,14 +132,10 @@ class ExtensionDetailView extends ConsumerWidget {
|
||||
if (installed != null && canUpdate)
|
||||
AppLoadingButton(
|
||||
key: const ValueKey('extension-details-update'),
|
||||
loading: busy,
|
||||
loading: false,
|
||||
variant: AppLoadingButtonVariant.primary,
|
||||
icon: const Icon(Icons.refresh, size: 16),
|
||||
onPressed: () => _runAction(
|
||||
context,
|
||||
() => ref.read(extensionsControllerProvider.notifier).upgradeExtension(installed),
|
||||
successMessage: context.l10n.extensionUpdateSuccess,
|
||||
),
|
||||
onPressed: busy ? null : () => showExtensionUpdateDialog(context, installed),
|
||||
child: Text(context.l10n.newVersionUpdate),
|
||||
),
|
||||
if (installed != null && !canUpdate)
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../../api/model/extension.dart' as api;
|
||||
import '../../../../l10n/l10n.dart';
|
||||
import '../../../../shared/widgets/app_loading_button.dart';
|
||||
import '../../../../shared/widgets/app_toast.dart';
|
||||
import '../../application/extensions_controller.dart';
|
||||
|
||||
Future<void> showExtensionUpdateDialog(BuildContext context, api.Extension extension) async {
|
||||
final overlay = const DialogOverlayHandler().show<bool>(
|
||||
context: context,
|
||||
alignment: Alignment.center,
|
||||
barrierDismissable: false,
|
||||
builder: (_) => _ExtensionUpdateDialog(extension: extension),
|
||||
);
|
||||
final updated = await overlay.future;
|
||||
if (updated == true && context.mounted) {
|
||||
showAppToast(context, context.l10n.extensionUpdateSuccess);
|
||||
}
|
||||
}
|
||||
|
||||
class _ExtensionUpdateDialog extends ConsumerStatefulWidget {
|
||||
const _ExtensionUpdateDialog({required this.extension});
|
||||
|
||||
final api.Extension extension;
|
||||
|
||||
@override
|
||||
ConsumerState<_ExtensionUpdateDialog> createState() => _ExtensionUpdateDialogState();
|
||||
}
|
||||
|
||||
class _ExtensionUpdateDialogState extends ConsumerState<_ExtensionUpdateDialog> {
|
||||
bool _updating = false;
|
||||
bool _failed = false;
|
||||
|
||||
Future<void> _update() async {
|
||||
if (_updating) return;
|
||||
setState(() {
|
||||
_updating = true;
|
||||
_failed = false;
|
||||
});
|
||||
try {
|
||||
await ref.read(extensionsControllerProvider.notifier).upgradeExtension(widget.extension);
|
||||
} catch (_) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_updating = false;
|
||||
_failed = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (mounted) await closeOverlay(context, true);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopScope(
|
||||
canPop: !_updating,
|
||||
child: AlertDialog(
|
||||
key: const ValueKey('extension-update-dialog'),
|
||||
title: Text(context.l10n.extensionCanUpdate),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(context.l10n.extensionUpdateConfirm(widget.extension.title)),
|
||||
if (_failed) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text(context.l10n.updateFailed, key: const ValueKey('extension-update-error')),
|
||||
],
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
SecondaryButton(
|
||||
key: const ValueKey('cancel-update-extension-button'),
|
||||
onPressed: _updating ? null : () => closeOverlay(context, false),
|
||||
child: Text(context.l10n.cancel),
|
||||
),
|
||||
AppLoadingButton(
|
||||
key: const ValueKey('confirm-update-extension-button'),
|
||||
variant: AppLoadingButtonVariant.primary,
|
||||
loading: _updating,
|
||||
onPressed: _update,
|
||||
child: Text(context.l10n.newVersionUpdate),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -103,6 +103,7 @@
|
||||
"extensions": "الإضافات",
|
||||
"extensionInstallUrl": "عنوان التثبيت",
|
||||
"extensionInstallSuccess": "تم التثبيت بنجاح",
|
||||
"extensionUpdateConfirm": "هل تريد تحديث «{name}» إلى أحدث إصدار؟",
|
||||
"extensionUpdateSuccess": "تم التحديث بنجاح",
|
||||
"extensionDelete": "حذف الإضافة",
|
||||
"extensionAlreadyLatest": "هذا هو أحدث إصدار بالفعل",
|
||||
|
||||
@@ -110,6 +110,7 @@
|
||||
"extensions": "Uzantılar",
|
||||
"extensionInstallUrl": "Quraşdırma URL-i",
|
||||
"extensionInstallSuccess": "Uğurla quraşdırıldı",
|
||||
"extensionUpdateConfirm": "“{name}” ən son versiyaya yenilənsin?",
|
||||
"extensionUpdateSuccess": "Uğurla yeniləndi",
|
||||
"extensionDelete": "Uzantını Sil",
|
||||
"extensionAlreadyLatest": "Artıq son versiyadır",
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
"extensions": "Extensions",
|
||||
"extensionInstallUrl": "URL d'instal·lació",
|
||||
"extensionInstallSuccess": "S'ha instal·lat amb èxit",
|
||||
"extensionUpdateConfirm": "Vols actualitzar «{name}» a la versió més recent?",
|
||||
"extensionUpdateSuccess": "S'ha actualitzat amb èxit",
|
||||
"extensionDelete": "Suprimeix l'extensió",
|
||||
"extensionAlreadyLatest": "Ja està actualitzada",
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
"extensions": "Erweiterungen",
|
||||
"extensionInstallUrl": "Installations-URL",
|
||||
"extensionInstallSuccess": "Erfolgreich installiert",
|
||||
"extensionUpdateConfirm": "„{name}“ auf die neueste Version aktualisieren?",
|
||||
"extensionUpdateSuccess": "Erfolgreich aktualisiert",
|
||||
"extensionDelete": "Erweiterung löschen",
|
||||
"extensionAlreadyLatest": "Es ist bereits die neueste Version",
|
||||
|
||||
@@ -110,6 +110,14 @@
|
||||
"extensions": "Extensions",
|
||||
"extensionInstallUrl": "Install URL",
|
||||
"extensionInstallSuccess": "Installed successfully",
|
||||
"extensionUpdateConfirm": "Update “{name}” to the latest version?",
|
||||
"@extensionUpdateConfirm": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extensionUpdateSuccess": "Updated successfully",
|
||||
"extensionDelete": "Delete Extension",
|
||||
"extensionAlreadyLatest": "It's already the latest version",
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
"extensions": "Extensiones",
|
||||
"extensionInstallUrl": "URL de instalación",
|
||||
"extensionInstallSuccess": "Instalado con éxito",
|
||||
"extensionUpdateConfirm": "¿Actualizar «{name}» a la última versión?",
|
||||
"extensionUpdateSuccess": "Actualizado con éxito",
|
||||
"extensionDelete": "Eliminar Extensión",
|
||||
"extensionAlreadyLatest": "Ya es la versión más reciente",
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
"extensions": "افزونهها",
|
||||
"extensionInstallUrl": "نشانی نصب",
|
||||
"extensionInstallSuccess": "نصب شده با موفقیت",
|
||||
"extensionUpdateConfirm": "«{name}» به آخرین نسخه بهروزرسانی شود؟",
|
||||
"extensionUpdateSuccess": "به روز رسانی موفقیت آمیز",
|
||||
"extensionDelete": "حذف افزونه",
|
||||
"extensionAlreadyLatest": "این نسخه در حال حاضر آخرین نسخه است",
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
"extensions": "Extensions",
|
||||
"extensionInstallUrl": "URL d'installation",
|
||||
"extensionInstallSuccess": "Installé avec succès",
|
||||
"extensionUpdateConfirm": "Mettre à jour « {name} » vers la dernière version ?",
|
||||
"extensionUpdateSuccess": "Mis à jour avec succès",
|
||||
"extensionDelete": "Supprimer l'extension",
|
||||
"extensionAlreadyLatest": "C'est déjà la dernière version",
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
"extensions": "Bővítmények",
|
||||
"extensionInstallUrl": "Telepítési URL",
|
||||
"extensionInstallSuccess": "Sikeresen telepítve",
|
||||
"extensionUpdateConfirm": "Frissíti a(z) „{name}” bővítményt a legújabb verzióra?",
|
||||
"extensionUpdateSuccess": "Sikeresen frissítve",
|
||||
"extensionDelete": "Bővítmény törlése",
|
||||
"extensionAlreadyLatest": "Ez már a legfrissebb verzió",
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
"extensions": "Ekstensi",
|
||||
"extensionInstallUrl": "URL Instal",
|
||||
"extensionInstallSuccess": "Diinstal dengan sukses",
|
||||
"extensionUpdateConfirm": "Perbarui “{name}” ke versi terbaru?",
|
||||
"extensionUpdateSuccess": "Diperbaruhi dengan sukses",
|
||||
"extensionDelete": "Hapus Ekstensi",
|
||||
"extensionAlreadyLatest": "Ini sudah versi terbaru",
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
"extensions": "Estensioni",
|
||||
"extensionInstallUrl": "Installa URL",
|
||||
"extensionInstallSuccess": "Installato con successo",
|
||||
"extensionUpdateConfirm": "Aggiornare «{name}» alla versione più recente?",
|
||||
"extensionUpdateSuccess": "Aggiornato con successo",
|
||||
"extensionDelete": "Elimina estensione",
|
||||
"extensionAlreadyLatest": "È già l'ultima versione",
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
"extensions": "拡張機能",
|
||||
"extensionInstallUrl": "インストールURL",
|
||||
"extensionInstallSuccess": "正常にインストールされました",
|
||||
"extensionUpdateConfirm": "「{name}」を最新バージョンに更新しますか?",
|
||||
"extensionUpdateSuccess": "正常に更新されました",
|
||||
"extensionDelete": "拡張機能を削除",
|
||||
"extensionAlreadyLatest": "既に最新バージョンです",
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
"extensions": "확장 프로그램",
|
||||
"extensionInstallUrl": "설치 URL",
|
||||
"extensionInstallSuccess": "설치되었습니다",
|
||||
"extensionUpdateConfirm": "“{name}”을(를) 최신 버전으로 업데이트할까요?",
|
||||
"extensionUpdateSuccess": "업데이트되었습니다",
|
||||
"extensionDelete": "확장 프로그램 삭제",
|
||||
"extensionAlreadyLatest": "이미 최신 버전입니다",
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
"extensions": "Wtyczki",
|
||||
"extensionInstallUrl": "Instaluj z url",
|
||||
"extensionInstallSuccess": "Instalacja zakończona",
|
||||
"extensionUpdateConfirm": "Zaktualizować „{name}” do najnowszej wersji?",
|
||||
"extensionUpdateSuccess": "Aktualizacja zakończona",
|
||||
"extensionDelete": "Usuń wtyczkę",
|
||||
"extensionAlreadyLatest": "Wtyczka w aktualnej wersji",
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
"extensions": "Extensões",
|
||||
"extensionInstallUrl": "Link de Instalação",
|
||||
"extensionInstallSuccess": "Instalado com Sucesso",
|
||||
"extensionUpdateConfirm": "Atualizar “{name}” para a versão mais recente?",
|
||||
"extensionUpdateSuccess": "Atualizado com Sucesso",
|
||||
"extensionDelete": "Excluir Extensão",
|
||||
"extensionAlreadyLatest": "Já é a última versão",
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
"extensions": "Расширения",
|
||||
"extensionInstallUrl": "URL для установки",
|
||||
"extensionInstallSuccess": "Установка завершена",
|
||||
"extensionUpdateConfirm": "Обновить «{name}» до последней версии?",
|
||||
"extensionUpdateSuccess": "Обновление завершено",
|
||||
"extensionDelete": "Удалить расширение",
|
||||
"extensionAlreadyLatest": "Это последняя версия",
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
"extensions": "நீட்டிப்புகள்",
|
||||
"extensionInstallUrl": "நிறுவப்படும் URL",
|
||||
"extensionInstallSuccess": "வெற்றிகரமாக நிறுவப்பட்டது",
|
||||
"extensionUpdateConfirm": "“{name}” ஐ சமீபத்திய பதிப்பிற்குப் புதுப்பிக்கவா?",
|
||||
"extensionUpdateSuccess": "வெற்றிகரமாக புதுப்பிக்கப்பட்டது",
|
||||
"extensionDelete": "நீட்டிப்பை நீக்கு",
|
||||
"extensionAlreadyLatest": "இது ஏற்கனவே சமீபத்திய பதிப்பாகும்",
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
"extensions": "Eklentiler",
|
||||
"extensionInstallUrl": "Kurulum URL'si",
|
||||
"extensionInstallSuccess": "Başarıyla kuruldu",
|
||||
"extensionUpdateConfirm": "“{name}” en son sürüme güncellensin mi?",
|
||||
"extensionUpdateSuccess": "Başarıyla güncellendi",
|
||||
"extensionDelete": "Eklentiyi Sil",
|
||||
"extensionAlreadyLatest": "Zaten en son sürüm",
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
"extensions": "Розширення",
|
||||
"extensionInstallUrl": "URL для встановлення",
|
||||
"extensionInstallSuccess": "Встановлено успішно",
|
||||
"extensionUpdateConfirm": "Оновити «{name}» до останньої версії?",
|
||||
"extensionUpdateSuccess": "Оновлено успішно",
|
||||
"extensionDelete": "Видалити розширення",
|
||||
"extensionAlreadyLatest": "Вже остання версія",
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
"extensions": "Tiện ích mở rộng",
|
||||
"extensionInstallUrl": "Liên kết cài đặt",
|
||||
"extensionInstallSuccess": "Cài đặt thành công",
|
||||
"extensionUpdateConfirm": "Cập nhật “{name}” lên phiên bản mới nhất?",
|
||||
"extensionUpdateSuccess": "Cập nhật thành công",
|
||||
"extensionDelete": "Xóa tiện ích mở rộng",
|
||||
"extensionAlreadyLatest": "Đây đã là phiên bản mới nhất",
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
"extensions": "扩展",
|
||||
"extensionInstallUrl": "安装链接",
|
||||
"extensionInstallSuccess": "安装成功",
|
||||
"extensionUpdateConfirm": "确定将“{name}”更新到最新版本吗?",
|
||||
"extensionUpdateSuccess": "更新成功",
|
||||
"extensionDelete": "删除扩展",
|
||||
"extensionAlreadyLatest": "已经是最新版本",
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
"extensions": "擴充功能",
|
||||
"extensionInstallUrl": "安裝 URL",
|
||||
"extensionInstallSuccess": "安裝成功",
|
||||
"extensionUpdateConfirm": "確定將「{name}」更新到最新版本嗎?",
|
||||
"extensionUpdateSuccess": "更新成功",
|
||||
"extensionDelete": "刪除擴充功能",
|
||||
"extensionAlreadyLatest": "已是最新版本",
|
||||
|
||||
@@ -648,6 +648,73 @@ void main() {
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
for (final (width, details) in [(390.0, false), (1100.0, false), (1100.0, true)]) {
|
||||
testWidgets('extension update confirms, shows progress and retries at $width details=$details', (tester) async {
|
||||
await _setTestSize(tester, Size(width, 900));
|
||||
final controller = UpdateableExtensionsController();
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [extensionsControllerProvider.overrideWith(() => controller)],
|
||||
child: shad.ShadcnApp(
|
||||
theme: AppTheme.light(),
|
||||
materialTheme: AppTheme.materialLight(),
|
||||
home: const ExtensionsPage(),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
if (details) {
|
||||
await tester.tap(find.byKey(const ValueKey('extension-card-extension-0')));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
final entry = find.byKey(ValueKey(details ? 'extension-details-update' : 'update-extension-extension-0'));
|
||||
final dialog = find.byKey(const ValueKey('extension-update-dialog'));
|
||||
final confirm = find.byKey(const ValueKey('confirm-update-extension-button'));
|
||||
final cancel = find.byKey(const ValueKey('cancel-update-extension-button'));
|
||||
await tester.tap(entry);
|
||||
await tester.pumpAndSettle();
|
||||
expect(dialog, findsOneWidget);
|
||||
expect(find.text('Update “Extension 0” to the latest version?'), findsOneWidget);
|
||||
expect(controller.updateCalls, 0);
|
||||
await tester.tap(cancel);
|
||||
await tester.pumpAndSettle();
|
||||
expect(dialog, findsNothing);
|
||||
expect(controller.updateCalls, 0);
|
||||
|
||||
await tester.tap(entry);
|
||||
await tester.pumpAndSettle();
|
||||
controller.failUpdate = true;
|
||||
await tester.tap(confirm);
|
||||
await tester.pump();
|
||||
expect(controller.updateCalls, 1);
|
||||
expect(tester.widget<AppLoadingButton>(confirm).loading, isTrue);
|
||||
expect(tester.widget<shad.SecondaryButton>(cancel).onPressed, isNull);
|
||||
await tester.tap(confirm);
|
||||
await tester.tapAt(const Offset(5, 5));
|
||||
await tester.binding.handlePopRoute();
|
||||
await tester.pump();
|
||||
expect(controller.updateCalls, 1);
|
||||
expect(dialog, findsOneWidget);
|
||||
controller.updateGate.complete();
|
||||
await tester.pumpAndSettle();
|
||||
expect(dialog, findsOneWidget);
|
||||
expect(find.byKey(const ValueKey('extension-update-error')), findsOneWidget);
|
||||
expect(tester.widget<AppLoadingButton>(confirm).loading, isFalse);
|
||||
|
||||
controller.failUpdate = false;
|
||||
controller.updateGate = Completer<void>();
|
||||
await tester.tap(confirm);
|
||||
await tester.pump();
|
||||
expect(controller.updateCalls, 2);
|
||||
controller.updateGate.complete();
|
||||
await tester.pumpAndSettle();
|
||||
expect(dialog, findsNothing);
|
||||
await tester.pump(const Duration(seconds: 6));
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
|
||||
testWidgets('installed extension details use a disabled installed button without a hero status', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
@@ -5304,6 +5371,23 @@ class PaginatedExtensionsController extends FakeExtensionsController {
|
||||
}
|
||||
}
|
||||
|
||||
class UpdateableExtensionsController extends FakeExtensionsController {
|
||||
int updateCalls = 0;
|
||||
bool failUpdate = false;
|
||||
Completer<void> updateGate = Completer<void>();
|
||||
|
||||
@override
|
||||
Future<ExtensionsState> build() async =>
|
||||
(await super.build()).copyWith(listFilter: ExtensionListFilter.installed, updateFlags: {'extension-0': '2.0.0'});
|
||||
|
||||
@override
|
||||
Future<void> upgradeExtension(api_extension.Extension extension) async {
|
||||
updateCalls++;
|
||||
await updateGate.future;
|
||||
if (failUpdate) throw StateError('update failed');
|
||||
}
|
||||
}
|
||||
|
||||
class BusyInstallExtensionsController extends FakeExtensionsController {
|
||||
@override
|
||||
Future<ExtensionsState> build() async {
|
||||
|
||||
Reference in new issue
Block a user