init
This commit is contained in:
commit
d205277cdd
182 changed files with 22978 additions and 0 deletions
119
lib/settings/settings_store.dart
Normal file
119
lib/settings/settings_store.dart
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
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`.
|
||||
class AppSettings {
|
||||
const AppSettings({
|
||||
this.streamMaxBitRate = 0,
|
||||
this.downloadMaxBitRate = 0,
|
||||
this.downloadFormat,
|
||||
});
|
||||
|
||||
/// Cap for live streaming, in kbps. 0 = original / no transcode.
|
||||
final int streamMaxBitRate;
|
||||
|
||||
/// Cap for downloaded files, in kbps. 0 = original (best offline quality).
|
||||
final int downloadMaxBitRate;
|
||||
|
||||
/// Transcode container for downloads (`mp3`, `opus`, `aac`), or null to keep
|
||||
/// the original file / let the server decide.
|
||||
final String? downloadFormat;
|
||||
|
||||
/// 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 String bitrateLabel(int rate) => rate == 0 ? 'Original' : '$rate kbps';
|
||||
static String formatLabel(String? f) => f ?? 'Original';
|
||||
|
||||
AppSettings copyWith({
|
||||
int? streamMaxBitRate,
|
||||
int? downloadMaxBitRate,
|
||||
// Sentinel so an explicit null (→ original) is distinguishable from "unset".
|
||||
Object? downloadFormat = _unset,
|
||||
}) =>
|
||||
AppSettings(
|
||||
streamMaxBitRate: streamMaxBitRate ?? this.streamMaxBitRate,
|
||||
downloadMaxBitRate: downloadMaxBitRate ?? this.downloadMaxBitRate,
|
||||
downloadFormat: identical(downloadFormat, _unset)
|
||||
? this.downloadFormat
|
||||
: downloadFormat as String?,
|
||||
);
|
||||
|
||||
static const Object _unset = Object();
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'streamMaxBitRate': streamMaxBitRate,
|
||||
'downloadMaxBitRate': downloadMaxBitRate,
|
||||
if (downloadFormat != null) 'downloadFormat': downloadFormat,
|
||||
};
|
||||
|
||||
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?,
|
||||
);
|
||||
}
|
||||
|
||||
/// Loads settings on launch and persists every change (atomic temp+rename).
|
||||
class SettingsController extends StateNotifier<AppSettings> {
|
||||
SettingsController() : super(const AppSettings()) {
|
||||
_load();
|
||||
}
|
||||
|
||||
File? _file;
|
||||
|
||||
Future<void> _load() async {
|
||||
try {
|
||||
final dir = await getApplicationSupportDirectory();
|
||||
_file = File('${dir.path}/settings.json');
|
||||
if (await _file!.exists()) {
|
||||
final raw = jsonDecode(await _file!.readAsString());
|
||||
if (raw is Map) {
|
||||
state = AppSettings.fromJson(raw.cast<String, dynamic>());
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// Missing/corrupt settings are non-fatal — keep the defaults.
|
||||
}
|
||||
}
|
||||
|
||||
void setStreamMaxBitRate(int rate) {
|
||||
state = state.copyWith(streamMaxBitRate: rate);
|
||||
_persist();
|
||||
}
|
||||
|
||||
void setDownloadMaxBitRate(int rate) {
|
||||
state = state.copyWith(downloadMaxBitRate: rate);
|
||||
_persist();
|
||||
}
|
||||
|
||||
void setDownloadFormat(String? format) {
|
||||
state = state.copyWith(downloadFormat: format);
|
||||
_persist();
|
||||
}
|
||||
|
||||
Future<void> _persist() async {
|
||||
try {
|
||||
final file = _file;
|
||||
if (file == null) return;
|
||||
final tmp = File('${file.path}.tmp');
|
||||
await tmp.writeAsString(jsonEncode(state.toJson()));
|
||||
await tmp.rename(file.path);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
final settingsProvider =
|
||||
StateNotifierProvider<SettingsController, AppSettings>(
|
||||
(ref) => SettingsController(),
|
||||
);
|
||||
Loading…
Add table
Add a link
Reference in a new issue