fix: reload migrated profiles before startup

This commit is contained in:
edde746
2026-05-01 23:08:21 +02:00
parent f6c6808352
commit 8a8c2ecb87
7 changed files with 157 additions and 18 deletions
+13 -1
View File
@@ -1031,6 +1031,7 @@ class _SetupScreenState extends State<SetupScreen> {
try {
final connRegistry = context.read<ConnectionRegistry>();
final profileRegistry = context.read<ProfileRegistry>();
final activeProfiles = context.read<ActiveProfileProvider>();
final bootstrap = ConnectionBootstrap(
storage: storage,
connectionRegistry: connRegistry,
@@ -1038,6 +1039,11 @@ class _SetupScreenState extends State<SetupScreen> {
profileRegistry: profileRegistry,
);
await bootstrap.run();
// Provider initialization starts before this screen runs the legacy
// migration. Reload after bootstrap so copied Plex Home users and the
// selected active profile are visible before setup decides binding is
// already settled and navigates to MainScreen.
await activeProfiles.reloadFromStorage();
} catch (e, st) {
appLogger.w('Boot-time migration failed', error: e, stackTrace: st);
}
@@ -1129,9 +1135,15 @@ class _SetupScreenState extends State<SetupScreen> {
// profile to bind. `initialize` is fire-and-forget at provider creation,
// so awaiting here pulls control through the same future and triggers
// the listener-driven rebind synchronously.
await activeProfile.initialize();
await activeProfile.reloadFromStorage();
if (!mounted) return;
if (activeProfile.active == null && activeProfile.profiles.isEmpty) {
appLogger.w('Setup: stored connections exist but no profiles resolved after bootstrap; returning to auth');
unawaited(Navigator.pushReplacement(context, fadeRoute(const AuthScreen())));
return;
}
// Wire the per-server status listener before either branch so the splash
// checkmarks fill in even while the user is choosing a profile.
_bindServerStatusListener(activeProfile, _serverManagerFromContext);
+26 -11
View File
@@ -125,18 +125,19 @@ class ActiveProfileProvider extends ChangeNotifier with DisposableChangeNotifier
return future;
}
Future<void> _initialize() async {
_storage ??= await StorageService.getInstance();
// Hydrate the Plex Home cache before we read it — `_plexHome.current`
// is only populated after start() finishes its disk-cache load.
await _plexHome.start();
/// Re-read connections, Plex Home cache, local profiles, and active id.
///
/// Provider initialization starts before boot-time migration, so the first
/// snapshot can legitimately miss the migrated connection/profile state.
Future<void> reloadFromStorage() async {
await initialize();
await _plexHome.reloadFromStorage();
await _reloadSnapshot();
safeNotifyListeners();
}
_localProfiles = await _registry.list();
final initialConns = await _connections.list();
_connectionsById = {for (final c in initialConns) c.id: c};
_plexHomeUsers = _plexHome.current;
_recomputeProfiles();
_resolveActive();
Future<void> _initialize() async {
await _reloadSnapshot();
_localSub = _registry.watchProfiles().listen((list) {
_localProfiles = list;
@@ -161,6 +162,20 @@ class ActiveProfileProvider extends ChangeNotifier with DisposableChangeNotifier
safeNotifyListeners();
}
Future<void> _reloadSnapshot() async {
_storage ??= await StorageService.getInstance();
// Hydrate the Plex Home cache before we read it — `_plexHome.current`
// is only populated after start() finishes its disk-cache load.
await _plexHome.start();
_localProfiles = await _registry.list();
final initialConns = await _connections.list();
_connectionsById = {for (final c in initialConns) c.id: c};
_plexHomeUsers = _plexHome.current;
_recomputeProfiles();
_resolveActive();
}
void _recomputeProfiles() {
_profiles = mergeLocalWithPlexHome(
locals: _localProfiles,
+30
View File
@@ -82,6 +82,36 @@ class PlexHomeService {
return future;
}
/// Re-read per-connection Plex Home user caches from storage.
///
/// This is used after boot-time legacy migration. The service is started
/// before [ConnectionBootstrap] runs, so it may have already missed the
/// copied `plex_home_users_{connectionId}` cache and new connection row.
Future<void> reloadFromStorage() async {
await start();
_storage ??= await StorageService.getInstance();
final current = await _connections.list();
final plexIds = current.whereType<PlexAccountConnection>().map((c) => c.id).toSet();
var changed = false;
for (final id in _byConnection.keys.toList()) {
if (!plexIds.contains(id)) {
_byConnection.remove(id);
changed = true;
}
}
for (final conn in current.whereType<PlexAccountConnection>()) {
final cached = _readCache(conn.id);
if (cached == null) continue;
_byConnection[conn.id] = cached;
changed = true;
}
if (changed) _emit();
}
Future<void> _start() async {
_storage ??= await StorageService.getInstance();
+3 -4
View File
@@ -835,6 +835,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
);
if (confirm && mounted) {
final navigator = Navigator.of(context, rootNavigator: true);
// Use comprehensive logout through UserProfileProvider
final userProfileProvider = Provider.of<UserProfileProvider>(context, listen: false);
final multiServerProvider = context.read<MultiServerProvider>();
@@ -862,11 +863,9 @@ class _DiscoverScreenState extends State<DiscoverScreen>
await hiddenLibrariesProvider.refresh();
playbackStateProvider.clearShuffle();
if (mounted) {
if (navigator.mounted) {
unawaited(
Navigator.of(
context,
).pushAndRemoveUntil(MaterialPageRoute(builder: (context) => const AuthScreen()), (route) => false),
navigator.pushAndRemoveUntil(MaterialPageRoute(builder: (context) => const AuthScreen()), (route) => false),
);
}
}
@@ -28,6 +28,7 @@ import '../../widgets/app_icon.dart';
import '../../widgets/backend_badge.dart';
import '../../widgets/focused_scroll_scaffold.dart';
import '../libraries/state_messages.dart';
import '../auth_screen.dart';
import 'add_local_profile_screen.dart';
import 'profile_detail_screen.dart';
@@ -245,9 +246,19 @@ class _ProfileSwitchScreenState extends State<ProfileSwitchScreen> {
.where((p) => p.id != activeProfile?.id && p.parentConnectionId != parentId)
.toList();
final binder = context.read<ActiveProfileBinder>();
final navigator = Navigator.of(context, rootNavigator: true);
try {
await connRegistry.remove(parentId);
final noConnectionsLeft = (await connRegistry.list()).isEmpty;
if (noConnectionsLeft) {
await active.clearActiveProfile();
unawaited(binder.rebindActive());
if (navigator.mounted) {
unawaited(navigator.pushAndRemoveUntil(MaterialPageRoute(builder: (_) => const AuthScreen()), (_) => false));
}
return;
}
if (!mounted) return;
// If the active virtual profile belonged to the removed account, make
// the storage state explicit instead of relying on provider fallback.
@@ -2,8 +2,10 @@ import 'dart:async';
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/connection/connection.dart';
import 'package:plezy/connection/connection_registry.dart';
import 'package:plezy/database/app_database.dart';
import 'package:plezy/models/plex/plex_home_user.dart';
import 'package:plezy/profiles/active_profile_provider.dart';
import 'package:plezy/profiles/plex_home_service.dart';
import 'package:plezy/profiles/profile.dart';
@@ -13,6 +15,31 @@ import 'package:plezy/services/storage_service.dart';
import '../test_helpers/prefs.dart';
PlexHomeUser _homeUser(String uuid, {String name = 'Home User'}) {
return PlexHomeUser(
id: 1,
uuid: uuid,
title: name,
thumb: '',
hasPassword: false,
restricted: false,
updatedAt: null,
admin: true,
guest: false,
protected: false,
);
}
PlexAccountConnection _account(String id) {
return PlexAccountConnection(
id: id,
accountToken: 'token-$id',
clientIdentifier: 'client-$id',
accountLabel: 'Plex',
createdAt: DateTime(2026, 1, 1),
);
}
void main() {
late AppDatabase db;
late ProfileRegistry registry;
@@ -20,6 +47,7 @@ void main() {
late PlexHomeService plexHome;
late ActiveProfileProvider provider;
late StorageService storage;
late List<PlexHomeUser> fetchedHomeUsers;
setUp(() async {
resetSharedPreferencesForTest();
@@ -27,12 +55,12 @@ void main() {
registry = ProfileRegistry(db);
connections = ConnectionRegistry(db);
storage = await StorageService.getInstance();
fetchedHomeUsers = const [];
plexHome = PlexHomeService(
connections: connections,
profileConnections: ProfileConnectionRegistry(db),
storage: storage,
// No accounts in tests, so the fetcher is never called.
plexHomeUserFetcher: (_) async => const [],
plexHomeUserFetcher: (_) async => fetchedHomeUsers,
);
provider = ActiveProfileProvider(
registry: registry,
@@ -77,6 +105,25 @@ void main() {
expect(provider.activeId, isNull);
});
test('reloadFromStorage resolves Plex Home profile added after early initialize', () async {
await provider.initialize();
expect(provider.profiles, isEmpty);
final account = _account('plex.migrated');
final user = _homeUser('home-user-1', name: 'Migrated User');
fetchedHomeUsers = [user];
await connections.upsert(account);
await storage.savePlexHomeUsersCache(account.id, [user.toJson()]);
final profileId = plexHomeProfileId(accountConnectionId: account.id, homeUserUuid: user.uuid);
await storage.setActiveProfileId(profileId);
await provider.reloadFromStorage();
expect(provider.profiles.map((p) => p.id), contains(profileId));
expect(provider.activeId, profileId);
expect(provider.active?.displayName, 'Migrated User');
});
test('initialize clears storage when stored id is stale', () async {
// A previously-active profile that was deleted should not keep
// storage-scoped settings under the removed profile id.
+25
View File
@@ -110,6 +110,31 @@ void main() {
expect(service.current[acct.id]!.first.uuid, 'seeded-uuid');
});
test('reloadFromStorage picks up caches written after startup', () async {
final refreshBlocker = Completer<List<PlexHomeUser>>();
service = PlexHomeService(
connections: connections,
profileConnections: profileConnections,
storage: storage,
plexHomeUserFetcher: (_) => refreshBlocker.future,
);
addTearDown(() {
if (!refreshBlocker.isCompleted) refreshBlocker.complete(const []);
});
await service.start();
expect(service.current, isEmpty);
final acct = _account('plex.migrated');
await connections.upsert(acct);
await storage.savePlexHomeUsersCache(acct.id, [_user('migrated-home-user').toJson()]);
await service.reloadFromStorage();
expect(service.current[acct.id], hasLength(1));
expect(service.current[acct.id]!.single.uuid, 'migrated-home-user');
});
test('concurrent start calls await the same in-flight startup', () async {
service = PlexHomeService(
connections: connections,