mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-14 12:23:43 +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
@@ -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),
|
||||
|
||||
Reference in new issue
Block a user