Files
plezy/lib/profiles/profile_merge.dart
T
edde746 860ce1e11a feat(profiles): show the first linked connection's user picture
A local profile had no picture of its own and always fell back to
initials. It now borrows the user picture of the connection it was
linked to first — oldest Connection.createdAt, ties broken by
connection id, since the join table carries no creation time.

Jellyfin links resolve to /Users/{id}/Images/Primary, keyed by the
PrimaryImageTag now captured at authentication and refreshed from the
/Users/Me body checkHealth already fetches. That endpoint is anonymous
on every Jellyfin release, so the URL carries no api_key and the access
token stays out of the image cache key. Plex links resolve the Home
user the link points at against PlexHomeService's live cache, so no
account-level lookup is needed and the picture tracks Plex's own
refresh.

The picture is derived per snapshot and never written back onto a
Profile: ProfileDetailScreen upserts the model it holds, so a
persisted URL would go stale and outlive the connection it came from.
Plex Home profiles are untouched, including one whose Plex avatar is
unset — it keeps its initials rather than borrowing a lent connection's
picture.

close #1667
2026-08-04 02:22:44 +02:00

72 lines
2.7 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;
}
/// 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);
}