From e9003542325307d39428233db7cde11bbab7be05 Mon Sep 17 00:00:00 2001 From: ngfchl Date: Sat, 18 Jul 2026 11:36:23 +0800 Subject: [PATCH] feat: add batch rename rule engine --- lib/models/batch_rename.dart | 134 +++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 lib/models/batch_rename.dart diff --git a/lib/models/batch_rename.dart b/lib/models/batch_rename.dart new file mode 100644 index 0000000..57bd1be --- /dev/null +++ b/lib/models/batch_rename.dart @@ -0,0 +1,134 @@ +import 'cloud_file.dart'; + +enum BatchRenameRuleKind { remove, replace, regex, prefix, suffix } + +class BatchRenameRule { + final String id; + final bool enabled; + final BatchRenameRuleKind kind; + final String pattern; + final String replacement; + final bool ignoreCase; + + const BatchRenameRule({ + required this.id, + this.enabled = true, + this.kind = BatchRenameRuleKind.replace, + this.pattern = '', + this.replacement = '', + this.ignoreCase = false, + }); + + BatchRenameRule copyWith({ + bool? enabled, + BatchRenameRuleKind? kind, + String? pattern, + String? replacement, + bool? ignoreCase, + }) => BatchRenameRule( + id: id, + enabled: enabled ?? this.enabled, + kind: kind ?? this.kind, + pattern: pattern ?? this.pattern, + replacement: replacement ?? this.replacement, + ignoreCase: ignoreCase ?? this.ignoreCase, + ); + + String apply(String value) { + if (!enabled) return value; + switch (kind) { + case BatchRenameRuleKind.remove: + return pattern.isEmpty + ? value + : value.replaceAll( + RegExp(RegExp.escape(pattern), caseSensitive: !ignoreCase), + '', + ); + case BatchRenameRuleKind.replace: + return pattern.isEmpty + ? value + : value.replaceAll( + RegExp(RegExp.escape(pattern), caseSensitive: !ignoreCase), + replacement, + ); + case BatchRenameRuleKind.regex: + return pattern.isEmpty + ? value + : value.replaceAll( + RegExp(pattern, caseSensitive: !ignoreCase), + replacement, + ); + case BatchRenameRuleKind.prefix: + return '$pattern$value'; + case BatchRenameRuleKind.suffix: + return '$value$pattern'; + } + } +} + +class BatchRenamePreview { + final CloudFile file; + final String newName; + final String? error; + + const BatchRenamePreview({ + required this.file, + required this.newName, + this.error, + }); + bool get changed => file.name != newName; + bool get applicable => changed && error == null; +} + +List buildRenamePreviews( + List files, + List rules, { + required bool preserveExtension, +}) { + final values = []; + for (final file in files) { + try { + final dot = !file.isDirectory && preserveExtension + ? file.name.lastIndexOf('.') + : -1; + final base = dot > 0 ? file.name.substring(0, dot) : file.name; + final extension = dot > 0 ? file.name.substring(dot) : ''; + final renamed = + rules.fold(base, (value, rule) => rule.apply(value)) + extension; + final trimmed = renamed.trim(); + values.add( + BatchRenamePreview( + file: file, + newName: renamed, + error: + trimmed.isEmpty || + renamed.contains('/') || + renamed.contains('\u0000') + ? '名称包含非法字符或为空' + : null, + ), + ); + } on FormatException { + values.add( + BatchRenamePreview(file: file, newName: file.name, error: '正则表达式无效'), + ); + } + } + final proposed = {}; + for (final item in values.where((item) => item.applicable)) { + final key = + '${item.file.cloudPath.toLowerCase()}/${item.newName.toLowerCase()}'; + proposed[key] = (proposed[key] ?? 0) + 1; + } + return values.map((item) { + final key = + '${item.file.cloudPath.toLowerCase()}/${item.newName.toLowerCase()}'; + return proposed[key] != null && proposed[key]! > 1 + ? BatchRenamePreview( + file: item.file, + newName: item.newName, + error: '规则产生同名项目', + ) + : item; + }).toList(); +}