155 lines
4.4 KiB
Dart
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()}';
|
|
}
|