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.
180 lines
5.7 KiB
Dart
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;
|
|
}
|