Files
guangya_flutter/lib/models/batch_rename.dart
T
2026-07-18 12:39:32 +08:00

155 lines
4.4 KiB
Dart

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<BatchRenamePreview> buildRenamePreviews(
List<CloudFile> files,
List<BatchRenameRule> rules, {
required bool preserveExtension,
}) {
final values = <BatchRenamePreview>[];
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 = <String, int>{};
final existing = <String, Set<String>>{};
for (final file in files) {
final key = _destinationKey(file.cloudPath, file.name);
(existing[key] ??= <String>{}).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()}';
}