import '../settings/settings_store.dart'; import '../subsonic/models.dart'; /// Session-scoped filter for a browse view: an optional genre and release year. /// Held in `state/providers.dart` (not persisted) so the app never silently /// reopens filtered. Sort order *is* persisted (see [AppSettings]). class BrowseFilter { const BrowseFilter({this.genre, this.year}); /// Case-insensitive genre match, or null for "all genres". final String? genre; /// Exact release-year match, or null for "all years". final int? year; bool get isActive => genre != null || year != null; int get activeCount => (genre != null ? 1 : 0) + (year != null ? 1 : 0); BrowseFilter copyWith({Object? genre = _unset, Object? year = _unset}) => BrowseFilter( genre: identical(genre, _unset) ? this.genre : genre as String?, year: identical(year, _unset) ? this.year : year as int?, ); static const Object _unset = Object(); } /// Distinct, display-cased genres from a set of raw genre strings, sorted /// alphabetically. Dedupes case-insensitively (keeping first-seen casing) so a /// server that mixes "Rock"/"rock" collapses to one entry. List distinctGenres(Iterable raw) { final byKey = {}; for (final g in raw) { final v = g?.trim(); if (v == null || v.isEmpty) continue; byKey.putIfAbsent(v.toLowerCase(), () => v); } final out = byKey.values.toList() ..sort((a, b) => a.toLowerCase().compareTo(b.toLowerCase())); return out; } /// Distinct release years present, newest first. List distinctYears(Iterable raw) { final set = {}; for (final y in raw) { if (y != null && y > 0) set.add(y); } final out = set.toList()..sort((a, b) => b.compareTo(a)); return out; } bool _genreMatches(String? itemGenre, String? filterGenre) { if (filterGenre == null) return true; return itemGenre != null && itemGenre.toLowerCase() == filterGenre.toLowerCase(); } int _byString(String? a, String? b) => (a ?? '').toLowerCase().compareTo((b ?? '').toLowerCase()); /// Compare where a null [a]/[b] always sorts *last*, regardless of [descending]. /// Takes bare [Comparable] so both `int` (`Comparable`) and `DateTime` work. int _nullsLast(Comparable? a, Comparable? b, {bool descending = false}) { if (a == null && b == null) return 0; if (a == null) return 1; if (b == null) return -1; final c = a.compareTo(b); return descending ? -c : c; } /// Filter then sort albums for the browse grid. Pure — no I/O. List applyAlbumQuery( List albums, BrowseFilter filter, AlbumSort sort, ) { final out = albums .where((a) => _genreMatches(a.genre, filter.genre)) .where((a) => filter.year == null || a.year == filter.year) .toList(); switch (sort) { case AlbumSort.nameAsc: out.sort((a, b) => _byString(a.name, b.name)); case AlbumSort.artistAsc: out.sort((a, b) { final c = _byString(a.artist, b.artist); return c != 0 ? c : _byString(a.name, b.name); }); case AlbumSort.yearDesc: out.sort((a, b) { final c = _nullsLast(a.year, b.year, descending: true); return c != 0 ? c : _byString(a.name, b.name); }); case AlbumSort.yearAsc: out.sort((a, b) { final c = _nullsLast(a.year, b.year); return c != 0 ? c : _byString(a.name, b.name); }); case AlbumSort.recentlyAdded: out.sort((a, b) { final c = _nullsLast(a.createdAt, b.createdAt, descending: true); return c != 0 ? c : _byString(a.name, b.name); }); } return out; } /// Filter then sort tracks for the browse list. Pure — no I/O. List applyTrackQuery( List songs, BrowseFilter filter, TrackSort sort, ) { final out = songs .where((s) => _genreMatches(s.genre, filter.genre)) .where((s) => filter.year == null || s.year == filter.year) .toList(); switch (sort) { case TrackSort.titleAsc: out.sort((a, b) => _byString(a.title, b.title)); case TrackSort.artistAsc: out.sort((a, b) { final c = _byString(a.artist, b.artist); return c != 0 ? c : _byString(a.title, b.title); }); case TrackSort.albumAsc: out.sort((a, b) { final c = _byString(a.album, b.album); if (c != 0) return c; final t = _nullsLast(a.track, b.track); return t != 0 ? t : _byString(a.title, b.title); }); case TrackSort.yearDesc: out.sort((a, b) { final c = _nullsLast(a.year, b.year, descending: true); return c != 0 ? c : _byString(a.title, b.title); }); case TrackSort.recentlyAdded: out.sort((a, b) { final c = _nullsLast(a.createdAt, b.createdAt, descending: true); return c != 0 ? c : _byString(a.title, b.title); }); } return out; }