xworkmate-app/lib/features/settings/settings_archived_tasks_panel.dart

348 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import '../../i18n/app_language.dart';
import '../../runtime/runtime_models.dart';
import '../../theme/app_palette.dart';
class SettingsArchivedTasksPanel extends StatelessWidget {
const SettingsArchivedTasksPanel({
super.key,
required this.sessions,
required this.onRestore,
required this.onDelete,
});
final List<GatewaySessionSummary> sessions;
final Future<void> Function(String sessionKey) onRestore;
final Future<void> Function(String sessionKey) onDelete;
@override
Widget build(BuildContext context) {
final palette = context.palette;
final theme = Theme.of(context);
return Column(
key: const ValueKey('settings-archived-tasks-panel'),
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Icon(Icons.inventory_2_outlined, color: palette.textSecondary),
const SizedBox(width: 10),
Expanded(
child: Text(
appText('归档任务管理', 'Archived task management'),
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
),
),
),
Text(
appText('${sessions.length}', '${sessions.length} items'),
style: theme.textTheme.bodySmall?.copyWith(
color: palette.textMuted,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 12),
if (sessions.isEmpty)
_ArchivedTasksEmptyState()
else
Column(
children: [
for (final session in sessions) ...[
_ArchivedTaskTile(
session: session,
onRestore: () => onRestore(session.key),
onDelete: () async {
final confirmed = await _confirmDelete(context, session);
if (confirmed) {
await onDelete(session.key);
}
},
),
if (session != sessions.last)
Divider(height: 1, color: palette.strokeSoft),
],
],
),
],
);
}
Future<bool> _confirmDelete(
BuildContext context,
GatewaySessionSummary session,
) async {
final palette = context.palette;
final result = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(appText('彻底删除归档记录', 'Delete archived record')),
content: Text(
appText(
'将从 XWorkmate 中删除「${session.label}」的任务记录、消息状态和本地线程工作目录。此操作不可撤销。',
'This removes "${session.label}" from XWorkmate task records, message state, and the local thread workspace. This cannot be undone.',
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(appText('取消', 'Cancel')),
),
FilledButton.icon(
key: const ValueKey('settings-archived-task-confirm-delete'),
onPressed: () => Navigator.of(context).pop(true),
style: FilledButton.styleFrom(
backgroundColor: palette.danger,
foregroundColor: Colors.white,
),
icon: const Icon(Icons.delete_outline_rounded),
label: Text(appText('彻底删除', 'Delete permanently')),
),
],
),
);
if (result != true || !context.mounted) {
return false;
}
return _confirmDeleteWithYes(context, session);
}
Future<bool> _confirmDeleteWithYes(
BuildContext context,
GatewaySessionSummary session,
) async {
final palette = context.palette;
final result = await showDialog<bool>(
context: context,
builder: (context) =>
_DeleteYesConfirmationDialog(session: session, palette: palette),
);
return result ?? false;
}
}
class _DeleteYesConfirmationDialog extends StatefulWidget {
const _DeleteYesConfirmationDialog({
required this.session,
required this.palette,
});
final GatewaySessionSummary session;
final AppPalette palette;
@override
State<_DeleteYesConfirmationDialog> createState() =>
_DeleteYesConfirmationDialogState();
}
class _DeleteYesConfirmationDialogState
extends State<_DeleteYesConfirmationDialog> {
final TextEditingController _confirmationController = TextEditingController();
bool get _confirmed => _confirmationController.text.trim() == 'Yes';
@override
void dispose() {
_confirmationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(appText('确认彻底删除', 'Confirm permanent delete')),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
appText(
'此操作会删除「${widget.session.label}」的归档记录和任务目录。请输入 Yes 继续。',
'This deletes "${widget.session.label}" archived records and task directory. Type Yes to continue.',
),
),
const SizedBox(height: 14),
TextField(
key: const ValueKey('settings-archived-task-delete-yes-input'),
controller: _confirmationController,
autofocus: true,
onChanged: (_) => setState(() {}),
decoration: InputDecoration(
labelText: appText('输入 Yes', 'Type Yes'),
border: const OutlineInputBorder(),
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(appText('取消', 'Cancel')),
),
FilledButton.icon(
key: const ValueKey('settings-archived-task-confirm-delete-yes'),
onPressed: _confirmed ? () => Navigator.of(context).pop(true) : null,
style: FilledButton.styleFrom(
backgroundColor: widget.palette.danger,
foregroundColor: Colors.white,
disabledBackgroundColor: widget.palette.strokeSoft,
disabledForegroundColor: widget.palette.textMuted,
),
icon: const Icon(Icons.delete_forever_outlined),
label: Text(appText('彻底删除', 'Delete permanently')),
),
],
);
}
}
class _ArchivedTasksEmptyState extends StatelessWidget {
@override
Widget build(BuildContext context) {
final palette = context.palette;
final theme = Theme.of(context);
return Container(
key: const ValueKey('settings-archived-tasks-empty'),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
decoration: BoxDecoration(
color: palette.surfaceSecondary,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: palette.strokeSoft),
),
child: Column(
children: [
Icon(Icons.archive_outlined, size: 28, color: palette.textMuted),
const SizedBox(height: 8),
Text(
appText('暂无归档任务', 'No archived tasks'),
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
),
),
],
),
);
}
}
class _ArchivedTaskTile extends StatelessWidget {
const _ArchivedTaskTile({
required this.session,
required this.onRestore,
required this.onDelete,
});
final GatewaySessionSummary session;
final Future<void> Function() onRestore;
final Future<void> Function() onDelete;
@override
Widget build(BuildContext context) {
final palette = context.palette;
final theme = Theme.of(context);
final preview = session.lastMessagePreview?.trim() ?? '';
return Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: palette.surfaceSecondary,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: palette.strokeSoft),
),
child: Icon(
Icons.archive_outlined,
color: palette.textSecondary,
size: 18,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
session.label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
),
),
if (preview.isNotEmpty) ...[
const SizedBox(height: 3),
Text(
preview,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith(
color: palette.textMuted,
),
),
],
const SizedBox(height: 4),
Text(
_archivedTaskUpdatedAtLabel(session.updatedAtMs),
style: theme.textTheme.bodySmall?.copyWith(
color: palette.textMuted,
),
),
],
),
),
const SizedBox(width: 12),
Wrap(
spacing: 8,
runSpacing: 8,
alignment: WrapAlignment.end,
children: [
FilledButton.tonalIcon(
key: ValueKey<String>(
'settings-archived-task-restore-${session.key}',
),
onPressed: () async {
await onRestore();
},
icon: const Icon(Icons.unarchive_outlined),
label: Text(appText('解除归档', 'Restore')),
),
IconButton(
key: ValueKey<String>(
'settings-archived-task-delete-${session.key}',
),
tooltip: appText('彻底删除归档记录', 'Delete archived record'),
onPressed: () async {
await onDelete();
},
icon: const Icon(Icons.delete_outline_rounded),
),
],
),
],
),
);
}
}
String _archivedTaskUpdatedAtLabel(double? updatedAtMs) {
if (updatedAtMs == null) {
return appText('无更新时间', 'No update time');
}
final timestamp = DateTime.fromMillisecondsSinceEpoch(updatedAtMs.round());
final date =
'${timestamp.year.toString().padLeft(4, '0')}-'
'${timestamp.month.toString().padLeft(2, '0')}-'
'${timestamp.day.toString().padLeft(2, '0')}';
final time =
'${timestamp.hour.toString().padLeft(2, '0')}:'
'${timestamp.minute.toString().padLeft(2, '0')}';
return appText('归档前更新于 $date $time', 'Updated before archive $date $time');
}