aves/lib/model/naming_pattern.dart

178 lines
5 KiB
Dart

import 'package:aves/model/entry/entry.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:intl/intl.dart';
@immutable
class NamingPattern {
final List<NamingProcessor> processors;
static final processorPattern = RegExp(r'<(.+?)(,(.+?))?>');
static const processorOptionSeparator = ',';
static const optionKeyValueSeparator = '=';
const NamingPattern(this.processors);
factory NamingPattern.from({
required String userPattern,
required int entryCount,
}) {
final processors = <NamingProcessor>[];
const defaultCounterStart = 1;
final defaultCounterPadding = '$entryCount'.length;
var index = 0;
final matches = processorPattern.allMatches(userPattern);
matches.forEach((match) {
final start = match.start;
final end = match.end;
if (index < start) {
processors.add(LiteralNamingProcessor(userPattern.substring(index, start)));
index = start;
}
final processorKey = match.group(1);
final processorOptions = match.group(3);
switch (processorKey) {
case DateNamingProcessor.key:
if (processorOptions != null) {
processors.add(DateNamingProcessor(processorOptions.trim()));
}
case NameNamingProcessor.key:
processors.add(const NameNamingProcessor());
case CounterNamingProcessor.key:
int? start, padding;
_applyProcessorOptions(processorOptions, (key, value) {
final valueInt = int.tryParse(value);
if (valueInt != null) {
switch (key) {
case CounterNamingProcessor.optionStart:
start = valueInt;
case CounterNamingProcessor.optionPadding:
padding = valueInt;
}
}
});
processors.add(CounterNamingProcessor(start: start ?? defaultCounterStart, padding: padding ?? defaultCounterPadding));
default:
debugPrint('unsupported naming processor: ${match.group(0)}');
}
index = end;
});
if (index < userPattern.length) {
processors.add(LiteralNamingProcessor(userPattern.substring(index, userPattern.length)));
}
return NamingPattern(processors);
}
static void _applyProcessorOptions(String? processorOptions, void Function(String key, String value) applyOption) {
if (processorOptions != null) {
processorOptions.split(processorOptionSeparator).map((v) => v.trim()).forEach((kv) {
final parts = kv.split(optionKeyValueSeparator);
if (parts.length >= 2) {
final key = parts[0];
final value = parts.skip(1).join(optionKeyValueSeparator);
applyOption(key, value);
}
});
}
}
static int getInsertionOffset(String userPattern, int offset) {
offset = offset.clamp(0, userPattern.length);
final matches = processorPattern.allMatches(userPattern);
for (final match in matches) {
final start = match.start;
final end = match.end;
if (offset <= start) return offset;
if (offset <= end) return end;
}
return offset;
}
static String defaultPatternFor(String processorKey) {
switch (processorKey) {
case DateNamingProcessor.key:
return '<$processorKey, yyyyMMdd-HHmmss>';
case CounterNamingProcessor.key:
case NameNamingProcessor.key:
default:
return '<$processorKey>';
}
}
String apply(AvesEntry entry, int index) => processors.map((v) => v.process(entry, index) ?? '').join().trimLeft();
}
@immutable
abstract class NamingProcessor extends Equatable {
const NamingProcessor();
String? process(AvesEntry entry, int index);
}
@immutable
class LiteralNamingProcessor extends NamingProcessor {
final String text;
@override
List<Object?> get props => [text];
const LiteralNamingProcessor(this.text);
@override
String? process(AvesEntry entry, int index) => text;
}
@immutable
class DateNamingProcessor extends NamingProcessor {
static const key = 'date';
final DateFormat format;
@override
List<Object?> get props => [format.pattern];
DateNamingProcessor(String pattern) : format = DateFormat(pattern);
@override
String? process(AvesEntry entry, int index) {
final date = entry.bestDate;
return date != null ? format.format(date) : null;
}
}
@immutable
class NameNamingProcessor extends NamingProcessor {
static const key = 'name';
@override
List<Object?> get props => [];
const NameNamingProcessor();
@override
String? process(AvesEntry entry, int index) => entry.filenameWithoutExtension;
}
@immutable
class CounterNamingProcessor extends NamingProcessor {
final int start;
final int padding;
static const key = 'counter';
static const optionStart = 'start';
static const optionPadding = 'padding';
@override
List<Object?> get props => [start, padding];
const CounterNamingProcessor({
required this.start,
required this.padding,
});
@override
String? process(AvesEntry entry, int index) => '${index + start}'.padLeft(padding, '0');
}