fix: reload migrated profiles before startup
This commit is contained in:
+13
-1
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user