import 'cloud_file.dart'; enum BatchRenameRuleKind { remove, replace, regex, prefix, suffix } enum BatchRenameItemType { all, files, folders } 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 = {}; final existing = >{}; for (final file in files) { final key = _destinationKey(file.cloudPath, file.name); (existing[key] ??= {}).add(file.id); } for (final item in values.where((item) => item.applicable)) { final key = _destinationKey(item.file.cloudPath, item.newName); proposed[key] = (proposed[key] ?? 0) + 1; } return values.map((item) { if (!item.applicable) return item; final key = _destinationKey(item.file.cloudPath, item.newName); if (proposed[key] != null && proposed[key]! > 1) { return BatchRenamePreview( file: item.file, newName: item.newName, error: '规则产生同名项目', ); } if (existing[key]?.any((id) => id != item.file.id) ?? false) { return BatchRenamePreview( file: item.file, newName: item.newName, error: '目标名称已存在', ); } return item; }).toList(); } String _destinationKey(String path, String name) { final separator = path.lastIndexOf('/'); final parent = separator < 0 ? '' : path.substring(0, separator); return '${parent.toLowerCase()}/${name.toLowerCase()}'; }