diff --git a/lib/pages/search_results_page.dart b/lib/pages/search_results_page.dart index 0b8ff46..6f707f9 100644 --- a/lib/pages/search_results_page.dart +++ b/lib/pages/search_results_page.dart @@ -116,6 +116,40 @@ class _FileSearchResultsPageState extends ConsumerState { }); } + Future _deleteFiles(List files) async { + if (files.isEmpty) return; + final confirmed = await showShadDialog( + 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 { ), 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 { onCopyFastTransfer: () => notifier.copyFastTransferJSON(file), onDownload: () => notifier.downloadFile(file), - onDelete: () => notifier.deleteFiles([file]), + onDelete: () => _deleteFiles([file]), ); }, ), diff --git a/lib/pages/workspace_tools_page.dart b/lib/pages/workspace_tools_page.dart index 1501e3d..ca2358e 100644 --- a/lib/pages/workspace_tools_page.dart +++ b/lib/pages/workspace_tools_page.dart @@ -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, + ), ), ), ],