updates
This commit is contained in:
parent
981b4836f9
commit
ed910748cb
34 changed files with 2054 additions and 153 deletions
|
|
@ -1,19 +1,43 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/widgets.dart' show Color;
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
/// User-tunable audio quality. Streaming and offline downloads have independent
|
||||
/// knobs, both driven by Subsonic's `stream` transcode params (`maxBitRate` +
|
||||
/// `format`; a rate of 0 means original / no transcode). These are global (not
|
||||
/// per-server) and persisted to a single atomic JSON file, following the
|
||||
/// write pattern in `library/library_index.dart`.
|
||||
/// Top-level Browse mode selector. Lives here (not in `state/providers.dart`)
|
||||
/// so [AppSettings] can persist the user's default; `providers.dart` re-exports
|
||||
/// it, so existing importers are unaffected.
|
||||
enum BrowseMode { artists, albums, tracks }
|
||||
|
||||
/// Search matching strategy. `discovery` returns the server's full `search3`
|
||||
/// result (broad — e.g. songs matched via their artist name). `standard`
|
||||
/// post-filters to items whose *own* name/title matches the query.
|
||||
enum SearchMode { discovery, standard }
|
||||
|
||||
/// A selectable static accent color offered by the theme override (TODO #1).
|
||||
class AccentChoice {
|
||||
const AccentChoice(this.name, this.color);
|
||||
final String name;
|
||||
final Color color;
|
||||
}
|
||||
|
||||
/// Application-wide preferences: audio quality, theme accent, default browse
|
||||
/// view, and search mode. Streaming and offline downloads have independent
|
||||
/// quality knobs, both driven by Subsonic's `stream` transcode params
|
||||
/// (`maxBitRate` + `format`; a rate of 0 means original / no transcode). These
|
||||
/// are global (not per-server) and persisted to a single atomic JSON file,
|
||||
/// following the write pattern in `library/library_index.dart`.
|
||||
class AppSettings {
|
||||
const AppSettings({
|
||||
this.streamMaxBitRate = 0,
|
||||
this.downloadMaxBitRate = 0,
|
||||
this.downloadFormat,
|
||||
this.useStaticAccent = false,
|
||||
this.staticAccentColor = _defaultStaticAccent,
|
||||
this.defaultBrowseMode = BrowseMode.artists,
|
||||
this.searchMode = SearchMode.discovery,
|
||||
this.nowPlayingCassette = false,
|
||||
});
|
||||
|
||||
/// Cap for live streaming, in kbps. 0 = original / no transcode.
|
||||
|
|
@ -26,20 +50,67 @@ class AppSettings {
|
|||
/// the original file / let the server decide.
|
||||
final String? downloadFormat;
|
||||
|
||||
/// 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.
|
||||
final bool useStaticAccent;
|
||||
|
||||
/// The fixed accent used while [useStaticAccent] is on. One of
|
||||
/// [accentChoices]; defaults to Orange.
|
||||
final Color staticAccentColor;
|
||||
|
||||
/// Which sub-view the Browse tab opens on.
|
||||
final BrowseMode defaultBrowseMode;
|
||||
|
||||
/// Which search matching strategy to apply globally.
|
||||
final SearchMode searchMode;
|
||||
|
||||
/// 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.
|
||||
final bool nowPlayingCassette;
|
||||
|
||||
/// Offered bitrate choices (kbps); 0 renders as "Original".
|
||||
static const List<int> bitrateChoices = [0, 96, 128, 192, 256, 320];
|
||||
|
||||
/// Offered download containers; null renders as "Original".
|
||||
static const List<String?> formatChoices = [null, 'mp3', 'opus', 'aac'];
|
||||
|
||||
/// Static accent palette offered when the override is enabled (TODO #1).
|
||||
static const List<AccentChoice> accentChoices = [
|
||||
AccentChoice('Red', Color(0xFFDF3535)),
|
||||
AccentChoice('Blue', Color(0xFF7177EA)),
|
||||
AccentChoice('Green', Color(0xFF10996B)),
|
||||
AccentChoice('Orange', Color(0xFFDF7E35)),
|
||||
AccentChoice('Yellow', Color(0xFFDFC535)),
|
||||
AccentChoice('Purple', Color(0xFF926CE9)),
|
||||
AccentChoice('Cream', Color(0xFFFFDB9E)),
|
||||
];
|
||||
|
||||
static const Color _defaultStaticAccent = Color(0xFFDF7E35); // Orange
|
||||
|
||||
static String bitrateLabel(int rate) => rate == 0 ? 'Original' : '$rate kbps';
|
||||
static String formatLabel(String? f) => f ?? 'Original';
|
||||
static String browseModeLabel(BrowseMode m) => switch (m) {
|
||||
BrowseMode.artists => 'Artists',
|
||||
BrowseMode.albums => 'Albums',
|
||||
BrowseMode.tracks => 'Tracks',
|
||||
};
|
||||
static String searchModeLabel(SearchMode m) => switch (m) {
|
||||
SearchMode.standard => 'Standard',
|
||||
SearchMode.discovery => 'Discovery',
|
||||
};
|
||||
|
||||
AppSettings copyWith({
|
||||
int? streamMaxBitRate,
|
||||
int? downloadMaxBitRate,
|
||||
// Sentinel so an explicit null (→ original) is distinguishable from "unset".
|
||||
Object? downloadFormat = _unset,
|
||||
bool? useStaticAccent,
|
||||
Color? staticAccentColor,
|
||||
BrowseMode? defaultBrowseMode,
|
||||
SearchMode? searchMode,
|
||||
bool? nowPlayingCassette,
|
||||
}) =>
|
||||
AppSettings(
|
||||
streamMaxBitRate: streamMaxBitRate ?? this.streamMaxBitRate,
|
||||
|
|
@ -47,6 +118,11 @@ class AppSettings {
|
|||
downloadFormat: identical(downloadFormat, _unset)
|
||||
? this.downloadFormat
|
||||
: downloadFormat as String?,
|
||||
useStaticAccent: useStaticAccent ?? this.useStaticAccent,
|
||||
staticAccentColor: staticAccentColor ?? this.staticAccentColor,
|
||||
defaultBrowseMode: defaultBrowseMode ?? this.defaultBrowseMode,
|
||||
searchMode: searchMode ?? this.searchMode,
|
||||
nowPlayingCassette: nowPlayingCassette ?? this.nowPlayingCassette,
|
||||
);
|
||||
|
||||
static const Object _unset = Object();
|
||||
|
|
@ -55,13 +131,49 @@ class AppSettings {
|
|||
'streamMaxBitRate': streamMaxBitRate,
|
||||
'downloadMaxBitRate': downloadMaxBitRate,
|
||||
if (downloadFormat != null) 'downloadFormat': downloadFormat,
|
||||
'useStaticAccent': useStaticAccent,
|
||||
'staticAccentColor': _hexOf(staticAccentColor),
|
||||
'defaultBrowseMode': defaultBrowseMode.name,
|
||||
'searchMode': searchMode.name,
|
||||
'nowPlayingCassette': nowPlayingCassette,
|
||||
};
|
||||
|
||||
factory AppSettings.fromJson(Map<String, dynamic> j) => AppSettings(
|
||||
streamMaxBitRate: (j['streamMaxBitRate'] as num?)?.toInt() ?? 0,
|
||||
downloadMaxBitRate: (j['downloadMaxBitRate'] as num?)?.toInt() ?? 0,
|
||||
downloadFormat: j['downloadFormat'] as String?,
|
||||
useStaticAccent: j['useStaticAccent'] as bool? ?? false,
|
||||
staticAccentColor:
|
||||
_colorOf(j['staticAccentColor'] as String?) ?? _defaultStaticAccent,
|
||||
defaultBrowseMode:
|
||||
_enumByName(BrowseMode.values, j['defaultBrowseMode'] as String?) ??
|
||||
BrowseMode.artists,
|
||||
searchMode:
|
||||
_enumByName(SearchMode.values, j['searchMode'] as String?) ??
|
||||
SearchMode.discovery,
|
||||
nowPlayingCassette: j['nowPlayingCassette'] as bool? ?? false,
|
||||
);
|
||||
|
||||
/// Serialize a color to a `#RRGGBB` hex string.
|
||||
static String _hexOf(Color c) =>
|
||||
'#${(c.toARGB32() & 0xFFFFFF).toRadixString(16).padLeft(6, '0').toUpperCase()}';
|
||||
|
||||
/// Parse a `#RRGGBB` (or `RRGGBB`) hex string, or null if unparseable.
|
||||
static Color? _colorOf(String? hex) {
|
||||
if (hex == null) return null;
|
||||
final h = hex.startsWith('#') ? hex.substring(1) : hex;
|
||||
final v = int.tryParse(h, radix: 16);
|
||||
if (v == null || h.length != 6) return null;
|
||||
return Color(0xFF000000 | v);
|
||||
}
|
||||
|
||||
static T? _enumByName<T extends Enum>(List<T> values, String? name) {
|
||||
if (name == null) return null;
|
||||
for (final v in values) {
|
||||
if (v.name == name) return v;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads settings on launch and persists every change (atomic temp+rename).
|
||||
|
|
@ -102,6 +214,31 @@ class SettingsController extends StateNotifier<AppSettings> {
|
|||
_persist();
|
||||
}
|
||||
|
||||
void setUseStaticAccent(bool value) {
|
||||
state = state.copyWith(useStaticAccent: value);
|
||||
_persist();
|
||||
}
|
||||
|
||||
void setStaticAccentColor(Color color) {
|
||||
state = state.copyWith(staticAccentColor: color);
|
||||
_persist();
|
||||
}
|
||||
|
||||
void setDefaultBrowseMode(BrowseMode mode) {
|
||||
state = state.copyWith(defaultBrowseMode: mode);
|
||||
_persist();
|
||||
}
|
||||
|
||||
void setSearchMode(SearchMode mode) {
|
||||
state = state.copyWith(searchMode: mode);
|
||||
_persist();
|
||||
}
|
||||
|
||||
void setNowPlayingCassette(bool value) {
|
||||
state = state.copyWith(nowPlayingCassette: value);
|
||||
_persist();
|
||||
}
|
||||
|
||||
Future<void> _persist() async {
|
||||
try {
|
||||
final file = _file;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue