profiles_view.dart rebuilt the exact merged-profile view ActiveProfileProvider already computes — the same four source streams, the same merge/avatar derivation, plus a hand-rolled combineLatest4 — and the profile switch screen was its only consumer while already reading the provider for activeId. The screen now renders from the provider (new connectionsByProfile/connectionsById/plexHomeByConnectionId getters) and gates loading on provider initialization; visibleProfileConnections moved to profile_merge.dart for profile_detail_screen; profiles_view.dart is deleted. The switch-screen tests initialize the provider up front like boot does, using a timer-less PlexHomeService subclass so start()'s periodic refresh timer cannot trip the widget-test pending-timer invariant; the deleted pipeline's merge assertions were ported to profile_merge_test and active_profile_provider_test.
85 lines
3.3 KiB
Dart
85 lines
3.3 KiB
Dart
import '../connection/connection.dart';
|
|
import '../models/plex/plex_home_user.dart';
|
|
import '../services/storage_service.dart';
|
|
import 'profile.dart';
|
|
import 'profile_connection.dart';
|
|
|
|
/// Groups profile-connection rows by profile id, preserving input order.
|
|
Map<String, List<ProfileConnection>> groupConnectionsByProfile(List<ProfileConnection> pcs) {
|
|
final out = <String, List<ProfileConnection>>{};
|
|
for (final pc in pcs) {
|
|
out.putIfAbsent(pc.profileId, () => []).add(pc);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/// Join-table rows that should be shown as explicit, user-manageable
|
|
/// connections for [profile].
|
|
///
|
|
/// Plex Home profiles own their parent account implicitly through
|
|
/// [Profile.parentConnectionId]. A parent [ProfileConnection] row may still
|
|
/// exist as a token cache, but UI should not render it as a removable
|
|
/// borrowed connection.
|
|
List<ProfileConnection> visibleProfileConnections(Profile profile, List<ProfileConnection> pcs) {
|
|
final parentId = profile.parentConnectionId;
|
|
if (!profile.isPlexHome || parentId == null) return pcs;
|
|
return pcs.where((pc) => pc.connectionId != parentId).toList();
|
|
}
|
|
|
|
/// Merge local profiles with virtual Plex Home profiles. Each Plex Home
|
|
/// user becomes a virtual profile attached to its `connectionId`. Home
|
|
/// users whose connection isn't registered are dropped — their profile
|
|
/// can't be activated until the parent account is re-added.
|
|
List<Profile> mergeLocalWithPlexHome({
|
|
required List<Profile> locals,
|
|
required Map<String, List<PlexHomeUser>> plexHomeByConnectionId,
|
|
required Map<String, Connection> connectionsById,
|
|
StorageService? storage,
|
|
}) {
|
|
final out = <Profile>[for (final local in locals) _withLatestStoredLastUsed(local, storage)];
|
|
for (final entry in plexHomeByConnectionId.entries) {
|
|
final connectionId = entry.key;
|
|
if (!connectionsById.containsKey(connectionId)) continue;
|
|
for (final user in entry.value) {
|
|
out.add(
|
|
Profile.virtualPlexHome(
|
|
connectionId: connectionId,
|
|
homeUser: user,
|
|
lastUsedAt: storage?.getProfileLastUsed(
|
|
plexHomeProfileId(accountConnectionId: connectionId, homeUserUuid: user.uuid),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
return sortProfilesByLastUsed(out);
|
|
}
|
|
|
|
List<Profile> sortProfilesByLastUsed(List<Profile> profiles) {
|
|
final indexed = profiles.indexed.toList();
|
|
indexed.sort((a, b) {
|
|
final aLastUsed = a.$2.lastUsedAt;
|
|
final bLastUsed = b.$2.lastUsedAt;
|
|
if (aLastUsed == null && bLastUsed == null) return a.$1.compareTo(b.$1);
|
|
if (aLastUsed == null) return 1;
|
|
if (bLastUsed == null) return -1;
|
|
final byLastUsed = bLastUsed.compareTo(aLastUsed);
|
|
if (byLastUsed != 0) return byLastUsed;
|
|
return a.$1.compareTo(b.$1);
|
|
});
|
|
return [for (final entry in indexed) entry.$2];
|
|
}
|
|
|
|
Profile _withLatestStoredLastUsed(Profile profile, StorageService? storage) {
|
|
final storedLastUsed = storage?.getProfileLastUsed(profile.id);
|
|
final currentLastUsed = profile.lastUsedAt;
|
|
final lastUsedAt = switch ((currentLastUsed, storedLastUsed)) {
|
|
(null, final stored?) => stored,
|
|
(final current?, null) => current,
|
|
(final current?, final stored?) => stored.isAfter(current) ? stored : current,
|
|
_ => null,
|
|
};
|
|
if (lastUsedAt == currentLastUsed) return profile;
|
|
return profile.copyWith(lastUsedAt: lastUsedAt);
|
|
}
|