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 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 = []; 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 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 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 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 get props => [start, padding]; const CounterNamingProcessor({ required this.start, required this.padding, }); @override String? process(AvesEntry entry, int index) => '${index + start}'.padLeft(padding, '0'); }