119 lines
3.8 KiB
Dart
119 lines
3.8 KiB
Dart
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(),
|
|
);
|