diff --git a/lib/screens/album_screen.dart b/lib/screens/album_screen.dart index ca4afefb..a315da94 100644 --- a/lib/screens/album_screen.dart +++ b/lib/screens/album_screen.dart @@ -269,6 +269,7 @@ class _AlbumScreenState extends ConsumerState } static final _qualitySeparators = RegExp(r'[\s_-]+'); + static final _releaseYearPattern = RegExp(r'(\d{4})'); static (String, bool?) _qualitySignature(Track track) => ( (track.audioQuality ?? '').trim().toLowerCase().replaceAll( @@ -360,7 +361,7 @@ class _AlbumScreenState extends ConsumerState String? _releaseYear(String? date) { if (date == null || date.isEmpty) return null; - final match = RegExp(r'(\d{4})').firstMatch(date); + final match = _releaseYearPattern.firstMatch(date); return match?.group(1); } diff --git a/lib/screens/queue_tab_sections.dart b/lib/screens/queue_tab_sections.dart index 216739e4..d1c7c820 100644 --- a/lib/screens/queue_tab_sections.dart +++ b/lib/screens/queue_tab_sections.dart @@ -13,6 +13,15 @@ class _LibrarySection { int get length => end - start; } +final _letterPattern = RegExp(r'[\p{L}]', unicode: true); + +final _monthYearFormats = {}; + +DateFormat _monthYearFormat(BuildContext context) { + final locale = Localizations.localeOf(context).toLanguageTag(); + return _monthYearFormats.putIfAbsent(locale, () => DateFormat.yMMMM(locale)); +} + /// First letter of [text] for an alphabetical index; digits and symbols /// share `#`. String _sectionInitial(String text) { @@ -20,7 +29,7 @@ String _sectionInitial(String text) { for (final character in trimmed.characters) { final upper = character.toUpperCase(); if (upper != character.toLowerCase()) return upper; - if (RegExp(r'[\p{L}]', unicode: true).hasMatch(character)) return upper; + if (_letterPattern.hasMatch(character)) return upper; return '#'; } return '#'; @@ -79,9 +88,7 @@ extension _QueueTabSections on _QueueTabState { return _sectionInitial(item.albumName); } if (_dateSorts.contains(_sortMode)) { - return DateFormat.yMMMM( - Localizations.localeOf(context).toLanguageTag(), - ).format(item.addedAt); + return _monthYearFormat(context).format(item.addedAt); } return null; } @@ -98,9 +105,7 @@ extension _QueueTabSections on _QueueTabState { } if (_artistSorts.contains(_sortMode)) return _sectionInitial(artist); if (_dateSorts.contains(_sortMode)) { - return DateFormat.yMMMM( - Localizations.localeOf(context).toLanguageTag(), - ).format(added); + return _monthYearFormat(context).format(added); } return null; } diff --git a/lib/screens/track_metadata_screen_display.dart b/lib/screens/track_metadata_screen_display.dart index c9987ad4..656ee44e 100644 --- a/lib/screens/track_metadata_screen_display.dart +++ b/lib/screens/track_metadata_screen_display.dart @@ -3,6 +3,8 @@ part of 'track_metadata_screen.dart'; // Display helpers: quality/bitrate labels, service track-id formatting, // snackbars, and path formatting. +final _spotifyTrackIdPattern = RegExp(r'^[A-Za-z0-9]{22}$'); + extension _TrackMetadataDisplay on _TrackMetadataScreenState { bool _isBitrateFormatValue(String? value) { return const { @@ -61,7 +63,7 @@ extension _TrackMetadataDisplay on _TrackMetadataScreenState { String _displayServiceTrackId(String value) { final raw = value.trim(); if (raw.isEmpty) return raw; - final spotifyTrackIdPattern = RegExp(r'^[A-Za-z0-9]{22}$'); + final spotifyTrackIdPattern = _spotifyTrackIdPattern; if (raw.startsWith('deezer:')) return raw.substring('deezer:'.length); if (raw.startsWith('tidal:')) return raw.substring('tidal:'.length); @@ -91,7 +93,7 @@ extension _TrackMetadataDisplay on _TrackMetadataScreenState { String _serviceForTrackId(String value, {required String fallbackService}) { final raw = value.trim(); if (raw.isEmpty) return fallbackService; - final spotifyTrackIdPattern = RegExp(r'^[A-Za-z0-9]{22}$'); + final spotifyTrackIdPattern = _spotifyTrackIdPattern; if (raw.startsWith('deezer:')) return MusicServices.deezer; if (raw.startsWith('tidal:')) return MusicServices.tidal; diff --git a/lib/widgets/aligned_lyric_pronunciation.dart b/lib/widgets/aligned_lyric_pronunciation.dart index 827f3220..ea6a8107 100644 --- a/lib/widgets/aligned_lyric_pronunciation.dart +++ b/lib/widgets/aligned_lyric_pronunciation.dart @@ -15,6 +15,9 @@ class LyricPronunciationLayout { static const _phraseGap = 4.0; static const _pronunciationGap = 2.0; static const _rowGap = 6.0; + static final _whitespaceRun = RegExp(r'\s+'); + static final _endsWithWhitespace = RegExp(r'\s$'); + static final _startsWithWhitespace = RegExp(r'^\s'); double get primaryHeight => _rows.fold(0.0, (height, row) => height + row.primaryHeight) + @@ -42,7 +45,7 @@ class LyricPronunciationLayout { maxWidth <= 0) { return null; } - String normalize(String text) => text.replaceAll(RegExp(r'\s+'), ''); + String normalize(String text) => text.replaceAll(_whitespaceRun, ''); if (normalize(original.map((word) => word.text).join()) != normalize(line.text) || normalize(pronunciation.map((word) => word.text).join()) != @@ -57,8 +60,8 @@ class LyricPronunciationLayout { for (var i = 1; i < pronunciation.length; i++) { // Preserve syllables inside a word. Providers may split a romanized // word into several timed spans without adding whitespace. - if (!RegExp(r'\s$').hasMatch(pronunciation[i - 1].text) && - !RegExp(r'^\s').hasMatch(pronunciation[i].text)) { + if (!_endsWithWhitespace.hasMatch(pronunciation[i - 1].text) && + !_startsWithWhitespace.hasMatch(pronunciation[i].text)) { continue; } final start = pronunciation[i].time.inMilliseconds; diff --git a/lib/widgets/audio_info_sheet.dart b/lib/widgets/audio_info_sheet.dart index 867cc623..f032ee27 100644 --- a/lib/widgets/audio_info_sheet.dart +++ b/lib/widgets/audio_info_sheet.dart @@ -671,9 +671,11 @@ String _peak(double value) { return '${value.toStringAsFixed(6)} ยท ${_db(dbfs).replaceFirst(' dB', ' dBFS')}'; } +final _numberPattern = RegExp(r'[-+]?\d+(?:\.\d+)?'); + /// Reads tag values such as "-6.52 dB" or "0.988831". double? _number(Object? raw) { if (raw is num) return raw.toDouble(); - final match = RegExp(r'[-+]?\d+(?:\.\d+)?').firstMatch(raw?.toString() ?? ''); + final match = _numberPattern.firstMatch(raw?.toString() ?? ''); return match == null ? null : double.tryParse(match[0]!); }