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
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import '../media/media_backend.dart';
|
||||
import '../models/plex/plex_home_user.dart';
|
||||
import '../services/plex_auth_service.dart';
|
||||
import '../utils/json_utils.dart';
|
||||
import '../utils/url_utils.dart';
|
||||
|
||||
/// Identifier of a backend kind a [Connection] points at. Lighter-weight than
|
||||
@@ -232,6 +233,13 @@ class JellyfinConnection extends Connection {
|
||||
/// match/unmatch, edit metadata) without an extra round-trip.
|
||||
final bool isAdministrator;
|
||||
|
||||
/// The authenticated user's `PrimaryImageTag`, or `null` when they have no
|
||||
/// profile picture. Jellyfin omits the key entirely in that case, and the
|
||||
/// tag is `MD5(imagePath + lastModified)` so it changes on every upload —
|
||||
/// which makes the derived avatar URL self-invalidating. Captured at auth
|
||||
/// time and refreshed by [JellyfinClient.checkHealth].
|
||||
final String? primaryImageTag;
|
||||
|
||||
JellyfinConnection({
|
||||
required this.id,
|
||||
required String baseUrl,
|
||||
@@ -243,6 +251,7 @@ class JellyfinConnection extends Connection {
|
||||
required this.accessToken,
|
||||
required this.deviceId,
|
||||
this.isAdministrator = false,
|
||||
this.primaryImageTag,
|
||||
this.status = ConnectionStatus.unknown,
|
||||
required this.createdAt,
|
||||
this.lastAuthenticatedAt,
|
||||
@@ -298,6 +307,12 @@ class JellyfinConnection extends Connection {
|
||||
String? accessToken,
|
||||
String? deviceId,
|
||||
bool? isAdministrator,
|
||||
String? primaryImageTag,
|
||||
|
||||
/// Deleting a Jellyfin profile picture drops `PrimaryImageTag` from the
|
||||
/// user DTO, so a refresh must be able to null the cached value — a bare
|
||||
/// `primaryImageTag: null` is indistinguishable from "unchanged".
|
||||
bool clearPrimaryImageTag = false,
|
||||
ConnectionStatus? status,
|
||||
DateTime? createdAt,
|
||||
DateTime? lastAuthenticatedAt,
|
||||
@@ -314,6 +329,7 @@ class JellyfinConnection extends Connection {
|
||||
accessToken: accessToken ?? this.accessToken,
|
||||
deviceId: deviceId ?? this.deviceId,
|
||||
isAdministrator: isAdministrator ?? this.isAdministrator,
|
||||
primaryImageTag: clearPrimaryImageTag ? null : (primaryImageTag ?? this.primaryImageTag),
|
||||
status: status ?? this.status,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
lastAuthenticatedAt: lastAuthenticatedAt ?? this.lastAuthenticatedAt,
|
||||
@@ -332,6 +348,7 @@ class JellyfinConnection extends Connection {
|
||||
'accessToken': accessToken,
|
||||
'deviceId': deviceId,
|
||||
'isAdministrator': isAdministrator,
|
||||
'primaryImageTag': primaryImageTag,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -359,9 +376,23 @@ class JellyfinConnection extends Connection {
|
||||
accessToken: json['accessToken'] as String? ?? '',
|
||||
deviceId: json['deviceId'] as String? ?? '',
|
||||
isAdministrator: json['isAdministrator'] as bool? ?? false,
|
||||
primaryImageTag: normalizePrimaryImageTag(json['primaryImageTag']),
|
||||
status: status,
|
||||
createdAt: createdAt,
|
||||
lastAuthenticatedAt: lastAuthenticatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
/// Tolerant read of a Jellyfin user DTO's `PrimaryImageTag`.
|
||||
///
|
||||
/// The tag is decorative — a fork or a drifted scalar type must never brick
|
||||
/// sign-in — so this coerces through [readStringField] rather than casting,
|
||||
/// and collapses absent/blank to `null` ("no picture").
|
||||
static String? readPrimaryImageTag(Map<String, Object?> userDto) =>
|
||||
normalizePrimaryImageTag(readStringField(userDto, 'PrimaryImageTag'));
|
||||
|
||||
static String? normalizePrimaryImageTag(Object? raw) {
|
||||
final tag = raw?.toString().trim();
|
||||
return tag == null || tag.isEmpty ? null : tag;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user