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 bitrateChoices = [0, 96, 128, 192, 256, 320]; /// Offered download containers; null renders as "Original". static const List 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 toJson() => { 'streamMaxBitRate': streamMaxBitRate, 'downloadMaxBitRate': downloadMaxBitRate, if (downloadFormat != null) 'downloadFormat': downloadFormat, }; factory AppSettings.fromJson(Map 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 { SettingsController() : super(const AppSettings()) { _load(); } File? _file; Future _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()); } } } 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 _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( (ref) => SettingsController(), );