Files
plezy/lib/profiles/profiles_view.dart
T
edde746 26dbce0277 fix(profiles): name the Plex user and account in one translated chip
A Plex account connection labels itself with the account owner's name.
Under a profile tile that reads as being signed in as the owner: the
Plex Home tile showed the owner beneath the Home user's own name, and a
local profile that borrowed a Home user out of someone else's account
showed only the lender.

Both halves of the relation now go through a single translated string,
so a locale orders them itself instead of inheriting the English
"user via account" — az, hu, ja, kk, ko, tr, uz, zh and zh-Hant put the
account first. When the Home cache cannot resolve the connection's uuid
the chip names the account alone rather than falling back to a bare
name. ProfilesView carries the Home user cache that resolution needs,
and chip labels ellipsize now that an account label can be an email.
2026-08-05 13:05:15 +02:00

180 lines
5.7 KiB
Dart

import 'dart:async';
import '../connection/connection.dart';
import '../connection/connection_registry.dart';
import '../models/plex/plex_home_user.dart';
import '../services/storage_service.dart';
import 'plex_home_service.dart';
import 'profile.dart';
import 'profile_avatar_source.dart';
import 'profile_connection.dart';
import 'profile_connection_registry.dart';
import 'profile_merge.dart';
import 'profile_registry.dart';
/// Snapshot for picker / manage-profiles UIs: every visible profile
/// (local rows from [ProfileRegistry] + virtual Plex Home profiles built
/// from [PlexHomeService]'s live cache) plus the data needed to render
/// per-profile connection chips.
class ProfilesView {
final List<Profile> profiles;
/// Per-profile borrowed connections. Does **not** include the Plex Home
/// parent — that's implicit via [Profile.parentConnectionId]. Plex Home
/// profiles can have entries here too (e.g. borrowed Jellyfin servers).
final Map<String, List<ProfileConnection>> connectionsByProfile;
final Map<String, Connection> connectionsById;
/// Picture URL per profile id; null means render initials. See [resolveProfileAvatarUrls].
final Map<String, String?> avatarUrlByProfile;
/// Live Plex Home users per account connection id. Chip/label UI needs it
/// to turn a [ProfileConnection.userIdentifier] uuid into the Home user's
/// name; without it a borrowed Plex connection can only name the account
/// owner, which reads as the wrong identity.
final Map<String, List<PlexHomeUser>> plexHomeByConnectionId;
const ProfilesView({
required this.profiles,
required this.connectionsByProfile,
required this.connectionsById,
required this.avatarUrlByProfile,
required this.plexHomeByConnectionId,
});
static const empty = ProfilesView(
profiles: [],
connectionsByProfile: {},
connectionsById: {},
avatarUrlByProfile: {},
plexHomeByConnectionId: {},
);
}
/// 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();
}
/// Combine [ProfileRegistry], [ProfileConnectionRegistry],
/// [ConnectionRegistry], and [PlexHomeService] into a single stream.
/// Plex Home profiles are constructed on the fly from the live cache; they
/// are never persisted as Profile rows.
Stream<ProfilesView> watchProfilesView({
required ProfileRegistry profiles,
required ProfileConnectionRegistry profileConnections,
required ConnectionRegistry connections,
required PlexHomeService plexHome,
StorageService? storage,
}) {
return _combineLatest4<
List<Profile>,
List<ProfileConnection>,
List<Connection>,
Map<String, List<PlexHomeUser>>,
ProfilesView
>(
profiles.watchProfiles(),
profileConnections.watchAll(),
connections.watchConnections(),
plexHome.stream,
(locals, pcs, conns, homes) => _build(locals: locals, pcs: pcs, conns: conns, homes: homes, storage: storage),
);
}
ProfilesView _build({
required List<Profile> locals,
required List<ProfileConnection> pcs,
required List<Connection> conns,
required Map<String, List<PlexHomeUser>> homes,
required StorageService? storage,
}) {
final connectionsById = {for (final c in conns) c.id: c};
final connectionsByProfile = groupConnectionsByProfile(pcs);
final all = mergeLocalWithPlexHome(
locals: locals,
plexHomeByConnectionId: homes,
connectionsById: connectionsById,
storage: storage,
);
return ProfilesView(
profiles: all,
connectionsByProfile: connectionsByProfile,
connectionsById: connectionsById,
avatarUrlByProfile: resolveProfileAvatarUrls(
profiles: all,
connectionsByProfile: connectionsByProfile,
connectionsById: connectionsById,
plexHomeByConnectionId: homes,
),
plexHomeByConnectionId: homes,
);
}
/// Lightweight `combineLatest4` — emits the combined value once each input
/// has produced a value, then on every subsequent tick from any input.
Stream<R> _combineLatest4<A, B, C, D, R>(
Stream<A> a,
Stream<B> b,
Stream<C> c,
Stream<D> d,
R Function(A, B, C, D) combine,
) {
late StreamController<R> controller;
StreamSubscription<A>? subA;
StreamSubscription<B>? subB;
StreamSubscription<C>? subC;
StreamSubscription<D>? subD;
A? lastA;
B? lastB;
C? lastC;
D? lastD;
var hasA = false, hasB = false, hasC = false, hasD = false;
void emit() {
if (hasA && hasB && hasC && hasD) controller.add(combine(lastA as A, lastB as B, lastC as C, lastD as D));
}
controller = StreamController<R>(
onListen: () {
subA = a.listen((v) {
lastA = v;
hasA = true;
emit();
}, onError: controller.addError);
subB = b.listen((v) {
lastB = v;
hasB = true;
emit();
}, onError: controller.addError);
subC = c.listen((v) {
lastC = v;
hasC = true;
emit();
}, onError: controller.addError);
subD = d.listen((v) {
lastD = v;
hasD = true;
emit();
}, onError: controller.addError);
},
onCancel: () async {
await subA?.cancel();
await subB?.cancel();
await subC?.cancel();
await subD?.cancel();
await controller.close();
},
);
return controller.stream;
}