149 lines
4.8 KiB
Dart
149 lines
4.8 KiB
Dart
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<String> distinctGenres(Iterable<String?> raw) {
|
|
final byKey = <String, String>{};
|
|
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<int> distinctYears(Iterable<int?> raw) {
|
|
final set = <int>{};
|
|
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<num>`) 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<Album> applyAlbumQuery(
|
|
List<Album> 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<Song> applyTrackQuery(
|
|
List<Song> 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;
|
|
}
|