refine batch rename preview and search deletion

This commit is contained in:
ngfchl
2026-07-18 19:35:07 +08:00
parent 5a23f438f1
commit d21625e18d
2 changed files with 194 additions and 77 deletions
+36 -2
View File
@@ -116,6 +116,40 @@ class _FileSearchResultsPageState extends ConsumerState<FileSearchResultsPage> {
});
}
Future<void> _deleteFiles(List<CloudFile> files) async {
if (files.isEmpty) return;
final confirmed = await showShadDialog<bool>(
context: context,
builder: (dialogContext) => ShadDialog(
title: Text('删除 ${files.length} 项?'),
description: Text(
files.length == 1 ? files.first.name : '将删除所选的 ${files.length} 个项目。',
),
actions: [
ShadButton.outline(
onPressed: () => Navigator.of(dialogContext).pop(false),
child: const Text('取消'),
),
ShadButton.destructive(
onPressed: () => Navigator.of(dialogContext).pop(true),
child: const Text('删除'),
),
],
child: const Padding(
padding: EdgeInsets.only(top: 8),
child: Text('项目会被移入回收站。'),
),
),
);
if (confirmed != true) return;
await ref.read(fileProvider.notifier).deleteFiles(files);
if (!mounted) return;
setState(() {
_selectedIDs.removeAll(files.map((file) => file.id));
_results = _search();
});
}
@override
void didUpdateWidget(covariant FileSearchResultsPage oldWidget) {
super.didUpdateWidget(oldWidget);
@@ -245,7 +279,7 @@ class _FileSearchResultsPageState extends ConsumerState<FileSearchResultsPage> {
),
ShadButton.ghost(
size: ShadButtonSize.sm,
onPressed: () => notifier.deleteFiles(selected),
onPressed: () => _deleteFiles(selected),
leading: const Icon(
Icons.delete_outline_rounded,
size: 16,
@@ -288,7 +322,7 @@ class _FileSearchResultsPageState extends ConsumerState<FileSearchResultsPage> {
onCopyFastTransfer: () =>
notifier.copyFastTransferJSON(file),
onDownload: () => notifier.downloadFile(file),
onDelete: () => notifier.deleteFiles([file]),
onDelete: () => _deleteFiles([file]),
);
},
),
+158 -75
View File
@@ -1137,7 +1137,10 @@ class _BatchRenameToolState extends ConsumerState<_BatchRenameTool> {
description: '规则按从上到下的顺序应用。',
child: Padding(
padding: const EdgeInsets.only(top: 12),
child: Column(
child: Wrap(
spacing: 8,
runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
for (var index = 0; index < _rules.length; index++)
_BatchRenameRuleRow(
@@ -1160,34 +1163,28 @@ class _BatchRenameToolState extends ConsumerState<_BatchRenameTool> {
}
}),
),
const SizedBox(height: 10),
Row(
children: [
ShadButton.outline(
onPressed: () => setState(() {
_rules.add(
BatchRenameRule(
id: 'rule-${DateTime.now().microsecondsSinceEpoch}',
kind: BatchRenameRuleKind.replace,
),
);
}),
leading: const Icon(Icons.add_rounded, size: 16),
child: const Text('添加规则'),
),
const SizedBox(width: 8),
ShadButton.ghost(
onPressed: () => setState(() {
_rules = const [
BatchRenameRule(
id: 'rule-0',
kind: BatchRenameRuleKind.replace,
),
];
}),
child: const Text('清空规则'),
),
],
ShadButton.outline(
onPressed: () => setState(() {
_rules.add(
BatchRenameRule(
id: 'rule-${DateTime.now().microsecondsSinceEpoch}',
kind: BatchRenameRuleKind.replace,
),
);
}),
leading: const Icon(Icons.add_rounded, size: 16),
child: const Text('添加规则'),
),
ShadButton.ghost(
onPressed: () => setState(() {
_rules = const [
BatchRenameRule(
id: 'rule-0',
kind: BatchRenameRuleKind.replace,
),
];
}),
child: const Text('清空规则'),
),
],
),
@@ -1269,25 +1266,36 @@ class _BatchRenameToolState extends ConsumerState<_BatchRenameTool> {
),
),
)
: ListView.separated(
itemCount: previews.length,
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, index) {
final preview = previews[index];
final canSelect = preview.applicable;
return _BatchRenamePreviewRow(
preview: preview,
selected: _selectedIDs.contains(preview.file.id),
enabled: canSelect && !_running,
onChanged: (selected) => setState(() {
if (selected == true && canSelect) {
_selectedIDs.add(preview.file.id);
} else {
_selectedIDs.remove(preview.file.id);
}
}),
);
},
: Column(
children: [
const _BatchRenamePreviewHeader(),
const Divider(height: 1),
Expanded(
child: ListView.separated(
itemCount: previews.length,
separatorBuilder: (_, _) =>
const Divider(height: 1),
itemBuilder: (context, index) {
final preview = previews[index];
final canSelect = preview.applicable;
return _BatchRenamePreviewRow(
preview: preview,
selected: _selectedIDs.contains(
preview.file.id,
),
enabled: canSelect && !_running,
onChanged: (selected) => setState(() {
if (selected == true && canSelect) {
_selectedIDs.add(preview.file.id);
} else {
_selectedIDs.remove(preview.file.id);
}
}),
);
},
),
),
],
),
),
],
@@ -1644,6 +1652,37 @@ class _RenameRuleIconButton extends StatelessWidget {
}
}
class _BatchRenamePreviewHeader extends StatelessWidget {
const _BatchRenamePreviewHeader();
@override
Widget build(BuildContext context) {
final cs = ShadTheme.of(context).colorScheme;
final style = TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: cs.mutedForeground,
);
return Container(
height: 34,
padding: const EdgeInsets.symmetric(horizontal: 12),
color: cs.muted.withValues(alpha: 0.28),
child: Row(
children: [
const SizedBox(width: 64),
Expanded(flex: 3, child: Text('名称变更', style: style)),
Expanded(flex: 4, child: Text('完整路径', style: style)),
SizedBox(width: 270, child: Text('GCID', style: style)),
SizedBox(
width: 130,
child: Text('状态', textAlign: TextAlign.right, style: style),
),
],
),
);
}
}
class _BatchRenamePreviewRow extends StatelessWidget {
final BatchRenamePreview preview;
final bool selected;
@@ -1666,8 +1705,15 @@ class _BatchRenamePreviewRow extends StatelessWidget {
? preview.file.name
: (rawPath.startsWith('/') ? rawPath : '/$rawPath');
final gcid = preview.file.gcid?.trim();
final gcidText = gcid?.isNotEmpty == true ? gcid! : '未获取';
final status = preview.error ?? (changed ? '将修改' : '无变化');
final statusColor = preview.error != null
? cs.destructive
: changed
? cs.primary
: cs.mutedForeground;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
child: Row(
children: [
ShadCheckbox(value: selected, onChanged: enabled ? onChanged : null),
@@ -1681,41 +1727,75 @@ class _BatchRenamePreviewRow extends StatelessWidget {
),
const SizedBox(width: 10),
Expanded(
flex: 3,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
preview.file.name,
'${preview.file.name}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 12, color: cs.mutedForeground),
),
const SizedBox(height: 2),
const SizedBox(height: 4),
Text(
preview.newName,
'${preview.newName}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: preview.error != null
? cs.destructive
: changed
? cs.primary
: cs.mutedForeground,
fontWeight: FontWeight.w700,
color: statusColor,
),
),
const SizedBox(height: 4),
SelectableText(
'路径 $path',
maxLines: 1,
style: TextStyle(fontSize: 11, color: cs.mutedForeground),
],
),
),
const SizedBox(width: 14),
Expanded(
flex: 4,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 5),
decoration: BoxDecoration(
color: cs.muted.withValues(alpha: 0.48),
borderRadius: BorderRadius.circular(5),
),
child: Row(
children: [
Icon(
Icons.folder_outlined,
size: 14,
color: cs.mutedForeground,
),
const SizedBox(width: 5),
Expanded(
child: SelectableText(
path,
maxLines: 1,
style: TextStyle(fontSize: 11, color: cs.mutedForeground),
),
),
],
),
),
),
const SizedBox(width: 14),
SizedBox(
width: 270,
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'GCID',
style: TextStyle(fontSize: 10, color: cs.mutedForeground),
),
const SizedBox(height: 2),
const SizedBox(height: 3),
SelectableText(
'GCID ${gcid?.isNotEmpty == true ? gcid : '未获取'}',
gcidText,
maxLines: 1,
textAlign: TextAlign.right,
style: TextStyle(
fontFamily: 'monospace',
fontSize: 11,
color: gcid?.isNotEmpty == true
? cs.mutedForeground
@@ -1725,16 +1805,19 @@ class _BatchRenamePreviewRow extends StatelessWidget {
],
),
),
const SizedBox(width: 10),
Text(
preview.error ?? (changed ? '将修改' : '无变化'),
style: TextStyle(
fontSize: 12,
color: preview.error != null
? cs.destructive
: changed
? cs.primary
: cs.mutedForeground,
const SizedBox(width: 12),
SizedBox(
width: 118,
child: Text(
status,
textAlign: TextAlign.right,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: statusColor,
),
),
),
],