Files
SpotiFLAC-Mobile/lib/providers/equalizer_provider.dart
T

190 lines
5.6 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:async';
import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotiflac_android/models/equalizer_settings.dart';
import 'package:spotiflac_android/services/playback_equalizer.dart';
class EqualizerState {
const EqualizerState({
this.settings = const EqualizerSettings(),
this.presets = const [],
this.error,
});
final EqualizerSettings settings;
final List<EqualizerPreset> presets;
final String? error;
}
final equalizerProvider = NotifierProvider<EqualizerController, EqualizerState>(
EqualizerController.new,
);
class EqualizerController extends Notifier<EqualizerState> {
static const storageKey = 'playback_equalizer_v1';
int _revision = 0;
bool _disposed = false;
String? _pending;
bool _saving = false;
Timer? _saveTimer;
late Future<void> _loading;
final _deletedPresets = <String>{};
@override
EqualizerState build() {
ref.onDispose(() {
_disposed = true;
_saveTimer?.cancel();
if (!_saving && _pending != null) unawaited(_save());
});
_loading = _load();
return const EqualizerState();
}
Future<void> _load() async {
final revision = _revision;
try {
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getString(storageKey);
if (_disposed || raw == null) return;
final data = jsonDecode(raw) as Map<String, dynamic>;
final settings = EqualizerSettings.fromJson(
Map<String, dynamic>.from(data['settings'] as Map),
);
final presets = (data['presets'] as List? ?? const [])
.take(100)
.map(
(v) =>
EqualizerPreset.fromJson(Map<String, dynamic>.from(v as Map)),
)
.toList(growable: false);
if (revision == _revision) {
state = EqualizerState(settings: settings, presets: presets);
} else {
final merged = {
for (final preset in presets) preset.name: preset,
for (final preset in state.presets) preset.name: preset,
};
for (final name in _deletedPresets) {
merged.remove(name);
}
state = EqualizerState(
settings: state.settings,
presets: merged.values.take(100).toList(),
);
_pending = _serialize();
}
playbackEqualizer.value = state.settings;
} catch (_) {
if (!_disposed && revision == _revision) {
state = const EqualizerState(
error: 'Could not read saved equalizer settings.',
);
}
}
}
void update(EqualizerSettings settings) => _commit(settings, state.presets);
void applyPreset(EqualizerPreset preset) =>
update(preset.settings.copyWith(enabled: state.settings.enabled));
void savePreset(String name) {
final trimmed = name.trim();
if (trimmed.isEmpty || trimmed.length > 80) {
throw const FormatException('Preset name must contain 1–80 characters.');
}
final next = state.presets.where((v) => v.name != trimmed).toList();
if (next.length >= 100) {
throw const FormatException('Maximum 100 custom presets.');
}
_deletedPresets.remove(trimmed);
_commit(state.settings, [
...next,
EqualizerPreset(trimmed, state.settings),
]);
}
void deletePreset(String name) {
_deletedPresets.add(name);
_commit(
state.settings,
state.presets.where((v) => v.name != name).toList(),
);
}
String exportPreset(String name) =>
const JsonEncoder.withIndent(' ').convert({
'format': 'spotiflac-equalizer',
'version': 1,
...EqualizerPreset(name, state.settings).toJson(),
});
void importPreset(String source) {
if (source.length > 65536) {
throw const FormatException('Preset file is too large.');
}
final decoded = jsonDecode(source);
if (decoded is! Map ||
decoded['format'] != 'spotiflac-equalizer' ||
decoded['version'] != 1) {
throw const FormatException('Unsupported equalizer preset.');
}
final preset = EqualizerPreset.fromJson(Map<String, dynamic>.from(decoded));
final others = state.presets.where((v) => v.name != preset.name).toList();
if (others.length >= 100) {
throw const FormatException('Maximum 100 custom presets.');
}
_deletedPresets.remove(preset.name);
_commit(preset.settings.copyWith(enabled: state.settings.enabled), [
...others,
preset,
]);
}
void _commit(EqualizerSettings settings, List<EqualizerPreset> presets) {
_revision++;
state = EqualizerState(
settings: settings,
presets: List.unmodifiable(presets),
);
playbackEqualizer.value = settings;
_pending = _serialize();
_saveTimer?.cancel();
_saveTimer = Timer(const Duration(milliseconds: 350), () {
if (!_saving) unawaited(_save());
});
}
Future<void> _save() async {
_saving = true;
try {
await _loading;
final prefs = await SharedPreferences.getInstance();
while (_pending != null) {
final value = _pending!;
_pending = null;
if (!await prefs.setString(storageKey, value)) {
throw StateError('Write failed');
}
}
} catch (_) {
if (!_disposed) {
state = EqualizerState(
settings: state.settings,
presets: state.presets,
error: 'Could not save equalizer settings.',
);
}
} finally {
_saving = false;
}
}
String _serialize() => jsonEncode({
'settings': state.settings.toJson(),
'presets': state.presets.map((v) => v.toJson()).toList(),
});
}