fix(downloads): retain offline media after logout

This commit is contained in:
edde746
2026-07-09 23:22:27 +02:00
parent ed183a855e
commit e2ad28ca32
3 changed files with 55 additions and 5 deletions
+20 -1
View File
@@ -89,6 +89,7 @@ class DownloadProvider extends ChangeNotifier with DisposableChangeNotifierMixin
final Map<String, SyncRuleItem> _syncRules = {};
String? _activeProfileId;
Future<void>? _profileScopedReloadFuture;
OfflineModeSource? _offlineSource;
@@ -141,7 +142,9 @@ class DownloadProvider extends ChangeNotifier with DisposableChangeNotifierMixin
void setActiveProfileId(String? profileId) {
if (_activeProfileId == profileId) return;
_activeProfileId = profileId;
unawaited(_reloadProfileScopedStateForActiveProfile());
final reload = _reloadProfileScopedStateForActiveProfile();
_profileScopedReloadFuture = reload;
unawaited(reload);
}
Future<void> _reloadProfileScopedStateForActiveProfile() async {
@@ -219,6 +222,22 @@ class DownloadProvider extends ChangeNotifier with DisposableChangeNotifierMixin
safeNotifyListeners();
}
/// Preserve physical downloads across a full logout while detaching them
/// from profiles that are about to be deleted. The next selected profile
/// adopts the ownerless rows through [_loadDownloadOwners].
Future<void> detachDownloadsForLogout() async {
// Invalidate any in-flight profile reload before waiting for it: an old
// reload may still finish its DB adoption, but cannot repopulate the
// active-profile view after this point.
_activeProfileId = null;
await _initFuture;
await _profileScopedReloadFuture;
await _database.clearAllDownloadOwners();
_ownedDownloadKeys.clear();
_syncRules.clear();
safeNotifyListeners();
}
/// Remove ownership rows for [profileId] that belong to the removed
/// connection's public server ids. Physical files stay when any other valid
/// owner remains.
+7 -4
View File
@@ -230,7 +230,10 @@ Future<void> logoutAllProfiles(BuildContext context) async {
await companionRemote.resetForLogout();
await userProfileProvider.logout();
await scope.downloads.deleteAllDownloads();
// Downloads are device-local data, not credentials. Keep their physical
// files and pinned metadata, but detach profile ownership before deleting
// the profiles so the next selected profile can adopt them.
await scope.downloads.detachDownloadsForLogout();
scope.multiServer.clearAllConnections();
// Drop the profile/connection rows so the next sign-in starts clean and
// doesn't bind to stale tokens or orphaned profile rows.
@@ -246,9 +249,9 @@ Future<void> logoutAllProfiles(BuildContext context) async {
// through the next sign-in's clients).
await scope.database.clearAllWatchActions();
await scope.database.clearAllSyncRules();
// Downloads were removed above, so no pinned cache rows need to survive this
// app-global logout into the next sign-in.
await ApiCache.instance.clearAll();
// Preserve pinned rows backing offline downloads; all session/API data is
// volatile and must not cross into the next sign-in.
await ApiCache.instance.clearVolatile();
await scope.hiddenLibraries?.refresh();
playbackState.clearShuffle();
@@ -156,6 +156,34 @@ void main() {
p.dispose();
});
test('logout detaches ownership without deleting physical downloads', () async {
const globalKey = 'srv:preserved';
await db.insertDownload(
serverId: ServerId('srv'),
ratingKey: 'preserved',
globalKey: globalKey,
type: 'movie',
status: DownloadStatus.completed.index,
);
await db.addDownloadOwner(profileId: 'test-profile', globalKey: globalKey);
final p = DownloadProvider.forTesting(downloadManager: downloadManager, database: db);
await p.ensureInitialized();
p.debugSeedState(
downloads: {globalKey: const DownloadProgress(globalKey: globalKey, status: DownloadStatus.completed)},
ownedDownloadKeys: {globalKey},
);
expect(p.downloads, contains(globalKey));
await p.detachDownloadsForLogout();
expect(await db.getDownloadedMedia(globalKey), isNotNull);
expect(await db.hasDownloadOwner(globalKey), isFalse);
expect(p.downloads, isEmpty);
p.dispose();
});
});
group('DownloadProvider — local file selection', () {