This commit is contained in:
Forrest 2026-08-04 16:08:51 -04:00
parent d558aba246
commit 3bd713d667
17 changed files with 1566 additions and 132 deletions

View file

@ -15,6 +15,12 @@ enum BrowseMode { artists, albums, tracks }
/// post-filters to items whose *own* name/title matches the query.
enum SearchMode { discovery, standard }
/// Ordering applied to the Albums browse grid (filtered client-side).
enum AlbumSort { nameAsc, artistAsc, yearDesc, yearAsc, recentlyAdded }
/// Ordering applied to the Tracks browse list (filtered client-side).
enum TrackSort { titleAsc, artistAsc, albumAsc, yearDesc, recentlyAdded }
/// A selectable static accent color offered by the theme override (TODO #1).
class AccentChoice {
const AccentChoice(this.name, this.color);
@ -33,10 +39,13 @@ class AppSettings {
this.streamMaxBitRate = 0,
this.downloadMaxBitRate = 0,
this.downloadFormat,
this.maxConcurrentDownloads = defaultConcurrentDownloads,
this.useStaticAccent = false,
this.staticAccentColor = _defaultStaticAccent,
this.defaultBrowseMode = BrowseMode.artists,
this.searchMode = SearchMode.discovery,
this.albumSort = AlbumSort.nameAsc,
this.trackSort = TrackSort.titleAsc,
this.nowPlayingCassette = false,
});
@ -50,6 +59,11 @@ class AppSettings {
/// the original file / let the server decide.
final String? downloadFormat;
/// How many tracks may download at once. Re-read live by the download pump
/// (see `downloads/download_manager.dart`) so changes apply mid-session.
/// Clamped to [[minConcurrentDownloads], [maxConcurrentDownloadsCap]].
final int maxConcurrentDownloads;
/// When true, ignore album-art-derived accent and use [staticAccentColor]
/// as the sole accent color. When false (default), the accent tracks the
/// currently-playing album art.
@ -65,6 +79,11 @@ class AppSettings {
/// Which search matching strategy to apply globally.
final SearchMode searchMode;
/// Persisted sort order for the Albums / Tracks browse views. (Filters —
/// genre/year — are session-only and live in `state/providers.dart`.)
final AlbumSort albumSort;
final TrackSort trackSort;
/// When true, the Now Playing "art" view shows the animated cassette (cover
/// art on the label, reels driven by playback) instead of the plain square
/// album cover.
@ -76,6 +95,16 @@ class AppSettings {
/// Offered download containers; null renders as "Original".
static const List<String?> formatChoices = [null, 'mp3', 'opus', 'aac'];
/// Bounds and default for [maxConcurrentDownloads].
static const int minConcurrentDownloads = 1;
static const int maxConcurrentDownloadsCap = 10;
static const int defaultConcurrentDownloads = 3;
/// Clamp any value into the allowed concurrent-download range.
static int clampConcurrentDownloads(int v) => v < minConcurrentDownloads
? minConcurrentDownloads
: (v > maxConcurrentDownloadsCap ? maxConcurrentDownloadsCap : v);
/// Static accent palette offered when the override is enabled (TODO #1).
static const List<AccentChoice> accentChoices = [
AccentChoice('Red', Color(0xFFDF3535)),
@ -100,16 +129,33 @@ class AppSettings {
SearchMode.standard => 'Standard',
SearchMode.discovery => 'Discovery',
};
static String albumSortLabel(AlbumSort s) => switch (s) {
AlbumSort.nameAsc => 'Name',
AlbumSort.artistAsc => 'Artist',
AlbumSort.yearDesc => 'Year (newest)',
AlbumSort.yearAsc => 'Year (oldest)',
AlbumSort.recentlyAdded => 'Recently added',
};
static String trackSortLabel(TrackSort s) => switch (s) {
TrackSort.titleAsc => 'Title',
TrackSort.artistAsc => 'Artist',
TrackSort.albumAsc => 'Album',
TrackSort.yearDesc => 'Year (newest)',
TrackSort.recentlyAdded => 'Recently added',
};
AppSettings copyWith({
int? streamMaxBitRate,
int? downloadMaxBitRate,
// Sentinel so an explicit null (→ original) is distinguishable from "unset".
Object? downloadFormat = _unset,
int? maxConcurrentDownloads,
bool? useStaticAccent,
Color? staticAccentColor,
BrowseMode? defaultBrowseMode,
SearchMode? searchMode,
AlbumSort? albumSort,
TrackSort? trackSort,
bool? nowPlayingCassette,
}) =>
AppSettings(
@ -118,10 +164,14 @@ class AppSettings {
downloadFormat: identical(downloadFormat, _unset)
? this.downloadFormat
: downloadFormat as String?,
maxConcurrentDownloads:
maxConcurrentDownloads ?? this.maxConcurrentDownloads,
useStaticAccent: useStaticAccent ?? this.useStaticAccent,
staticAccentColor: staticAccentColor ?? this.staticAccentColor,
defaultBrowseMode: defaultBrowseMode ?? this.defaultBrowseMode,
searchMode: searchMode ?? this.searchMode,
albumSort: albumSort ?? this.albumSort,
trackSort: trackSort ?? this.trackSort,
nowPlayingCassette: nowPlayingCassette ?? this.nowPlayingCassette,
);
@ -131,10 +181,13 @@ class AppSettings {
'streamMaxBitRate': streamMaxBitRate,
'downloadMaxBitRate': downloadMaxBitRate,
if (downloadFormat != null) 'downloadFormat': downloadFormat,
'maxConcurrentDownloads': maxConcurrentDownloads,
'useStaticAccent': useStaticAccent,
'staticAccentColor': _hexOf(staticAccentColor),
'defaultBrowseMode': defaultBrowseMode.name,
'searchMode': searchMode.name,
'albumSort': albumSort.name,
'trackSort': trackSort.name,
'nowPlayingCassette': nowPlayingCassette,
};
@ -142,6 +195,9 @@ class AppSettings {
streamMaxBitRate: (j['streamMaxBitRate'] as num?)?.toInt() ?? 0,
downloadMaxBitRate: (j['downloadMaxBitRate'] as num?)?.toInt() ?? 0,
downloadFormat: j['downloadFormat'] as String?,
maxConcurrentDownloads: clampConcurrentDownloads(
(j['maxConcurrentDownloads'] as num?)?.toInt() ??
defaultConcurrentDownloads),
useStaticAccent: j['useStaticAccent'] as bool? ?? false,
staticAccentColor:
_colorOf(j['staticAccentColor'] as String?) ?? _defaultStaticAccent,
@ -151,6 +207,10 @@ class AppSettings {
searchMode:
_enumByName(SearchMode.values, j['searchMode'] as String?) ??
SearchMode.discovery,
albumSort: _enumByName(AlbumSort.values, j['albumSort'] as String?) ??
AlbumSort.nameAsc,
trackSort: _enumByName(TrackSort.values, j['trackSort'] as String?) ??
TrackSort.titleAsc,
nowPlayingCassette: j['nowPlayingCassette'] as bool? ?? false,
);
@ -214,6 +274,13 @@ class SettingsController extends StateNotifier<AppSettings> {
_persist();
}
void setMaxConcurrentDownloads(int count) {
state = state.copyWith(
maxConcurrentDownloads:
AppSettings.clampConcurrentDownloads(count));
_persist();
}
void setUseStaticAccent(bool value) {
state = state.copyWith(useStaticAccent: value);
_persist();
@ -234,6 +301,16 @@ class SettingsController extends StateNotifier<AppSettings> {
_persist();
}
void setAlbumSort(AlbumSort sort) {
state = state.copyWith(albumSort: sort);
_persist();
}
void setTrackSort(TrackSort sort) {
state = state.copyWith(trackSort: sort);
_persist();
}
void setNowPlayingCassette(bool value) {
state = state.copyWith(nowPlayingCassette: value);
_persist();