mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-12 11:24:08 +00:00
feat(ui): emphasize task creation and add empty-list actions (#1505)
This commit is contained in:
28 files changed
+216
-68
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_delete_dialog.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_update_url_dialog.dart';
|
||||
import '../widgets/primary_rail.dart';
|
||||
@@ -172,7 +173,10 @@ class _HomePageState extends ConsumerState<HomePage> {
|
||||
loading: () => const Center(child: shad.CircularProgressIndicator()),
|
||||
error: (error, _) => const TaskEmptyState(),
|
||||
data: (_) => filteredTasks.isEmpty
|
||||
? TaskEmptyState(message: emptyStateMessage)
|
||||
? TaskEmptyState(
|
||||
message: emptyStateMessage,
|
||||
onCreateTask: _searchController.text.trim().isEmpty ? _handleAddTask : null,
|
||||
)
|
||||
: ListView.separated(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
itemCount: filteredTasks.length,
|
||||
@@ -260,6 +264,7 @@ class _HomePageState extends ConsumerState<HomePage> {
|
||||
data: (_) => _MobileTasksView(
|
||||
tasks: filteredTasks,
|
||||
emptyStateMessage: emptyStateMessage,
|
||||
onCreateTask: _searchController.text.trim().isEmpty ? _handleAddTask : null,
|
||||
batchMode: _batchMode,
|
||||
batchSelection: _batchSelection,
|
||||
onToggleSelectAll: _toggleSelectAll,
|
||||
@@ -685,6 +690,7 @@ class _MobileTasksView extends StatelessWidget {
|
||||
required this.contextActionsForTask,
|
||||
required this.pendingUpdateTaskId,
|
||||
this.emptyStateMessage,
|
||||
this.onCreateTask,
|
||||
this.loading = false,
|
||||
});
|
||||
|
||||
@@ -705,6 +711,7 @@ class _MobileTasksView extends StatelessWidget {
|
||||
final TaskContextActions Function(TaskRecord task, bool selected, bool allSelected) contextActionsForTask;
|
||||
final String? pendingUpdateTaskId;
|
||||
final String? emptyStateMessage;
|
||||
final VoidCallback? onCreateTask;
|
||||
final bool loading;
|
||||
|
||||
@override
|
||||
@@ -745,7 +752,7 @@ class _MobileTasksView extends StatelessWidget {
|
||||
child: loading
|
||||
? const Center(child: shad.CircularProgressIndicator())
|
||||
: tasks.isEmpty
|
||||
? TaskEmptyState(message: emptyStateMessage)
|
||||
? TaskEmptyState(message: emptyStateMessage, onCreateTask: onCreateTask)
|
||||
: ListView.separated(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 12),
|
||||
itemCount: tasks.length,
|
||||
@@ -814,9 +821,19 @@ class _MobileTasksHeader extends StatelessWidget {
|
||||
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: 34,
|
||||
compact: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_MobileHeaderIconButton(
|
||||
key: const ValueKey('tasks-mobile-batch-button'),
|
||||
icon: Icons.checklist_rtl_outlined,
|
||||
active: batchMode,
|
||||
badge: batchMode && selectedCount > 0 ? selectedCount.toString() : null,
|
||||
@@ -903,7 +920,13 @@ class _MobileBatchToolbar extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _MobileHeaderIconButton extends StatelessWidget {
|
||||
const _MobileHeaderIconButton({required this.icon, required this.onPressed, this.active = false, this.badge});
|
||||
const _MobileHeaderIconButton({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.onPressed,
|
||||
this.active = false,
|
||||
this.badge,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
|
||||
|
||||
import '../../../../shared/theme/app_design_tokens.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 '../../../../l10n/l10n.dart';
|
||||
|
||||
@@ -62,7 +62,7 @@ class TasksTopBar extends StatelessWidget {
|
||||
ConstrainedBox(
|
||||
key: const ValueKey('tasks-create-button-container'),
|
||||
constraints: const BoxConstraints(minWidth: 120, maxWidth: 190),
|
||||
child: _AddTaskButton(onPressed: onAddTask),
|
||||
child: TaskCreateButton(onPressed: onAddTask),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
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 {
|
||||
const _ActionIconButton({required this.icon, required this.onPressed});
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
|
||||
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, this.compact = false});
|
||||
|
||||
final VoidCallback onPressed;
|
||||
final double minHeight;
|
||||
final bool compact;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(minHeight: minHeight),
|
||||
child: AppPrimaryButton(
|
||||
onPressed: onPressed,
|
||||
size: compact ? shad.ButtonSize.small : shad.ButtonSize.normal,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.add, size: compact ? 16 : null),
|
||||
SizedBox(width: compact ? AppDesignTokens.space4 : AppDesignTokens.space8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
context.l10n.create,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: compact ? const TextStyle(fontSize: 13) : null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
|
||||
import '../../../../l10n/l10n.dart';
|
||||
import '../../../../shared/theme/app_palette.dart';
|
||||
import '../../../../shared/widgets/app_tooltip.dart';
|
||||
|
||||
/// Keeps the action inside the localized sentence, including RTL word order.
|
||||
class TaskEmptyCreateHint extends StatelessWidget {
|
||||
const TaskEmptyCreateHint({super.key, required this.onCreateTask});
|
||||
|
||||
final VoidCallback onCreateTask;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const actionMarker = '\uFFFC';
|
||||
final parts = context.l10n.emptyTaskCreateHint(actionMarker).split(actionMarker);
|
||||
return Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(text: parts.first.trimRight()),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: AppTooltip(
|
||||
message: context.l10n.create,
|
||||
child: Semantics(
|
||||
label: context.l10n.create,
|
||||
child: SizedBox.square(
|
||||
dimension: 30,
|
||||
child: IconButton.outline(
|
||||
key: const ValueKey('tasks-empty-create-button'),
|
||||
density: ButtonDensity.compact,
|
||||
onPressed: onCreateTask,
|
||||
icon: const Icon(Icons.add, size: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
TextSpan(text: parts.last.trimLeft()),
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: AppPalette.of(context).textMuted, fontSize: 13),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,45 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import '../../../../shared/theme/app_palette.dart';
|
||||
import '../../../../shared/theme/app_design_tokens.dart';
|
||||
import 'task_empty_create_hint.dart';
|
||||
import '../../../../l10n/l10n.dart';
|
||||
|
||||
class TaskEmptyState extends StatelessWidget {
|
||||
const TaskEmptyState({super.key, this.message});
|
||||
const TaskEmptyState({super.key, this.message, this.onCreateTask});
|
||||
|
||||
final String? message;
|
||||
final VoidCallback? onCreateTask;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final palette = AppPalette.of(context);
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 360),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CustomPaint(
|
||||
key: const ValueKey('task-empty-illustration'),
|
||||
size: const Size(148, 108),
|
||||
painter: _TaskEmptyIllustrationPainter(palette: palette),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
Text(
|
||||
message ?? context.l10n.emptyTaskList,
|
||||
style: TextStyle(color: palette.textPrimary, fontSize: 17, fontWeight: FontWeight.w700),
|
||||
),
|
||||
],
|
||||
child: SingleChildScrollView(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 360),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CustomPaint(
|
||||
key: const ValueKey('task-empty-illustration'),
|
||||
size: const Size(148, 108),
|
||||
painter: _TaskEmptyIllustrationPainter(palette: palette),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
Text(
|
||||
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),
|
||||
TaskEmptyCreateHint(onCreateTask: onCreateTask!),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in new issue
Block a user