perf: optimize providers, throttle polling, queued settings save, remove dead screens

This commit is contained in:
zarzet
2026-02-07 16:37:54 +07:00
parent 35f412dbd2
commit 3d366d21b7
13 changed files with 1184 additions and 2637 deletions
+176 -62
View File
@@ -226,8 +226,11 @@ class DownloadHistoryState {
}
class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
static const int _safRepairBatchSize = 20;
static const int _safRepairMaxPerLaunch = 60;
final HistoryDatabase _db = HistoryDatabase.instance;
bool _isLoaded = false;
bool _isSafRepairInProgress = false;
@override
DownloadHistoryState build() {
@@ -267,8 +270,14 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
if (Platform.isAndroid) {
Future.microtask(() async {
await _repairMissingSafEntries(items);
await _repairMissingSafEntries(
items,
maxItems: _safRepairMaxPerLaunch,
);
await cleanupOrphanedDownloads();
});
} else {
Future.microtask(() => cleanupOrphanedDownloads());
}
} catch (e, stack) {
_historyLog.e('Failed to load history from database: $e', e, stack);
@@ -285,10 +294,16 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
return '';
}
Future<void> _repairMissingSafEntries(List<DownloadHistoryItem> items) async {
final updatedItems = [...items];
var changed = false;
Future<void> _repairMissingSafEntries(
List<DownloadHistoryItem> items, {
required int maxItems,
}) async {
if (_isSafRepairInProgress || items.isEmpty) {
return;
}
_isSafRepairInProgress = true;
final candidateIndexes = <int>[];
for (var i = 0; i < items.length; i++) {
final item = items[i];
if (item.storageMode != 'saf') continue;
@@ -299,46 +314,85 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
if (item.filePath.isEmpty || !isContentUri(item.filePath)) {
continue;
}
final exists = await fileExists(item.filePath);
if (exists) continue;
final fallbackName = item.safFileName ?? _fileNameFromUri(item.filePath);
if (fallbackName.isEmpty) {
_historyLog.w('Missing SAF filename for history item: ${item.id}');
continue;
}
try {
final resolved = await PlatformBridge.resolveSafFile(
treeUri: item.downloadTreeUri!,
relativeDir: item.safRelativeDir ?? '',
fileName: fallbackName,
);
final newUri = resolved['uri'] as String? ?? '';
if (newUri.isEmpty) continue;
final newRelativeDir = resolved['relative_dir'] as String?;
final updated = item.copyWith(
filePath: newUri,
safRelativeDir: (newRelativeDir != null && newRelativeDir.isNotEmpty)
? newRelativeDir
: item.safRelativeDir,
safFileName: fallbackName,
safRepaired: true,
);
updatedItems[i] = updated;
changed = true;
await _db.upsert(updated.toJson());
_historyLog.i('Repaired SAF URI for history item: ${item.id}');
} catch (e) {
_historyLog.w('Failed to repair SAF URI: $e');
}
candidateIndexes.add(i);
if (candidateIndexes.length >= maxItems) break;
}
if (changed) {
state = state.copyWith(items: updatedItems);
if (candidateIndexes.isEmpty) {
_isSafRepairInProgress = false;
return;
}
final updatedItems = [...items];
var changed = false;
var repairedCount = 0;
var verifiedCount = 0;
try {
for (var c = 0; c < candidateIndexes.length; c++) {
final i = candidateIndexes[c];
final item = items[i];
final exists = await fileExists(item.filePath);
if (exists) {
final verified = item.copyWith(
safRepaired: true,
safFileName: item.safFileName ?? _fileNameFromUri(item.filePath),
);
updatedItems[i] = verified;
changed = true;
verifiedCount++;
await _db.upsert(verified.toJson());
} else {
final fallbackName =
item.safFileName ?? _fileNameFromUri(item.filePath);
if (fallbackName.isEmpty) {
_historyLog.w('Missing SAF filename for history item: ${item.id}');
continue;
}
try {
final resolved = await PlatformBridge.resolveSafFile(
treeUri: item.downloadTreeUri!,
relativeDir: item.safRelativeDir ?? '',
fileName: fallbackName,
);
final newUri = resolved['uri'] as String? ?? '';
if (newUri.isEmpty) continue;
final newRelativeDir = resolved['relative_dir'] as String?;
final updated = item.copyWith(
filePath: newUri,
safRelativeDir:
(newRelativeDir != null && newRelativeDir.isNotEmpty)
? newRelativeDir
: item.safRelativeDir,
safFileName: fallbackName,
safRepaired: true,
);
updatedItems[i] = updated;
changed = true;
repairedCount++;
await _db.upsert(updated.toJson());
} catch (e) {
_historyLog.w('Failed to repair SAF URI: $e');
}
}
if ((c + 1) % _safRepairBatchSize == 0) {
await Future.delayed(const Duration(milliseconds: 16));
}
}
if (changed) {
state = state.copyWith(items: updatedItems);
_historyLog.i(
'SAF repair pass: verified=$verifiedCount, repaired=$repairedCount, checked=${candidateIndexes.length}',
);
}
} finally {
_isSafRepairInProgress = false;
}
}
@@ -412,18 +466,18 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
/// Returns the number of orphaned entries removed
Future<int> cleanupOrphanedDownloads() async {
_historyLog.i('Starting orphaned downloads cleanup...');
final entries = await _db.getAllEntriesWithPaths();
final orphanedIds = <String>[];
for (final entry in entries) {
final id = entry['id'] as String;
final filePath = entry['file_path'] as String?;
if (filePath == null || filePath.isEmpty) continue;
bool exists = false;
if (filePath.startsWith('content://')) {
// SAF path - check via platform bridge
try {
@@ -436,31 +490,33 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
// Regular file path
exists = File(filePath).existsSync();
}
if (!exists) {
orphanedIds.add(id);
_historyLog.d('Found orphaned entry: $id ($filePath)');
}
}
if (orphanedIds.isEmpty) {
_historyLog.i('No orphaned entries found');
return 0;
}
// Delete from database
final deletedCount = await _db.deleteByIds(orphanedIds);
// Update in-memory state
final orphanedSet = orphanedIds.toSet();
state = state.copyWith(
items: state.items.where((item) => !orphanedSet.contains(item.id)).toList(),
items: state.items
.where((item) => !orphanedSet.contains(item.id))
.toList(),
);
_historyLog.i('Cleaned up $deletedCount orphaned entries');
return deletedCount;
}
void clearHistory() {
state = DownloadHistoryState();
_db.clearAll().catchError((e) {
@@ -557,6 +613,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
int _downloadCount = 0;
static const _cleanupInterval = 50;
static const _queueStorageKey = 'download_queue';
static const _progressPollingInterval = Duration(milliseconds: 800);
final NotificationService _notificationService = NotificationService();
final Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
int _totalQueuedAtStart = 0;
@@ -564,6 +621,12 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
int _failedInSession = 0;
bool _isLoaded = false;
final Set<String> _ensuredDirs = {};
int _progressPollingErrorCount = 0;
String? _lastServiceTrackName;
String? _lastServiceArtistName;
int _lastServicePercent = -1;
int _lastServiceQueueCount = -1;
DateTime _lastServiceUpdateAt = DateTime.fromMillisecondsSinceEpoch(0);
@override
DownloadQueueState build() {
@@ -647,9 +710,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
void _startMultiProgressPolling() {
_progressTimer?.cancel();
_progressTimer = Timer.periodic(const Duration(milliseconds: 500), (
timer,
) async {
_progressTimer = Timer.periodic(_progressPollingInterval, (timer) async {
try {
final allProgress = await PlatformBridge.getAllDownloadProgress();
final items = allProgress['items'] as Map<String, dynamic>? ?? {};
@@ -818,23 +879,76 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
);
if (Platform.isAndroid) {
PlatformBridge.updateDownloadServiceProgress(
_maybeUpdateAndroidDownloadService(
trackName: firstDownloading.track.name,
artistName: firstDownloading.track.artistName,
progress: notifProgress,
total: notifTotal > 0 ? notifTotal : 1,
queueCount: queuedCount,
).catchError((_) {});
);
}
}
}
} catch (_) {}
_progressPollingErrorCount = 0;
} catch (e) {
_progressPollingErrorCount++;
if (_progressPollingErrorCount <= 3) {
_log.w('Progress polling failed: $e');
}
}
});
}
void _maybeUpdateAndroidDownloadService({
required String trackName,
required String artistName,
required int progress,
required int total,
required int queueCount,
}) {
final now = DateTime.now();
final safeTotal = total > 0 ? total : 1;
final progressPercent = ((progress * 100) / safeTotal)
.round()
.clamp(0, 100)
.toInt();
final didContentChange =
trackName != _lastServiceTrackName ||
artistName != _lastServiceArtistName ||
queueCount != _lastServiceQueueCount ||
progressPercent != _lastServicePercent;
final allowHeartbeat =
now.difference(_lastServiceUpdateAt) >= const Duration(seconds: 5);
if (!didContentChange && !allowHeartbeat) {
return;
}
_lastServiceTrackName = trackName;
_lastServiceArtistName = artistName;
_lastServicePercent = progressPercent;
_lastServiceQueueCount = queueCount;
_lastServiceUpdateAt = now;
PlatformBridge.updateDownloadServiceProgress(
trackName: trackName,
artistName: artistName,
progress: progress,
total: safeTotal,
queueCount: queueCount,
).catchError((_) {});
}
void _stopProgressPolling() {
_progressTimer?.cancel();
_progressTimer = null;
_progressPollingErrorCount = 0;
_lastServiceTrackName = null;
_lastServiceArtistName = null;
_lastServicePercent = -1;
_lastServiceQueueCount = -1;
_lastServiceUpdateAt = DateTime.fromMillisecondsSinceEpoch(0);
}
Future<void> _initOutputDir() async {
@@ -2241,7 +2355,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
while (true) {
if (state.isPaused) {
_log.d('Queue is paused, waiting...');
await Future.delayed(const Duration(milliseconds: 500));
await Future.delayed(_progressPollingInterval);
continue;
}
@@ -2280,7 +2394,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
if (activeDownloads.isNotEmpty) {
await Future.any(activeDownloads.values);
} else {
await Future.delayed(const Duration(milliseconds: 500));
await Future.delayed(_progressPollingInterval);
}
continue;
}
+121 -65
View File
@@ -36,16 +36,16 @@ class LocalLibraryState {
this.scanErrorCount = 0,
this.scanWasCancelled = false,
this.lastScannedAt,
}) : _isrcSet = items
.where((item) => item.isrc != null && item.isrc!.isNotEmpty)
.map((item) => item.isrc!)
.toSet(),
_trackKeySet = items.map((item) => item.matchKey).toSet(),
_byIsrc = Map.fromEntries(
items
.where((item) => item.isrc != null && item.isrc!.isNotEmpty)
.map((item) => MapEntry(item.isrc!, item)),
);
}) : _isrcSet = items
.where((item) => item.isrc != null && item.isrc!.isNotEmpty)
.map((item) => item.isrc!)
.toSet(),
_trackKeySet = items.map((item) => item.matchKey).toSet(),
_byIsrc = Map.fromEntries(
items
.where((item) => item.isrc != null && item.isrc!.isNotEmpty)
.map((item) => MapEntry(item.isrc!, item)),
);
bool hasIsrc(String isrc) => _isrcSet.contains(isrc);
@@ -99,9 +99,11 @@ class LocalLibraryState {
class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
final LibraryDatabase _db = LibraryDatabase.instance;
final HistoryDatabase _historyDb = HistoryDatabase.instance;
static const _progressPollingInterval = Duration(milliseconds: 800);
Timer? _progressTimer;
bool _isLoaded = false;
bool _scanCancelRequested = false;
int _progressPollingErrorCount = 0;
@override
LocalLibraryState build() {
@@ -121,10 +123,8 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
try {
final jsonList = await _db.getAll();
final items = jsonList
.map((e) => LocalLibraryItem.fromJson(e))
.toList();
final items = jsonList.map((e) => LocalLibraryItem.fromJson(e)).toList();
DateTime? lastScannedAt;
try {
final prefs = await SharedPreferences.getInstance();
@@ -135,9 +135,11 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
} catch (e) {
_log.w('Failed to load lastScannedAt: $e');
}
state = state.copyWith(items: items, lastScannedAt: lastScannedAt);
_log.i('Loaded ${items.length} items from library database, lastScannedAt: $lastScannedAt');
_log.i(
'Loaded ${items.length} items from library database, lastScannedAt: $lastScannedAt',
);
} catch (e, stack) {
_log.e('Failed to load library from database: $e', e, stack);
}
@@ -148,14 +150,19 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
await _loadFromDatabase();
}
Future<void> startScan(String folderPath, {bool forceFullScan = false}) async {
Future<void> startScan(
String folderPath, {
bool forceFullScan = false,
}) async {
if (state.isScanning) {
_log.w('Scan already in progress');
return;
}
_scanCancelRequested = false;
_log.i('Starting library scan: $folderPath (incremental: ${!forceFullScan})');
_log.i(
'Starting library scan: $folderPath (incremental: ${!forceFullScan})',
);
state = state.copyWith(
isScanning: true,
scanProgress: 0,
@@ -179,11 +186,13 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
try {
final isSaf = folderPath.startsWith('content://');
// Get all file paths from download history to exclude them
final downloadedPaths = await _historyDb.getAllFilePaths();
_log.i('Excluding ${downloadedPaths.length} downloaded files from library scan');
_log.i(
'Excluding ${downloadedPaths.length} downloaded files from library scan',
);
if (forceFullScan) {
// Full scan path - ignores existing data
final results = isSaf
@@ -193,7 +202,7 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
state = state.copyWith(isScanning: false, scanWasCancelled: true);
return;
}
final items = <LocalLibraryItem>[];
int skippedDownloads = 0;
for (final json in results) {
@@ -206,7 +215,7 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
final item = LocalLibraryItem.fromJson(json);
items.add(item);
}
if (skippedDownloads > 0) {
_log.i('Skipped $skippedDownloads files already in download history');
}
@@ -234,7 +243,9 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
} else {
// Incremental scan path - only scans new/modified files
final existingFiles = await _db.getFileModTimes();
_log.i('Incremental scan: ${existingFiles.length} existing files in database');
_log.i(
'Incremental scan: ${existingFiles.length} existing files in database',
);
final backfilledModTimes = await _backfillLegacyFileModTimes(
isSaf: isSaf,
@@ -245,7 +256,7 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
existingFiles.addAll(backfilledModTimes);
_log.i('Backfilled ${backfilledModTimes.length} legacy mod times');
}
// Use appropriate incremental scan method based on SAF or not
final Map<String, dynamic> result;
if (isSaf) {
@@ -259,63 +270,76 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
existingFiles,
);
}
if (_scanCancelRequested) {
state = state.copyWith(isScanning: false, scanWasCancelled: true);
return;
}
// Parse incremental scan result
// SAF returns 'files' and 'removedUris', non-SAF returns 'scanned' and 'deletedPaths'
final scannedList = (result['files'] as List<dynamic>?)
?? (result['scanned'] as List<dynamic>?)
?? [];
final deletedPaths = (result['removedUris'] as List<dynamic>?)
?.map((e) => e as String)
.toList()
?? (result['deletedPaths'] as List<dynamic>?)
final scannedList =
(result['files'] as List<dynamic>?) ??
(result['scanned'] as List<dynamic>?) ??
[];
final deletedPaths =
(result['removedUris'] as List<dynamic>?)
?.map((e) => e as String)
.toList()
?? [];
.toList() ??
(result['deletedPaths'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
[];
final skippedCount = result['skippedCount'] as int? ?? 0;
final totalFiles = result['totalFiles'] as int? ?? 0;
_log.i('Incremental result: ${scannedList.length} scanned, '
'$skippedCount skipped, ${deletedPaths.length} deleted, $totalFiles total');
_log.i(
'Incremental result: ${scannedList.length} scanned, '
'$skippedCount skipped, ${deletedPaths.length} deleted, $totalFiles total',
);
final currentByPath = <String, LocalLibraryItem>{
for (final item in state.items) item.filePath: item,
};
// Upsert new/modified items (excluding downloaded files)
final updatedItems = <LocalLibraryItem>[];
int skippedDownloads = 0;
if (scannedList.isNotEmpty) {
final items = <LocalLibraryItem>[];
int skippedDownloads = 0;
for (final json in scannedList) {
final map = json as Map<String, dynamic>;
final filePath = map['filePath'] as String?;
// Skip files that are already in download history
if (filePath != null && downloadedPaths.contains(filePath)) {
skippedDownloads++;
continue;
}
items.add(LocalLibraryItem.fromJson(map));
final item = LocalLibraryItem.fromJson(map);
updatedItems.add(item);
currentByPath[item.filePath] = item;
}
if (items.isNotEmpty) {
await _db.upsertBatch(items.map((e) => e.toJson()).toList());
_log.i('Upserted ${items.length} items');
if (updatedItems.isNotEmpty) {
await _db.upsertBatch(updatedItems.map((e) => e.toJson()).toList());
_log.i('Upserted ${updatedItems.length} items');
}
if (skippedDownloads > 0) {
_log.i('Skipped $skippedDownloads files already in download history');
_log.i(
'Skipped $skippedDownloads files already in download history',
);
}
}
// Delete removed items
if (deletedPaths.isNotEmpty) {
final deleteCount = await _db.deleteByPaths(deletedPaths);
for (final path in deletedPaths) {
currentByPath.remove(path);
}
_log.i('Deleted $deleteCount items from database');
}
// Reload all items from database to get complete list
final allItems = await _db.getAll();
final items = allItems.map((e) => LocalLibraryItem.fromJson(e)).toList();
final items = currentByPath.values.toList(growable: false)
..sort(_compareLibraryItems);
final now = DateTime.now();
try {
final prefs = await SharedPreferences.getInstance();
@@ -333,8 +357,10 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
scanWasCancelled: false,
);
_log.i('Incremental scan complete: ${items.length} total tracks '
'(${scannedList.length} new/updated, $skippedCount unchanged, ${deletedPaths.length} removed)');
_log.i(
'Incremental scan complete: ${items.length} total tracks '
'(${scannedList.length} new/updated, $skippedCount unchanged, ${deletedPaths.length} removed)',
);
}
} catch (e, stack) {
_log.e('Library scan failed: $e', e, stack);
@@ -346,10 +372,10 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
void _startProgressPolling() {
_progressTimer?.cancel();
_progressTimer = Timer.periodic(const Duration(milliseconds: 500), (_) async {
_progressTimer = Timer.periodic(_progressPollingInterval, (_) async {
try {
final progress = await PlatformBridge.getLibraryScanProgress();
state = state.copyWith(
scanProgress: (progress['progress_pct'] as num?)?.toDouble() ?? 0,
scanCurrentFile: progress['current_file'] as String?,
@@ -361,18 +387,25 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
if (progress['is_complete'] == true) {
_stopProgressPolling();
}
} catch (_) {}
_progressPollingErrorCount = 0;
} catch (e) {
_progressPollingErrorCount++;
if (_progressPollingErrorCount <= 3) {
_log.w('Library scan progress polling failed: $e');
}
}
});
}
void _stopProgressPolling() {
_progressTimer?.cancel();
_progressTimer = null;
_progressPollingErrorCount = 0;
}
Future<void> cancelScan() async {
if (!state.isScanning) return;
_log.i('Cancelling library scan');
_scanCancelRequested = true;
await PlatformBridge.cancelLibraryScan();
@@ -390,14 +423,14 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
Future<void> clearLibrary() async {
await _db.clearAll();
try {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_lastScannedAtKey);
} catch (e) {
_log.w('Failed to clear lastScannedAt: $e');
}
state = LocalLibraryState();
_log.i('Library cleared');
}
@@ -421,7 +454,11 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
return state.getByIsrc(isrc);
}
LocalLibraryItem? findExisting({String? isrc, String? trackName, String? artistName}) {
LocalLibraryItem? findExisting({
String? isrc,
String? trackName,
String? artistName,
}) {
if (isrc != null && isrc.isNotEmpty) {
final byIsrc = state.getByIsrc(isrc);
if (byIsrc != null) return byIsrc;
@@ -434,7 +471,7 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
Future<List<LocalLibraryItem>> search(String query) async {
if (query.isEmpty) return [];
final results = await _db.search(query);
return results.map((e) => LocalLibraryItem.fromJson(e)).toList();
}
@@ -443,6 +480,23 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
return await _db.getCount();
}
int _compareLibraryItems(LocalLibraryItem a, LocalLibraryItem b) {
final artistA = (a.albumArtist ?? a.artistName).toLowerCase();
final artistB = (b.albumArtist ?? b.artistName).toLowerCase();
final artistCompare = artistA.compareTo(artistB);
if (artistCompare != 0) return artistCompare;
final albumCompare = a.albumName.toLowerCase().compareTo(
b.albumName.toLowerCase(),
);
if (albumCompare != 0) return albumCompare;
final discCompare = (a.discNumber ?? 0).compareTo(b.discNumber ?? 0);
if (discCompare != 0) return discCompare;
return (a.trackNumber ?? 0).compareTo(b.trackNumber ?? 0);
}
Future<Map<String, int>> _backfillLegacyFileModTimes({
required bool isSaf,
required Map<String, int> existingFiles,
@@ -469,7 +523,9 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
if (_scanCancelRequested) {
break;
}
final end = (i + chunkSize < uris.length) ? i + chunkSize : uris.length;
final end = (i + chunkSize < uris.length)
? i + chunkSize
: uris.length;
final chunk = uris.sublist(i, end);
final chunkResult = await PlatformBridge.getSafFileModTimes(chunk);
backfilled.addAll(chunkResult);
+45 -18
View File
@@ -10,10 +10,14 @@ const _settingsKey = 'app_settings';
const _migrationVersionKey = 'settings_migration_version';
const _currentMigrationVersion = 2;
const _spotifyClientSecretKey = 'spotify_client_secret';
final _log = AppLogger('SettingsProvider');
class SettingsNotifier extends Notifier<AppSettings> {
final Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
final FlutterSecureStorage _secureStorage = const FlutterSecureStorage();
bool _isSavingSettings = false;
bool _saveQueued = false;
String? _pendingSettingsJson;
@override
AppSettings build() {
@@ -26,27 +30,27 @@ class SettingsNotifier extends Notifier<AppSettings> {
final json = prefs.getString(_settingsKey);
if (json != null) {
state = AppSettings.fromJson(jsonDecode(json));
await _runMigrations(prefs);
}
await _loadSpotifyClientSecret(prefs);
_applySpotifyCredentials();
LogBuffer.loggingEnabled = state.enableLogging;
}
Future<void> _runMigrations(SharedPreferences prefs) async {
final lastMigration = prefs.getInt(_migrationVersionKey) ?? 0;
if (lastMigration < 1) {
if (!state.useCustomSpotifyCredentials) {
state = state.copyWith(metadataSource: 'deezer');
await _saveSettings();
}
}
if (lastMigration < _currentMigrationVersion) {
if (state.downloadTreeUri.isNotEmpty && state.storageMode != 'saf') {
state = state.copyWith(storageMode: 'saf');
@@ -61,20 +65,43 @@ class SettingsNotifier extends Notifier<AppSettings> {
}
Future<void> _saveSettings() async {
final prefs = await _prefs;
final settingsToSave = state.copyWith(
spotifyClientSecret: '',
);
await prefs.setString(_settingsKey, jsonEncode(settingsToSave.toJson()));
final settingsToSave = state.copyWith(spotifyClientSecret: '');
_pendingSettingsJson = jsonEncode(settingsToSave.toJson());
if (_isSavingSettings) {
_saveQueued = true;
return;
}
_isSavingSettings = true;
try {
final prefs = await _prefs;
do {
final jsonToWrite = _pendingSettingsJson;
_saveQueued = false;
if (jsonToWrite != null) {
await prefs.setString(_settingsKey, jsonToWrite);
}
} while (_saveQueued);
} catch (e) {
_log.e('Failed to save settings: $e');
} finally {
_isSavingSettings = false;
}
}
Future<void> _loadSpotifyClientSecret(SharedPreferences prefs) async {
final storedSecret = await _secureStorage.read(key: _spotifyClientSecretKey);
final storedSecret = await _secureStorage.read(
key: _spotifyClientSecretKey,
);
final prefsSecret = state.spotifyClientSecret;
if ((storedSecret == null || storedSecret.isEmpty) &&
prefsSecret.isNotEmpty) {
await _secureStorage.write(key: _spotifyClientSecretKey, value: prefsSecret);
await _secureStorage.write(
key: _spotifyClientSecretKey,
value: prefsSecret,
);
}
final effectiveSecret = (storedSecret != null && storedSecret.isNotEmpty)
@@ -99,7 +126,7 @@ class SettingsNotifier extends Notifier<AppSettings> {
}
Future<void> _applySpotifyCredentials() async {
if (state.spotifyClientId.isNotEmpty &&
if (state.spotifyClientId.isNotEmpty &&
state.spotifyClientSecret.isNotEmpty) {
await PlatformBridge.setSpotifyCredentials(
state.spotifyClientId,
@@ -225,7 +252,10 @@ class SettingsNotifier extends Notifier<AppSettings> {
_saveSettings();
}
Future<void> setSpotifyCredentials(String clientId, String clientSecret) async {
Future<void> setSpotifyCredentials(
String clientId,
String clientSecret,
) async {
state = state.copyWith(
spotifyClientId: clientId,
spotifyClientSecret: clientSecret,
@@ -236,10 +266,7 @@ class SettingsNotifier extends Notifier<AppSettings> {
}
Future<void> clearSpotifyCredentials() async {
state = state.copyWith(
spotifyClientId: '',
spotifyClientSecret: '',
);
state = state.copyWith(spotifyClientId: '', spotifyClientSecret: '');
await _storeSpotifyClientSecret('');
_saveSettings();
_applySpotifyCredentials();
@@ -301,7 +328,7 @@ class SettingsNotifier extends Notifier<AppSettings> {
_saveSettings();
}
void setUseAllFilesAccess(bool enabled) {
void setUseAllFilesAccess(bool enabled) {
state = state.copyWith(useAllFilesAccess: enabled);
_saveSettings();
}