mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-06 08:30:29 +00:00
Merge latest PR #1510 into 2.0.0-beta.2
# Conflicts: # ui/flutter/lib/features/extensions/presentation/pages/extensions_page.dart # ui/flutter/test/widget_test.dart
This commit is contained in:
commit
67d4aa9554
32 files changed
+648
-123
No files matched your search
@@ -19,7 +19,7 @@ Future<bool> open(String filePath) async {
|
||||
Future<bool> reveal(String filePath) async {
|
||||
try {
|
||||
if (!(Platform.isWindows || Platform.isMacOS || Platform.isLinux)) {
|
||||
return open(filePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (await FileSystemEntity.isFile(filePath)) {
|
||||
|
||||
@@ -26,6 +26,7 @@ import '../../application/pending_extension_install.dart';
|
||||
import '../widgets/extension_detail_view.dart';
|
||||
import '../widgets/extension_icon.dart';
|
||||
import '../widgets/extension_setting_field.dart';
|
||||
import '../widgets/extension_update_dialog.dart';
|
||||
|
||||
const _extensionCardMinWidth = 296.0;
|
||||
const _extensionGridSpacing = 10.0;
|
||||
@@ -211,7 +212,9 @@ class _ExtensionsPageState extends ConsumerState<ExtensionsPage> {
|
||||
}
|
||||
|
||||
void _openInstallPopover(BuildContext anchorContext) {
|
||||
ref.read(extensionsControllerProvider.notifier).tryOpenDevMode();
|
||||
if (AppWindowChrome.isDesktopWindow) {
|
||||
ref.read(extensionsControllerProvider.notifier).tryOpenDevMode();
|
||||
}
|
||||
_installDevMode = false;
|
||||
_showInstallPopover(anchorContext);
|
||||
}
|
||||
@@ -225,6 +228,9 @@ class _ExtensionsPageState extends ConsumerState<ExtensionsPage> {
|
||||
offset: const Offset(0, 8),
|
||||
modal: false,
|
||||
consumeOutsideTaps: false,
|
||||
// Flutter's native selection toolbar uses the EditableText tap group.
|
||||
// Pasting from it must not dismiss the form and restore search focus.
|
||||
regionGroupId: EditableText,
|
||||
builder: (context) => _InstallPopover(
|
||||
key: _installPopoverKey,
|
||||
controller: _installController,
|
||||
@@ -667,7 +673,6 @@ class _Toolbar extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final palette = AppPalette.of(context);
|
||||
final manualInstallBusy = state.busyExtensionIds.contains(ExtensionsController.manualInstallBusyKey);
|
||||
final search = AppTextField(
|
||||
key: const ValueKey('extension-search-input'),
|
||||
controller: searchController,
|
||||
@@ -709,12 +714,11 @@ class _Toolbar extends StatelessWidget {
|
||||
onPressed: onDevelopExtension,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
if (state.devMode) ...[
|
||||
if (AppWindowChrome.isDesktopWindow && state.devMode) ...[
|
||||
_OutlineToolbarIconButton(
|
||||
key: const ValueKey('load-local-extension-button'),
|
||||
tooltip: context.l10n.extensionLoadLocal,
|
||||
icon: Icons.folder_open_outlined,
|
||||
loading: manualInstallBusy,
|
||||
onPressed: onInstallFolder,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
@@ -725,7 +729,6 @@ class _Toolbar extends StatelessWidget {
|
||||
key: const ValueKey('install-extension-button'),
|
||||
tooltip: context.l10n.extensionInstallFromUrl,
|
||||
icon: Icons.add_link,
|
||||
loading: manualInstallBusy,
|
||||
onPressed: () => onOpenInstall(buttonContext),
|
||||
),
|
||||
),
|
||||
@@ -801,18 +804,11 @@ class _FilterBar extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _OutlineToolbarIconButton extends StatelessWidget {
|
||||
const _OutlineToolbarIconButton({
|
||||
super.key,
|
||||
required this.tooltip,
|
||||
required this.icon,
|
||||
required this.onPressed,
|
||||
this.loading = false,
|
||||
});
|
||||
const _OutlineToolbarIconButton({super.key, required this.tooltip, required this.icon, required this.onPressed});
|
||||
|
||||
final String tooltip;
|
||||
final IconData icon;
|
||||
final VoidCallback? onPressed;
|
||||
final bool loading;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -820,13 +816,7 @@ class _OutlineToolbarIconButton extends StatelessWidget {
|
||||
message: tooltip,
|
||||
child: SizedBox.square(
|
||||
dimension: 32,
|
||||
child: shad.IconButton.outline(
|
||||
size: shad.ButtonSize.xSmall,
|
||||
onPressed: loading ? null : onPressed,
|
||||
icon: loading
|
||||
? const SizedBox.square(dimension: 13, child: shad.CircularProgressIndicator())
|
||||
: Icon(icon, size: 17),
|
||||
),
|
||||
child: shad.IconButton.outline(size: shad.ButtonSize.xSmall, onPressed: onPressed, icon: Icon(icon, size: 17)),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1073,11 +1063,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)
|
||||
|
||||
@@ -20,6 +20,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});
|
||||
@@ -132,14 +133,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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -133,7 +133,7 @@ class TaskRecord {
|
||||
status: status,
|
||||
downloaded: _formatBytes(downloadedBytes),
|
||||
total: totalBytes > 0 ? _formatBytes(totalBytes) : null,
|
||||
speed: task.progress.speed > 0 ? TransferRateFormatter.format(task.progress.speed).text : null,
|
||||
speed: totalBytes > 0 || task.progress.speed > 0 ? TransferRateFormatter.format(task.progress.speed).text : null,
|
||||
uploadSpeed: task.uploading ? TransferRateFormatter.format(task.progress.uploadSpeed).text : null,
|
||||
remainingSeconds: _remainingSeconds(task, totalBytes, downloadedBytes),
|
||||
downloadDuration: task.progress.used > 0 ? Duration(microseconds: (task.progress.used + 999) ~/ 1000) : null,
|
||||
|
||||
@@ -37,6 +37,7 @@ import '../../../../shared/widgets/app_toast.dart';
|
||||
import '../../../../l10n/l10n.dart';
|
||||
import '../../../../util/util.dart';
|
||||
import '../../application/pending_create_task.dart';
|
||||
import '../../application/task_list_navigation.dart';
|
||||
import '../widgets/history_search_field.dart';
|
||||
import '../widgets/create_task_action_buttons.dart';
|
||||
import '../widgets/resolve_file_tree.dart';
|
||||
@@ -736,6 +737,8 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
|
||||
} catch (error) {
|
||||
debugPrint('Unable to select downloading tasks: $error');
|
||||
}
|
||||
} else {
|
||||
ref.read(taskListNavigationProvider.notifier).showDownloading();
|
||||
}
|
||||
await _closeWindow();
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart' show Colors, Icons;
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
|
||||
|
||||
import '../../../../../core/window/app_window_chrome.dart';
|
||||
import '../../../../../shared/theme/app_design_tokens.dart';
|
||||
import '../../../../../shared/theme/app_palette.dart';
|
||||
import '../../../domain/task_record.dart';
|
||||
@@ -237,7 +238,10 @@ class _ProgressContent extends StatelessWidget {
|
||||
TaskStatus.downloading => [(Icons.pause, onPause), (Icons.close, onDelete)],
|
||||
TaskStatus.paused => [(Icons.play_arrow, onResume), (Icons.close, onDelete)],
|
||||
TaskStatus.failed => [(Icons.refresh, onResume), (Icons.delete_outline, onDelete)],
|
||||
TaskStatus.completed => [(Icons.folder_open_outlined, null), (Icons.delete_outline, onDelete)],
|
||||
TaskStatus.completed => [
|
||||
if (AppWindowChrome.isDesktopWindow) (Icons.folder_open_outlined, null),
|
||||
(Icons.delete_outline, onDelete),
|
||||
],
|
||||
};
|
||||
|
||||
return actions
|
||||
@@ -306,7 +310,10 @@ class _CompletedContent extends StatelessWidget {
|
||||
TaskStatus.downloading => [(Icons.pause, null), (Icons.close, onDelete)],
|
||||
TaskStatus.paused => [(Icons.play_arrow, null), (Icons.close, onDelete)],
|
||||
TaskStatus.failed => [(Icons.refresh, null), (Icons.delete_outline, onDelete)],
|
||||
TaskStatus.completed => [(Icons.folder_open_outlined, onReveal), (Icons.delete_outline, onDelete)],
|
||||
TaskStatus.completed => [
|
||||
if (AppWindowChrome.isDesktopWindow) (Icons.folder_open_outlined, onReveal),
|
||||
(Icons.delete_outline, onDelete),
|
||||
],
|
||||
};
|
||||
|
||||
return actions
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart' show Colors, Icons;
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
|
||||
|
||||
import '../../../../../core/window/app_window_chrome.dart';
|
||||
import '../../../../../shared/theme/app_design_tokens.dart';
|
||||
import '../../../../../shared/theme/app_palette.dart';
|
||||
import '../../../domain/task_record.dart';
|
||||
@@ -171,7 +172,10 @@ class _TaskCardMobileState extends State<TaskCardMobile> {
|
||||
TaskStatus.downloading => [(Icons.pause, widget.onPause), (Icons.close, widget.onDelete)],
|
||||
TaskStatus.paused => [(Icons.play_arrow, widget.onResume), (Icons.close, widget.onDelete)],
|
||||
TaskStatus.failed => [(Icons.refresh, widget.onResume), (Icons.delete_outline, widget.onDelete)],
|
||||
TaskStatus.completed => [(Icons.folder_open_outlined, widget.onReveal), (Icons.delete_outline, widget.onDelete)],
|
||||
TaskStatus.completed => [
|
||||
if (AppWindowChrome.isDesktopWindow) (Icons.folder_open_outlined, widget.onReveal),
|
||||
(Icons.delete_outline, widget.onDelete),
|
||||
],
|
||||
};
|
||||
|
||||
return actions
|
||||
|
||||
@@ -106,7 +106,7 @@ class TaskCardFooter extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
if (task.speed != null && !isError) ...[
|
||||
if (task.speed != null) ...[
|
||||
_TaskTransferSpeed(icon: Icons.south, value: task.speed!, color: metaColor),
|
||||
const SizedBox(width: 8),
|
||||
TickDivider(color: palette.progressTrack),
|
||||
|
||||
@@ -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": "已是最新版本",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart' show TargetPlatform, debugDefaultTargetPlatformOverride;
|
||||
import 'package:flutter/foundation.dart'
|
||||
show TargetPlatform, defaultTargetPlatform, debugDefaultTargetPlatformOverride, kIsWeb;
|
||||
import 'package:flutter/material.dart' show Icons, Scrollbar, SelectionArea;
|
||||
import 'package:flutter/gestures.dart' show PointerDeviceKind, kDoubleTapMinTime, kSecondaryMouseButton;
|
||||
import 'package:flutter/rendering.dart' show RenderParagraph;
|
||||
@@ -15,6 +16,12 @@ import 'package:window_manager/window_manager.dart';
|
||||
import 'package:gopeed/features/home/presentation/pages/home_page.dart';
|
||||
import 'package:gopeed/app/app.dart';
|
||||
import 'package:gopeed/app/router/app_router.dart';
|
||||
import 'package:gopeed/core/capabilities/capability_rpc.dart';
|
||||
import 'package:gopeed/core/capabilities/gopeed_capability.dart';
|
||||
import 'package:gopeed/core/capabilities/storage_capability.dart';
|
||||
import 'package:gopeed/api/model/resolve_result.dart';
|
||||
import 'package:gopeed/features/tasks/application/pending_create_task.dart';
|
||||
import 'package:gopeed/features/tasks/application/task_list_navigation.dart';
|
||||
import 'package:gopeed/core/capabilities/app_capabilities.dart';
|
||||
import 'package:gopeed/core/capabilities/app_navigation_capability.dart';
|
||||
import 'package:gopeed/app/application/app_appearance_controller.dart';
|
||||
@@ -99,6 +106,85 @@ import 'package:gopeed/util/updater.dart';
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized().platformDispatcher.localeTestValue = const Locale('en');
|
||||
|
||||
for (final direct in [true, false]) {
|
||||
testWidgets(
|
||||
'mobile creation selects downloading after success (direct: $direct)',
|
||||
(tester) async {
|
||||
await _setTestSize(tester, const Size(700, 900));
|
||||
var created = 0;
|
||||
final config = DownloaderConfig(downloadDir: '/downloads')..extra.defaultDirectDownload = direct;
|
||||
final registry = CapabilityRegistry(createAppCapabilityCodecs())
|
||||
..bind(GopeedMethods.getConfig, (_) => config)
|
||||
..bind(StorageMethods.saveCreateHistory, (_) => const RpcUnit())
|
||||
..bind(
|
||||
GopeedMethods.resolve,
|
||||
(_) => ResolveResult(
|
||||
id: 'resolved',
|
||||
res: Resource(
|
||||
name: 'new.zip',
|
||||
files: [FileInfo(name: 'new.zip', size: 10)],
|
||||
),
|
||||
),
|
||||
)
|
||||
..bind(GopeedMethods.createTask, (_) {
|
||||
created++;
|
||||
return 'new-task';
|
||||
});
|
||||
final container = ProviderContainer(
|
||||
overrides: [
|
||||
appCapabilitiesProvider.overrideWithValue(AppCapabilities(LocalCapabilityInvoker(registry))),
|
||||
appRuntimeControllerProvider.overrideWith(FakeRuntimeController.new),
|
||||
appPlatformControllerProvider.overrideWith(FakePlatformController.new),
|
||||
appDeepLinkControllerProvider.overrideWith(FakeDeepLinkController.new),
|
||||
appNotificationControllerProvider.overrideWith(FakeNotificationController.new),
|
||||
tasksControllerProvider.overrideWith(CompletedTasksController.new),
|
||||
settingsControllerProvider.overrideWith(FakeSettingsController.new),
|
||||
],
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
await tester.pumpWidget(UncontrolledProviderScope(container: container, child: const GopeedApp()));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Completed 1'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('done.zip'), findsOneWidget);
|
||||
final router = GoRouter.of(AppRouter.rootNavigatorKey.currentContext!);
|
||||
void openCreate() {
|
||||
container
|
||||
.read(pendingCreateTaskProvider.notifier)
|
||||
.set(CreateTask(req: Request(url: 'https://example.com/new.zip')));
|
||||
router.go('/create');
|
||||
}
|
||||
|
||||
openCreate();
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byKey(const ValueKey('create-task-cancel-button')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('done.zip'), findsOneWidget);
|
||||
expect(container.read(taskListNavigationProvider), 0);
|
||||
expect(created, 0);
|
||||
openCreate();
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byKey(const ValueKey('create-task-confirm-button')));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
if (!direct) {
|
||||
expect(created, 0);
|
||||
await tester.tap(find.byKey(const ValueKey('resolve-create-button')));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
await tester.pumpAndSettle();
|
||||
expect(created, 1);
|
||||
expect(router.routeInformationProvider.value.uri.path, '/');
|
||||
expect(container.read(taskListNavigationProvider), 1);
|
||||
expect(find.text('done.zip'), findsNothing);
|
||||
expect(tester.takeException(), isNull);
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
await tester.pumpAndSettle();
|
||||
},
|
||||
variant: TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}),
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('child success returns to downloading from completed and another page', (tester) async {
|
||||
await _setTestSize(tester, const Size(1024, 768));
|
||||
await tester.pumpWidget(
|
||||
@@ -672,6 +758,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 {
|
||||
@@ -881,30 +1034,37 @@ void main() {
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('extension install actions show progress instead of a disabled icon while busy', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
await _setTestSize(tester, const Size(1100, 900));
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [extensionsControllerProvider.overrideWith(BusyInstallExtensionsController.new)],
|
||||
child: shad.ShadcnApp(
|
||||
theme: AppTheme.light(),
|
||||
materialTheme: AppTheme.materialLight(),
|
||||
home: const ExtensionsPage(),
|
||||
testWidgets(
|
||||
'URL installation leaves toolbar icons unchanged while store installation shows progress',
|
||||
(WidgetTester tester) async {
|
||||
await _setTestSize(tester, const Size(1100, 900));
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [extensionsControllerProvider.overrideWith(BusyInstallExtensionsController.new)],
|
||||
child: shad.ShadcnApp(
|
||||
theme: AppTheme.light(),
|
||||
materialTheme: AppTheme.materialLight(),
|
||||
home: const ExtensionsPage(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
final manualInstall = find.byKey(const ValueKey('install-extension-button'));
|
||||
final storeInstall = find.byKey(const ValueKey('install-store-extension-extension-1'));
|
||||
expect(find.descendant(of: manualInstall, matching: find.byType(shad.CircularProgressIndicator)), findsOneWidget);
|
||||
expect(find.descendant(of: storeInstall, matching: find.byType(shad.CircularProgressIndicator)), findsOneWidget);
|
||||
expect(find.descendant(of: storeInstall, matching: find.byIcon(Icons.download)), findsNothing);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
final manualInstall = find.byKey(const ValueKey('install-extension-button'));
|
||||
final storeInstall = find.byKey(const ValueKey('install-store-extension-extension-1'));
|
||||
expect(find.descendant(of: manualInstall, matching: find.byType(shad.CircularProgressIndicator)), findsNothing);
|
||||
expect(find.descendant(of: manualInstall, matching: find.byIcon(Icons.add_link)), findsOneWidget);
|
||||
final localInstall = find.byKey(const ValueKey('load-local-extension-button'));
|
||||
expect(localInstall, findsOneWidget);
|
||||
expect(find.descendant(of: localInstall, matching: find.byType(shad.CircularProgressIndicator)), findsNothing);
|
||||
expect(find.descendant(of: localInstall, matching: find.byIcon(Icons.folder_open_outlined)), findsOneWidget);
|
||||
expect(find.descendant(of: storeInstall, matching: find.byType(shad.CircularProgressIndicator)), findsOneWidget);
|
||||
expect(find.descendant(of: storeInstall, matching: find.byIcon(Icons.download)), findsNothing);
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
variant: TargetPlatformVariant.only(TargetPlatform.macOS),
|
||||
);
|
||||
|
||||
for (final (width, platform) in [
|
||||
(320.0, TargetPlatform.android),
|
||||
@@ -987,41 +1147,136 @@ void main() {
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('extension install popover unlocks local loading and installed cards use switches', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
await _setTestSize(tester, const Size(1100, 900));
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [extensionsControllerProvider.overrideWith(FakeExtensionsController.new)],
|
||||
child: shad.ShadcnApp(
|
||||
theme: AppTheme.light(),
|
||||
materialTheme: AppTheme.materialLight(),
|
||||
home: const ExtensionsPage(),
|
||||
testWidgets(
|
||||
'install URL keeps focus after native menu paste',
|
||||
(tester) async {
|
||||
await _setTestSize(tester, const Size(390, 900));
|
||||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (call) async {
|
||||
if (call.method == 'Clipboard.hasStrings') return {'value': true};
|
||||
if (call.method == 'Clipboard.getData') return {'text': 'https://github.com/author/repo'};
|
||||
return null;
|
||||
});
|
||||
addTearDown(() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, null));
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [extensionsControllerProvider.overrideWith(FakeExtensionsController.new)],
|
||||
child: shad.ShadcnApp(
|
||||
theme: AppTheme.light(),
|
||||
materialTheme: AppTheme.materialLight(),
|
||||
home: const ExtensionsPage(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
final search = find.byKey(const ValueKey('extension-search-input'));
|
||||
await tester.tap(search);
|
||||
await tester.tap(find.byKey(const ValueKey('install-extension-button')));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
final input = find.byKey(const ValueKey('extension-install-url-input'));
|
||||
final editableFinder = find.descendant(of: input, matching: find.byType(EditableText));
|
||||
final editable = tester.widget<EditableText>(editableFinder);
|
||||
expect(editable.focusNode.hasFocus, isTrue);
|
||||
if (defaultTargetPlatform == TargetPlatform.macOS) {
|
||||
final gesture = await tester.startGesture(
|
||||
tester.getCenter(input),
|
||||
kind: PointerDeviceKind.mouse,
|
||||
buttons: kSecondaryMouseButton,
|
||||
);
|
||||
await gesture.up();
|
||||
} else {
|
||||
await tester.longPress(input);
|
||||
}
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
await tester.tap(find.text('Paste'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
expect(input, findsOneWidget);
|
||||
expect(editable.controller.text, 'https://github.com/author/repo');
|
||||
expect(editable.focusNode.hasFocus, isTrue);
|
||||
expect(
|
||||
tester
|
||||
.widget<EditableText>(find.descendant(of: search, matching: find.byType(EditableText)))
|
||||
.focusNode
|
||||
.hasFocus,
|
||||
isFalse,
|
||||
);
|
||||
await tester.tapAt(const Offset(10, 500));
|
||||
await tester.pumpAndSettle();
|
||||
expect(input, findsNothing);
|
||||
},
|
||||
variant: TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS, TargetPlatform.macOS}),
|
||||
);
|
||||
|
||||
expect(find.byType(shad.Switch), findsOneWidget);
|
||||
expect(find.byKey(const ValueKey('load-local-extension-button')), findsNothing);
|
||||
final installButton = find.byKey(const ValueKey('install-extension-button'));
|
||||
expect(find.descendant(of: installButton, matching: find.byType(shad.IconButton)), findsOneWidget);
|
||||
testWidgets(
|
||||
'mobile does not unlock local extension installation after five taps',
|
||||
(tester) async {
|
||||
// A tablet-sized mobile screen must still use mobile platform policy.
|
||||
await _setTestSize(tester, const Size(1100, 900));
|
||||
final controller = FakeExtensionsController();
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [extensionsControllerProvider.overrideWith(() => controller)],
|
||||
child: shad.ShadcnApp(
|
||||
theme: AppTheme.light(),
|
||||
materialTheme: AppTheme.materialLight(),
|
||||
home: const ExtensionsPage(),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
for (var i = 0; i < 5; i++) {
|
||||
await tester.tap(find.byKey(const ValueKey('install-extension-button')));
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
}
|
||||
expect(controller.devModeEnabled, isFalse);
|
||||
expect(find.byKey(const ValueKey('load-local-extension-button')), findsNothing);
|
||||
// Even a pre-existing dev-mode state must not expose the native folder action.
|
||||
controller.enableDevModeForTest();
|
||||
await tester.pump();
|
||||
expect(find.byKey(const ValueKey('load-local-extension-button')), findsNothing);
|
||||
},
|
||||
variant: TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}),
|
||||
);
|
||||
|
||||
for (var index = 0; index < 5; index++) {
|
||||
await tester.tap(installButton);
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
}
|
||||
testWidgets(
|
||||
'extension install popover unlocks local loading and installed cards use switches',
|
||||
(WidgetTester tester) async {
|
||||
await _setTestSize(tester, const Size(1100, 900));
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [extensionsControllerProvider.overrideWith(FakeExtensionsController.new)],
|
||||
child: shad.ShadcnApp(
|
||||
theme: AppTheme.light(),
|
||||
materialTheme: AppTheme.materialLight(),
|
||||
home: const ExtensionsPage(),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const ValueKey('extension-install-popover')), findsOneWidget);
|
||||
expect(find.byKey(const ValueKey('extension-install-url-input')), findsOneWidget);
|
||||
final localButton = find.byKey(const ValueKey('load-local-extension-button'));
|
||||
expect(localButton, findsOneWidget);
|
||||
expect(find.descendant(of: localButton, matching: find.byType(shad.IconButton)), findsOneWidget);
|
||||
expect(find.byType(shad.Switch), findsOneWidget);
|
||||
expect(find.byKey(const ValueKey('load-local-extension-button')), findsNothing);
|
||||
final installButton = find.byKey(const ValueKey('install-extension-button'));
|
||||
expect(find.descendant(of: installButton, matching: find.byType(shad.IconButton)), findsOneWidget);
|
||||
|
||||
await tester.pump(const Duration(seconds: 3));
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
for (var index = 0; index < 5; index++) {
|
||||
await tester.tap(installButton);
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
}
|
||||
|
||||
expect(find.byKey(const ValueKey('extension-install-popover')), findsOneWidget);
|
||||
expect(find.byKey(const ValueKey('extension-install-url-input')), findsOneWidget);
|
||||
final localButton = find.byKey(const ValueKey('load-local-extension-button'));
|
||||
expect(localButton, findsOneWidget);
|
||||
expect(find.descendant(of: localButton, matching: find.byType(shad.IconButton)), findsOneWidget);
|
||||
|
||||
await tester.pump(const Duration(seconds: 3));
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
variant: TargetPlatformVariant({TargetPlatform.windows, TargetPlatform.macOS, TargetPlatform.linux}),
|
||||
);
|
||||
|
||||
testWidgets('responsive menu uses sidebar on desktop and two-level navigation on mobile', (
|
||||
WidgetTester tester,
|
||||
@@ -3154,6 +3409,78 @@ void main() {
|
||||
expect(record.isIndeterminate, isTrue);
|
||||
});
|
||||
|
||||
test('known total size preserves zero download speed independently of task status', () {
|
||||
for (final status in api_task.Status.values) {
|
||||
for (final total in [0, 1024]) {
|
||||
for (final speed in [0, 512]) {
|
||||
final task = _apiTransferTask(id: 'speed', status: status, uploading: false, speed: speed, uploadSpeed: 0);
|
||||
task.meta.res = Resource(size: total, files: []);
|
||||
expect(TaskRecord.fromApi(task).speed, total > 0 || speed > 0 ? '$speed B/s' : isNull);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (final width in [390.0, 1024.0]) {
|
||||
testWidgets('known-size task cards retain zero download speed at width $width', (tester) async {
|
||||
await _setTestSize(tester, Size(width, 220));
|
||||
for (final status in api_task.Status.values) {
|
||||
final task = _apiTransferTask(id: 'zero-speed', status: status, uploading: false, speed: 0, uploadSpeed: 0);
|
||||
task.meta.res = Resource(size: 1024, files: []);
|
||||
await tester.pumpWidget(
|
||||
shad.ShadcnApp(
|
||||
theme: AppTheme.light(),
|
||||
materialTheme: AppTheme.materialLight(),
|
||||
home: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: TaskCard(
|
||||
task: TaskRecord.fromApi(task),
|
||||
selected: false,
|
||||
batchMode: false,
|
||||
selectedInBatch: false,
|
||||
onPressed: () {},
|
||||
onToggleBatch: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
expect(find.text('0 B/s'), findsOneWidget);
|
||||
expect(find.byIcon(Icons.south), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
testWidgets('known-size task details retain zero download speed', (tester) async {
|
||||
await _setTestSize(tester, const Size(700, 900));
|
||||
final task = _apiTransferTask(
|
||||
id: 'zero-details',
|
||||
status: api_task.Status.running,
|
||||
uploading: false,
|
||||
speed: 0,
|
||||
uploadSpeed: 0,
|
||||
);
|
||||
task.meta.res = Resource(size: 1024, files: []);
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
child: shad.ShadcnApp(
|
||||
theme: AppTheme.light(),
|
||||
materialTheme: AppTheme.materialLight(),
|
||||
home: TaskDetailsView(
|
||||
task: TaskRecord.fromApi(task),
|
||||
mobile: true,
|
||||
onOpenStorage: () {},
|
||||
onUpdateUrl: (_) async {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('0 B/s'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
test('task records preserve zero upload speed while uploading', () {
|
||||
final record = TaskRecord.fromApi(
|
||||
_apiTransferTask(id: 'seeding', status: api_task.Status.done, uploading: true, speed: 0, uploadSpeed: 0),
|
||||
@@ -4928,45 +5255,100 @@ void main() {
|
||||
expect(openedCount, 0);
|
||||
});
|
||||
|
||||
testWidgets('completed task double click opens while folder action reveals', (WidgetTester tester) async {
|
||||
await _setTestSize(tester, const Size(1024, 220));
|
||||
var openedCount = 0;
|
||||
var revealedCount = 0;
|
||||
for (final width in [390.0, 1024.0]) {
|
||||
testWidgets(
|
||||
'completed card actions respect platform at width $width',
|
||||
(tester) async {
|
||||
await _setTestSize(tester, Size(width, 220));
|
||||
var deleted = 0;
|
||||
var revealed = 0;
|
||||
await tester.pumpWidget(
|
||||
shad.ShadcnApp(
|
||||
theme: AppTheme.light(),
|
||||
materialTheme: AppTheme.materialLight(),
|
||||
home: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: TaskCard(
|
||||
task: _taskRecord(id: 'platform-actions', name: 'done.zip', status: TaskStatus.completed),
|
||||
selected: false,
|
||||
batchMode: false,
|
||||
selectedInBatch: false,
|
||||
onPressed: () {},
|
||||
onToggleBatch: () {},
|
||||
onDelete: () => deleted++,
|
||||
onReveal: () => revealed++,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
final desktop =
|
||||
!kIsWeb &&
|
||||
{TargetPlatform.windows, TargetPlatform.macOS, TargetPlatform.linux}.contains(defaultTargetPlatform);
|
||||
expect(find.byIcon(Icons.folder_open_outlined), desktop ? findsOneWidget : findsNothing);
|
||||
expect(find.byIcon(Icons.delete_outline), findsOneWidget);
|
||||
if (desktop) {
|
||||
await tester.tap(find.byIcon(Icons.folder_open_outlined));
|
||||
expect(revealed, 1);
|
||||
}
|
||||
await tester.tap(find.byIcon(Icons.delete_outline));
|
||||
expect(deleted, 1);
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
variant: TargetPlatformVariant({
|
||||
TargetPlatform.android,
|
||||
TargetPlatform.iOS,
|
||||
TargetPlatform.windows,
|
||||
TargetPlatform.macOS,
|
||||
TargetPlatform.linux,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(
|
||||
shad.ShadcnApp(
|
||||
theme: AppTheme.light(),
|
||||
materialTheme: AppTheme.materialLight(),
|
||||
home: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: TaskCard(
|
||||
task: _taskRecord(id: 'completed-actions', name: 'completed.zip', status: TaskStatus.completed),
|
||||
selected: false,
|
||||
batchMode: false,
|
||||
selectedInBatch: false,
|
||||
onPressed: () {},
|
||||
onToggleBatch: () {},
|
||||
onOpen: () => openedCount++,
|
||||
onReveal: () => revealedCount++,
|
||||
testWidgets(
|
||||
'completed task double click opens while folder action reveals',
|
||||
(WidgetTester tester) async {
|
||||
await _setTestSize(tester, const Size(1024, 220));
|
||||
var openedCount = 0;
|
||||
var revealedCount = 0;
|
||||
|
||||
await tester.pumpWidget(
|
||||
shad.ShadcnApp(
|
||||
theme: AppTheme.light(),
|
||||
materialTheme: AppTheme.materialLight(),
|
||||
home: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: TaskCard(
|
||||
task: _taskRecord(id: 'completed-actions', name: 'completed.zip', status: TaskStatus.completed),
|
||||
selected: false,
|
||||
batchMode: false,
|
||||
selectedInBatch: false,
|
||||
onPressed: () {},
|
||||
onToggleBatch: () {},
|
||||
onOpen: () => openedCount++,
|
||||
onReveal: () => revealedCount++,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
|
||||
await tester.tap(find.text('completed.zip'));
|
||||
await tester.pump(kDoubleTapMinTime);
|
||||
await tester.tap(find.text('completed.zip'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('completed.zip'));
|
||||
await tester.pump(kDoubleTapMinTime);
|
||||
await tester.tap(find.text('completed.zip'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(openedCount, 1);
|
||||
expect(revealedCount, 0);
|
||||
expect(openedCount, 1);
|
||||
expect(revealedCount, 0);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.folder_open_outlined));
|
||||
await tester.pump();
|
||||
await tester.tap(find.byIcon(Icons.folder_open_outlined));
|
||||
await tester.pump();
|
||||
|
||||
expect(openedCount, 1);
|
||||
expect(revealedCount, 1);
|
||||
});
|
||||
expect(openedCount, 1);
|
||||
expect(revealedCount, 1);
|
||||
},
|
||||
variant: TargetPlatformVariant.only(TargetPlatform.macOS),
|
||||
skip: kIsWeb,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _setTestSize(WidgetTester tester, Size size) async {
|
||||
@@ -5097,6 +5479,12 @@ class FailingTasksController extends TasksController {
|
||||
}
|
||||
|
||||
class FakeExtensionsController extends ExtensionsController {
|
||||
bool get devModeEnabled => state.requireValue.devMode;
|
||||
|
||||
void enableDevModeForTest() {
|
||||
state = AsyncValue.data(state.requireValue.copyWith(devMode: true));
|
||||
}
|
||||
|
||||
int installCalls = 0;
|
||||
String? lastInstallUrl;
|
||||
Completer<void>? installGate;
|
||||
@@ -5274,11 +5662,31 @@ 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 {
|
||||
final initial = await super.build();
|
||||
return initial.copyWith(busyExtensionIds: {ExtensionsController.manualInstallBusyKey, 'extension-1'});
|
||||
return initial.copyWith(
|
||||
devMode: true,
|
||||
busyExtensionIds: {ExtensionsController.manualInstallBusyKey, 'extension-1'},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user