mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-12 19:33:41 +00:00
Merge PR #1505: emphasize task creation and empty-list actions
This commit is contained in:
commit
2d239495a0
5 files changed
+100
-49
No files matched your search
@@ -30,6 +30,7 @@ import '../../../tasks/presentation/widgets/task_drawer.dart';
|
|||||||
import '../../../tasks/presentation/widgets/task_context_menu.dart';
|
import '../../../tasks/presentation/widgets/task_context_menu.dart';
|
||||||
import '../../../tasks/presentation/widgets/task_delete_dialog.dart';
|
import '../../../tasks/presentation/widgets/task_delete_dialog.dart';
|
||||||
import '../../../tasks/presentation/widgets/task_empty_state.dart';
|
import '../../../tasks/presentation/widgets/task_empty_state.dart';
|
||||||
|
import '../../../tasks/presentation/widgets/task_create_button.dart';
|
||||||
import '../../../tasks/presentation/widgets/task_file_browser_dialog.dart';
|
import '../../../tasks/presentation/widgets/task_file_browser_dialog.dart';
|
||||||
import '../../../tasks/presentation/widgets/task_update_url_dialog.dart';
|
import '../../../tasks/presentation/widgets/task_update_url_dialog.dart';
|
||||||
import '../widgets/primary_rail.dart';
|
import '../widgets/primary_rail.dart';
|
||||||
@@ -172,7 +173,10 @@ class _HomePageState extends ConsumerState<HomePage> {
|
|||||||
loading: () => const Center(child: shad.CircularProgressIndicator()),
|
loading: () => const Center(child: shad.CircularProgressIndicator()),
|
||||||
error: (error, _) => const TaskEmptyState(),
|
error: (error, _) => const TaskEmptyState(),
|
||||||
data: (_) => filteredTasks.isEmpty
|
data: (_) => filteredTasks.isEmpty
|
||||||
? TaskEmptyState(message: emptyStateMessage)
|
? TaskEmptyState(
|
||||||
|
message: emptyStateMessage,
|
||||||
|
onCreateTask: _searchController.text.trim().isEmpty ? _handleAddTask : null,
|
||||||
|
)
|
||||||
: ListView.separated(
|
: ListView.separated(
|
||||||
padding: const EdgeInsets.only(bottom: 16),
|
padding: const EdgeInsets.only(bottom: 16),
|
||||||
itemCount: filteredTasks.length,
|
itemCount: filteredTasks.length,
|
||||||
@@ -260,6 +264,7 @@ class _HomePageState extends ConsumerState<HomePage> {
|
|||||||
data: (_) => _MobileTasksView(
|
data: (_) => _MobileTasksView(
|
||||||
tasks: filteredTasks,
|
tasks: filteredTasks,
|
||||||
emptyStateMessage: emptyStateMessage,
|
emptyStateMessage: emptyStateMessage,
|
||||||
|
onCreateTask: _searchController.text.trim().isEmpty ? _handleAddTask : null,
|
||||||
batchMode: _batchMode,
|
batchMode: _batchMode,
|
||||||
batchSelection: _batchSelection,
|
batchSelection: _batchSelection,
|
||||||
onToggleSelectAll: _toggleSelectAll,
|
onToggleSelectAll: _toggleSelectAll,
|
||||||
@@ -685,6 +690,7 @@ class _MobileTasksView extends StatelessWidget {
|
|||||||
required this.contextActionsForTask,
|
required this.contextActionsForTask,
|
||||||
required this.pendingUpdateTaskId,
|
required this.pendingUpdateTaskId,
|
||||||
this.emptyStateMessage,
|
this.emptyStateMessage,
|
||||||
|
this.onCreateTask,
|
||||||
this.loading = false,
|
this.loading = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -705,6 +711,7 @@ class _MobileTasksView extends StatelessWidget {
|
|||||||
final TaskContextActions Function(TaskRecord task, bool selected, bool allSelected) contextActionsForTask;
|
final TaskContextActions Function(TaskRecord task, bool selected, bool allSelected) contextActionsForTask;
|
||||||
final String? pendingUpdateTaskId;
|
final String? pendingUpdateTaskId;
|
||||||
final String? emptyStateMessage;
|
final String? emptyStateMessage;
|
||||||
|
final VoidCallback? onCreateTask;
|
||||||
final bool loading;
|
final bool loading;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -745,7 +752,7 @@ class _MobileTasksView extends StatelessWidget {
|
|||||||
child: loading
|
child: loading
|
||||||
? const Center(child: shad.CircularProgressIndicator())
|
? const Center(child: shad.CircularProgressIndicator())
|
||||||
: tasks.isEmpty
|
: tasks.isEmpty
|
||||||
? TaskEmptyState(message: emptyStateMessage)
|
? TaskEmptyState(message: emptyStateMessage, onCreateTask: onCreateTask)
|
||||||
: ListView.separated(
|
: ListView.separated(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 12),
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 12),
|
||||||
itemCount: tasks.length,
|
itemCount: tasks.length,
|
||||||
@@ -814,7 +821,15 @@ class _MobileTasksHeader extends StatelessWidget {
|
|||||||
style: TextStyle(color: palette.textPrimary, fontSize: 22, fontWeight: FontWeight.w800),
|
style: TextStyle(color: palette.textPrimary, fontSize: 22, fontWeight: FontWeight.w800),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_MobileHeaderIconButton(icon: Icons.add_rounded, onPressed: onAddTask),
|
const SizedBox(width: AppDesignTokens.space8),
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(maxWidth: MediaQuery.sizeOf(context).width * 0.5),
|
||||||
|
child: TaskCreateButton(
|
||||||
|
key: const ValueKey('tasks-mobile-create-button'),
|
||||||
|
onPressed: onAddTask,
|
||||||
|
minHeight: 44,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
_MobileHeaderIconButton(
|
_MobileHeaderIconButton(
|
||||||
icon: Icons.checklist_rtl_outlined,
|
icon: Icons.checklist_rtl_outlined,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
|
|||||||
|
|
||||||
import '../../../../shared/theme/app_design_tokens.dart';
|
import '../../../../shared/theme/app_design_tokens.dart';
|
||||||
import '../../../../shared/theme/app_palette.dart';
|
import '../../../../shared/theme/app_palette.dart';
|
||||||
import '../../../../shared/widgets/app_primary_button.dart';
|
import '../../../tasks/presentation/widgets/task_create_button.dart';
|
||||||
import '../../../../shared/widgets/app_text_field.dart';
|
import '../../../../shared/widgets/app_text_field.dart';
|
||||||
import '../../../../l10n/l10n.dart';
|
import '../../../../l10n/l10n.dart';
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ class TasksTopBar extends StatelessWidget {
|
|||||||
ConstrainedBox(
|
ConstrainedBox(
|
||||||
key: const ValueKey('tasks-create-button-container'),
|
key: const ValueKey('tasks-create-button-container'),
|
||||||
constraints: const BoxConstraints(minWidth: 120, maxWidth: 190),
|
constraints: const BoxConstraints(minWidth: 120, maxWidth: 190),
|
||||||
child: _AddTaskButton(onPressed: onAddTask),
|
child: TaskCreateButton(onPressed: onAddTask),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Row(
|
Row(
|
||||||
@@ -131,27 +131,6 @@ class _SearchField extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AddTaskButton extends StatelessWidget {
|
|
||||||
const _AddTaskButton({required this.onPressed});
|
|
||||||
|
|
||||||
final VoidCallback onPressed;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AppPrimaryButton(
|
|
||||||
onPressed: onPressed,
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.add),
|
|
||||||
const SizedBox(width: AppDesignTokens.space8),
|
|
||||||
Flexible(child: Text(context.l10n.create, maxLines: 1, overflow: TextOverflow.ellipsis)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ActionIconButton extends StatelessWidget {
|
class _ActionIconButton extends StatelessWidget {
|
||||||
const _ActionIconButton({required this.icon, required this.onPressed});
|
const _ActionIconButton({required this.icon, required this.onPressed});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import 'package:flutter/material.dart' show Icons;
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import '../../../../l10n/l10n.dart';
|
||||||
|
import '../../../../shared/theme/app_design_tokens.dart';
|
||||||
|
import '../../../../shared/widgets/app_primary_button.dart';
|
||||||
|
|
||||||
|
/// Shared creation action for task headers and empty-list guidance.
|
||||||
|
class TaskCreateButton extends StatelessWidget {
|
||||||
|
const TaskCreateButton({super.key, required this.onPressed, this.minHeight = 0});
|
||||||
|
|
||||||
|
final VoidCallback onPressed;
|
||||||
|
final double minHeight;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(minHeight: minHeight),
|
||||||
|
child: AppPrimaryButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.add),
|
||||||
|
const SizedBox(width: AppDesignTokens.space8),
|
||||||
|
Flexible(child: Text(context.l10n.create, maxLines: 1, overflow: TextOverflow.ellipsis)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,35 +1,49 @@
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import '../../../../shared/theme/app_palette.dart';
|
import '../../../../shared/theme/app_palette.dart';
|
||||||
|
import '../../../../shared/theme/app_design_tokens.dart';
|
||||||
|
import 'task_create_button.dart';
|
||||||
import '../../../../l10n/l10n.dart';
|
import '../../../../l10n/l10n.dart';
|
||||||
|
|
||||||
class TaskEmptyState extends StatelessWidget {
|
class TaskEmptyState extends StatelessWidget {
|
||||||
const TaskEmptyState({super.key, this.message});
|
const TaskEmptyState({super.key, this.message, this.onCreateTask});
|
||||||
|
|
||||||
final String? message;
|
final String? message;
|
||||||
|
final VoidCallback? onCreateTask;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final palette = AppPalette.of(context);
|
final palette = AppPalette.of(context);
|
||||||
return Center(
|
return Center(
|
||||||
child: ConstrainedBox(
|
child: SingleChildScrollView(
|
||||||
constraints: const BoxConstraints(maxWidth: 360),
|
child: ConstrainedBox(
|
||||||
child: Padding(
|
constraints: const BoxConstraints(maxWidth: 360),
|
||||||
padding: const EdgeInsets.all(24),
|
child: Padding(
|
||||||
child: Column(
|
padding: const EdgeInsets.all(24),
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Column(
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
CustomPaint(
|
children: [
|
||||||
key: const ValueKey('task-empty-illustration'),
|
CustomPaint(
|
||||||
size: const Size(148, 108),
|
key: const ValueKey('task-empty-illustration'),
|
||||||
painter: _TaskEmptyIllustrationPainter(palette: palette),
|
size: const Size(148, 108),
|
||||||
),
|
painter: _TaskEmptyIllustrationPainter(palette: palette),
|
||||||
const SizedBox(height: 18),
|
),
|
||||||
Text(
|
const SizedBox(height: 18),
|
||||||
message ?? context.l10n.emptyTaskList,
|
Text(
|
||||||
style: TextStyle(color: palette.textPrimary, fontSize: 17, fontWeight: FontWeight.w700),
|
message ?? context.l10n.emptyTaskList,
|
||||||
),
|
textAlign: TextAlign.center,
|
||||||
],
|
style: TextStyle(color: palette.textPrimary, fontSize: 17, fontWeight: FontWeight.w700),
|
||||||
|
),
|
||||||
|
if (onCreateTask != null) ...[
|
||||||
|
const SizedBox(height: AppDesignTokens.space24),
|
||||||
|
TaskCreateButton(
|
||||||
|
key: const ValueKey('tasks-empty-create-button'),
|
||||||
|
onPressed: onCreateTask!,
|
||||||
|
minHeight: 48,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ void main() {
|
|||||||
await LocalAppCapabilities.instance.registry.invoke(NavigationMethods.showDownloadingTasks.name, {});
|
await LocalAppCapabilities.instance.registry.invoke(NavigationMethods.showDownloadingTasks.name, {});
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('done.zip'), findsNothing);
|
expect(find.text('done.zip'), findsNothing);
|
||||||
expect(find.text('Create Task'), findsOneWidget);
|
expect(find.text('Create Task'), findsNWidgets(2));
|
||||||
|
|
||||||
GoRouter.of(AppRouter.rootNavigatorKey.currentContext!).go('/settings');
|
GoRouter.of(AppRouter.rootNavigatorKey.currentContext!).go('/settings');
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
@@ -125,7 +125,7 @@ void main() {
|
|||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(GoRouter.of(AppRouter.rootNavigatorKey.currentContext!).routeInformationProvider.value.uri.path, '/');
|
expect(GoRouter.of(AppRouter.rootNavigatorKey.currentContext!).routeInformationProvider.value.uri.path, '/');
|
||||||
expect(find.text('done.zip'), findsNothing);
|
expect(find.text('done.zip'), findsNothing);
|
||||||
expect(find.text('Create Task'), findsOneWidget);
|
expect(find.text('Create Task'), findsNWidgets(2));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Web MCP endpoint excludes page path, query, and hash route', () {
|
test('Web MCP endpoint excludes page path, query, and hash route', () {
|
||||||
@@ -227,14 +227,14 @@ void main() {
|
|||||||
expect(find.text('Downloading'), findsOneWidget);
|
expect(find.text('Downloading'), findsOneWidget);
|
||||||
expect(find.text('Completed'), findsOneWidget);
|
expect(find.text('Completed'), findsOneWidget);
|
||||||
expect(find.text('Failed'), findsOneWidget);
|
expect(find.text('Failed'), findsOneWidget);
|
||||||
expect(find.text('Create Task'), findsOneWidget);
|
expect(find.text('Create Task'), findsNWidgets(2));
|
||||||
expect(find.text('No tasks in this list'), findsOneWidget);
|
expect(find.text('No tasks in this list'), findsOneWidget);
|
||||||
expect(find.text('Add Task'), findsNothing);
|
expect(find.text('Add Task'), findsNothing);
|
||||||
expect(find.byKey(const ValueKey('task-empty-illustration')), findsOneWidget);
|
expect(find.byKey(const ValueKey('task-empty-illustration')), findsOneWidget);
|
||||||
expect(find.text('Create a task or refresh the list.'), findsNothing);
|
expect(find.text('Create a task or refresh the list.'), findsNothing);
|
||||||
expect(find.text('Refresh'), findsNothing);
|
expect(find.text('Refresh'), findsNothing);
|
||||||
expect(find.textContaining('ACTIVE CONNECTIONS'), findsNothing);
|
expect(find.textContaining('ACTIVE CONNECTIONS'), findsNothing);
|
||||||
expect(find.ancestor(of: find.text('Create Task'), matching: find.byType(AppPrimaryButton)), findsOneWidget);
|
expect(find.ancestor(of: find.text('Create Task'), matching: find.byType(AppPrimaryButton)), findsNWidgets(2));
|
||||||
final taskActions = find.byKey(const ValueKey('tasks-top-action-buttons'));
|
final taskActions = find.byKey(const ValueKey('tasks-top-action-buttons'));
|
||||||
final taskSearch = find.byKey(const ValueKey('tasks-search-field-container'));
|
final taskSearch = find.byKey(const ValueKey('tasks-search-field-container'));
|
||||||
final createButton = find.byKey(const ValueKey('tasks-create-button-container'));
|
final createButton = find.byKey(const ValueKey('tasks-create-button-container'));
|
||||||
@@ -411,6 +411,7 @@ void main() {
|
|||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(find.text('No matching tasks found'), findsOneWidget);
|
expect(find.text('No matching tasks found'), findsOneWidget);
|
||||||
|
expect(find.byKey(const ValueKey('tasks-empty-create-button')), findsNothing);
|
||||||
expect(find.text('No tasks in this list'), findsNothing);
|
expect(find.text('No tasks in this list'), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -437,6 +438,16 @@ void main() {
|
|||||||
expect(find.text('Downloading 0'), findsOneWidget);
|
expect(find.text('Downloading 0'), findsOneWidget);
|
||||||
expect(find.text('Completed 0'), findsOneWidget);
|
expect(find.text('Completed 0'), findsOneWidget);
|
||||||
expect(find.text('Failed 0'), findsOneWidget);
|
expect(find.text('Failed 0'), findsOneWidget);
|
||||||
|
expect(find.text('Create Task'), findsNWidgets(2));
|
||||||
|
final headerCreate = find.byKey(const ValueKey('tasks-mobile-create-button'));
|
||||||
|
expect(tester.getSize(headerCreate).height, greaterThanOrEqualTo(44));
|
||||||
|
expect(tester.getBottomLeft(headerCreate).dy, lessThan(tester.getTopLeft(find.text('Downloading 0')).dy));
|
||||||
|
for (final size in [const Size(320, 568), const Size(768, 1024)]) {
|
||||||
|
tester.view.physicalSize = size;
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
expect(find.byKey(const ValueKey('tasks-empty-create-button')), findsOneWidget);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('extensions grid adds columns from a minimum card width and keeps desktop toolbar aligned', (
|
testWidgets('extensions grid adds columns from a minimum card width and keeps desktop toolbar aligned', (
|
||||||
|
|||||||
Reference in new issue
Block a user