refactor(profiles): delete dead client accessors, the write-only focus aspect, and test-only registry API
getServerBoundPlexClient/getClientForLibrary existed only for their own unit tests; MetadataEditAdapter.backend and Plex's unreachable prefKey fallback and music-kind branches had no production path (edit is double-gated on supportsKind); MainScreenFocusScope's focus aspect, isSidebarFocused, and focusContent were write-only; resolveActivePlexIdentity.preferredAccount was never passed; ProfileRegistry.get was unused outside tests — its removal exposed a stale test contract, now pinned correctly: list() deliberately never serves plex_home rows.
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import '../i18n/strings.g.dart';
|
||||
import '../media/media_backend.dart';
|
||||
import '../media/media_item.dart';
|
||||
import '../media/media_kind.dart';
|
||||
import '../media/media_server_client.dart';
|
||||
@@ -12,9 +11,6 @@ class JellyfinMetadataEditAdapter extends MetadataEditAdapter {
|
||||
|
||||
JellyfinMetadataEditAdapter(this.client);
|
||||
|
||||
@override
|
||||
MediaBackend get backend => client.backend;
|
||||
|
||||
@override
|
||||
MediaServerClient get mediaClient => client;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import '../i18n/strings.g.dart';
|
||||
import '../media/media_backend.dart';
|
||||
import '../media/media_item.dart';
|
||||
import '../media/media_kind.dart';
|
||||
import '../media/media_server_client.dart';
|
||||
@@ -119,7 +118,6 @@ class MetadataEditDraft {
|
||||
}
|
||||
|
||||
abstract class MetadataEditAdapter {
|
||||
MediaBackend get backend;
|
||||
MediaServerClient get mediaClient;
|
||||
|
||||
bool supportsKind(MediaKind kind);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import '../exceptions/media_server_exceptions.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
import '../media/media_backend.dart';
|
||||
import '../media/media_item.dart';
|
||||
import '../media/media_kind.dart';
|
||||
import '../media/media_server_client.dart';
|
||||
@@ -14,9 +13,6 @@ class PlexMetadataEditAdapter extends MetadataEditAdapter {
|
||||
|
||||
PlexMetadataEditAdapter(this.client);
|
||||
|
||||
@override
|
||||
MediaBackend get backend => MediaBackend.plex;
|
||||
|
||||
@override
|
||||
MediaServerClient get mediaClient => client;
|
||||
|
||||
@@ -103,8 +99,7 @@ class PlexMetadataEditAdapter extends MetadataEditAdapter {
|
||||
|
||||
@override
|
||||
Future<bool> saveImmediateField(MetadataEditDraft draft, MetadataEditField field, Object? value) async {
|
||||
final prefKey = _prefKey(field.id);
|
||||
if (prefKey == null) return super.saveImmediateField(draft, field, value);
|
||||
final prefKey = _prefKey(field.id)!;
|
||||
final success = await client.updateMetadataPrefs(draft.sourceItem.id, {prefKey: (value as String?) ?? ''});
|
||||
if (success) {
|
||||
draft.originalValues[field.id] = value;
|
||||
@@ -221,19 +216,6 @@ class PlexMetadataEditAdapter extends MetadataEditAdapter {
|
||||
tag('label', t.metadataEdit.label),
|
||||
],
|
||||
MediaKind.episode => [tag('director', t.metadataEdit.director), tag('writer', t.metadataEdit.writer)],
|
||||
MediaKind.artist => [
|
||||
tag('genre', t.metadataEdit.genre),
|
||||
tag('style', t.metadataEdit.style),
|
||||
tag('mood', t.metadataEdit.mood),
|
||||
tag('country', t.metadataEdit.country),
|
||||
tag('collection', t.metadataEdit.collection),
|
||||
],
|
||||
MediaKind.album => [
|
||||
tag('genre', t.metadataEdit.genre),
|
||||
tag('style', t.metadataEdit.style),
|
||||
tag('mood', t.metadataEdit.mood),
|
||||
tag('collection', t.metadataEdit.collection),
|
||||
],
|
||||
_ => const [],
|
||||
};
|
||||
}
|
||||
@@ -352,9 +334,6 @@ int _plexTypeNumberForKind(MediaKind kind) => switch (kind) {
|
||||
MediaKind.show => 2,
|
||||
MediaKind.season => 3,
|
||||
MediaKind.episode => 4,
|
||||
MediaKind.artist => 8,
|
||||
MediaKind.album => 9,
|
||||
MediaKind.track => 10,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../media/media_library.dart';
|
||||
import '../media/media_server_client.dart';
|
||||
import '../services/plex_client.dart';
|
||||
import '../utils/provider_extensions.dart';
|
||||
|
||||
/// Mixin providing common functionality for library tab screens
|
||||
@@ -9,14 +8,7 @@ import '../utils/provider_extensions.dart';
|
||||
mixin LibraryTabStateMixin<T extends StatefulWidget> on State<T> {
|
||||
MediaLibrary get library;
|
||||
|
||||
/// Get the [PlexClient] for this library's server. Throws if unavailable.
|
||||
/// Use [getMediaClientForLibrary] in code paths that work for both Plex
|
||||
/// and Jellyfin via the [MediaServerClient] interface — this getter is
|
||||
/// for Plex-only methods (collections, metadata edit, etc.).
|
||||
PlexClient getClientForLibrary() => context.getPlexClientForLibrary(library);
|
||||
|
||||
/// Get a backend-neutral [MediaServerClient] for this library's server.
|
||||
/// Throws if unavailable. Prefer this over [getClientForLibrary] for any
|
||||
/// flow that doesn't strictly need Plex-only APIs.
|
||||
/// Throws if unavailable.
|
||||
MediaServerClient getMediaClientForLibrary() => context.getMediaClientForLibrary(library);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import '../media/ids.dart';
|
||||
|
||||
import '../media/media_item.dart';
|
||||
import '../media/media_server_client.dart';
|
||||
import '../services/plex_client.dart';
|
||||
import '../utils/global_key_utils.dart';
|
||||
import '../utils/provider_extensions.dart';
|
||||
|
||||
@@ -23,14 +22,6 @@ mixin ServerBoundMediaMixin<T extends StatefulWidget> on State<T> {
|
||||
return buildGlobalKey(resolved, ratingKey);
|
||||
}
|
||||
|
||||
/// Returns the [PlexClient] for the bound server, or null when offline /
|
||||
/// the server is Jellyfin / not registered. Use [getServerBoundMediaClient]
|
||||
/// for backend-neutral flows.
|
||||
PlexClient? getServerBoundPlexClient(BuildContext context) {
|
||||
if (isServerBoundOffline) return null;
|
||||
return context.tryGetPlexClientForServer(serverIdOrNull(serverBoundMetadata.serverId));
|
||||
}
|
||||
|
||||
/// Returns a backend-neutral [MediaServerClient] for the bound server, or
|
||||
/// null when offline / not registered.
|
||||
MediaServerClient? getServerBoundMediaClient(BuildContext context) =>
|
||||
|
||||
@@ -12,9 +12,6 @@ enum MainScreenScopeAspect {
|
||||
/// widgets (e.g. [SideNavigationBleedBuilder] call sites).
|
||||
offset,
|
||||
|
||||
/// `isSidebarFocused`.
|
||||
focus,
|
||||
|
||||
/// `foregroundWidth` / `viewportWidth` / `reservedSideNavigationWidth` —
|
||||
/// stable across sidebar flips (only change with window geometry).
|
||||
layout,
|
||||
@@ -22,8 +19,6 @@ enum MainScreenScopeAspect {
|
||||
|
||||
class MainScreenFocusScope extends InheritedModel<MainScreenScopeAspect> {
|
||||
final VoidCallback focusSidebar;
|
||||
final VoidCallback focusContent;
|
||||
final bool isSidebarFocused;
|
||||
final double sideNavigationWidth;
|
||||
final double? reservedSideNavigationWidth;
|
||||
final double? foregroundLeft;
|
||||
@@ -35,8 +30,6 @@ class MainScreenFocusScope extends InheritedModel<MainScreenScopeAspect> {
|
||||
const MainScreenFocusScope({
|
||||
super.key,
|
||||
required this.focusSidebar,
|
||||
required this.focusContent,
|
||||
required this.isSidebarFocused,
|
||||
required this.sideNavigationWidth,
|
||||
this.reservedSideNavigationWidth,
|
||||
this.foregroundLeft,
|
||||
@@ -98,8 +91,7 @@ class MainScreenFocusScope extends InheritedModel<MainScreenScopeAspect> {
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(MainScreenFocusScope oldWidget) {
|
||||
return isSidebarFocused != oldWidget.isSidebarFocused ||
|
||||
sideNavigationWidth != oldWidget.sideNavigationWidth ||
|
||||
return sideNavigationWidth != oldWidget.sideNavigationWidth ||
|
||||
reservedSideNavigationWidth != oldWidget.reservedSideNavigationWidth ||
|
||||
foregroundLeft != oldWidget.foregroundLeft ||
|
||||
foregroundWidth != oldWidget.foregroundWidth ||
|
||||
@@ -112,9 +104,6 @@ class MainScreenFocusScope extends InheritedModel<MainScreenScopeAspect> {
|
||||
(foregroundLeft != oldWidget.foregroundLeft || sideNavigationWidth != oldWidget.sideNavigationWidth)) {
|
||||
return true;
|
||||
}
|
||||
if (dependencies.contains(MainScreenScopeAspect.focus) && isSidebarFocused != oldWidget.isSidebarFocused) {
|
||||
return true;
|
||||
}
|
||||
if (dependencies.contains(MainScreenScopeAspect.layout) &&
|
||||
(foregroundWidth != oldWidget.foregroundWidth ||
|
||||
viewportWidth != oldWidget.viewportWidth ||
|
||||
|
||||
@@ -14,38 +14,10 @@ Future<ActivePlexIdentity?> resolveActivePlexIdentity({
|
||||
required ActiveProfileProvider activeProfile,
|
||||
required ConnectionRegistry connections,
|
||||
required ProfileConnectionRegistry profileConnections,
|
||||
PlexAccountConnection? preferredAccount,
|
||||
}) async {
|
||||
await activeProfile.initialize();
|
||||
final profile = activeProfile.active;
|
||||
|
||||
String? userUuidForPreferred() {
|
||||
if (profile == null) return null;
|
||||
if (profile.parentConnectionId == preferredAccount?.id) {
|
||||
return profile.plexHomeUserUuid;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (preferredAccount != null) {
|
||||
final preferredUserUuid = userUuidForPreferred();
|
||||
if (preferredUserUuid != null) {
|
||||
return ActivePlexIdentity(account: preferredAccount, userUuid: preferredUserUuid);
|
||||
}
|
||||
if (profile != null) {
|
||||
final pcs = await profileConnections.listForProfile(profile.id);
|
||||
for (final pc in pcs) {
|
||||
if (pc.connectionId == preferredAccount.id) {
|
||||
return ActivePlexIdentity(
|
||||
account: preferredAccount,
|
||||
userUuid: pc.userIdentifier.isEmpty ? null : pc.userIdentifier,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ActivePlexIdentity(account: preferredAccount);
|
||||
}
|
||||
|
||||
final parentId = profile?.parentConnectionId;
|
||||
if (parentId != null) {
|
||||
final account = await connections.getPlexAccount(parentId);
|
||||
|
||||
@@ -421,15 +421,7 @@ class PlexHomeService {
|
||||
PlexHome? materializePlexHome(String connectionId) {
|
||||
final users = _byConnection[connectionId];
|
||||
if (users == null || users.isEmpty) return null;
|
||||
return PlexHome(
|
||||
id: 0,
|
||||
name: '',
|
||||
guestUserID: null,
|
||||
guestUserUUID: '',
|
||||
guestEnabled: false,
|
||||
subscription: false,
|
||||
users: users,
|
||||
);
|
||||
return PlexHome(id: 0, users: users);
|
||||
}
|
||||
|
||||
/// Await startup cache hydration, then materialize the home attached to
|
||||
|
||||
@@ -36,11 +36,6 @@ class ProfileRegistry {
|
||||
return rows.map(_rowToProfile).whereType<Profile>().toList();
|
||||
}
|
||||
|
||||
Future<Profile?> get(String id) async {
|
||||
final row = await (_db.select(_db.profiles)..where((t) => t.id.equals(id))).getSingleOrNull();
|
||||
return row == null ? null : _rowToProfile(row);
|
||||
}
|
||||
|
||||
Future<void> upsert(Profile profile) async {
|
||||
await _db.runIdentityMutation(() async {
|
||||
final row = ProfilesCompanion(
|
||||
|
||||
@@ -1752,8 +1752,6 @@ class _MainScreenState extends State<MainScreen>
|
||||
);
|
||||
return MainScreenFocusScope(
|
||||
focusSidebar: _focusSidebar,
|
||||
focusContent: _focusContent,
|
||||
isSidebarFocused: _isSidebarFocused,
|
||||
sideNavigationWidth: targetContentOffset,
|
||||
reservedSideNavigationWidth: reservedContentOffset,
|
||||
foregroundLeft: contentLayout.left,
|
||||
|
||||
@@ -1073,7 +1073,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
);
|
||||
}
|
||||
|
||||
/// Backend-neutral counterpart of [getServerBoundPlexClient]. Returns a
|
||||
/// Returns a
|
||||
/// [MediaServerClient] for Jellyfin items too, so image URLs use the
|
||||
/// right server's transcoder.
|
||||
MediaServerClient? _getMediaClientForMetadata(BuildContext context) {
|
||||
|
||||
@@ -84,20 +84,6 @@ void main() {
|
||||
{'Name': 'archive'},
|
||||
]);
|
||||
});
|
||||
|
||||
test("the adapter reports the dialect's backend", () {
|
||||
final embyClient = _clientForDto(connection: testEmbyConnection(), dto: _embyItem(), postedBodies: <String>[]);
|
||||
final jellyfinClient = _clientForDto(
|
||||
connection: _jellyfinConnection(),
|
||||
dto: _jellyfinItem(),
|
||||
postedBodies: <String>[],
|
||||
);
|
||||
addTearDown(embyClient.close);
|
||||
addTearDown(jellyfinClient.close);
|
||||
|
||||
expect(JellyfinMetadataEditAdapter(embyClient).backend, MediaBackend.emby);
|
||||
expect(JellyfinMetadataEditAdapter(jellyfinClient).backend, MediaBackend.jellyfin);
|
||||
});
|
||||
}
|
||||
|
||||
JellyfinClient _clientForDto({
|
||||
|
||||
@@ -45,9 +45,6 @@ void main() {
|
||||
}
|
||||
|
||||
class _TestMetadataEditAdapter extends MetadataEditAdapter {
|
||||
@override
|
||||
MediaBackend get backend => MediaBackend.plex;
|
||||
|
||||
@override
|
||||
MediaServerClient get mediaClient => throw UnimplementedError();
|
||||
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/media/media_backend.dart';
|
||||
import 'package:plezy/media/media_kind.dart';
|
||||
import 'package:plezy/media/media_library.dart';
|
||||
import 'package:plezy/mixins/library_tab_state.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
|
||||
import '../test_helpers/multi_server_fixtures.dart';
|
||||
|
||||
class _Probe extends StatefulWidget {
|
||||
const _Probe({required this.library, required this.onState});
|
||||
|
||||
final MediaLibrary library;
|
||||
final void Function(_ProbeState state) onState;
|
||||
|
||||
@override
|
||||
State<_Probe> createState() => _ProbeState();
|
||||
}
|
||||
|
||||
class _ProbeState extends State<_Probe> with LibraryTabStateMixin<_Probe> {
|
||||
@override
|
||||
MediaLibrary get library => widget.library;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Surface state after the first frame so tests receive a mounted probe.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) widget.onState(this);
|
||||
});
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
MediaLibrary _lib({ServerId? serverId, String key = '1'}) =>
|
||||
MediaLibrary(id: key, backend: MediaBackend.plex, title: 'Movies', kind: MediaKind.movie, serverId: serverId);
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('LibraryTabStateMixin', () {
|
||||
testWidgets('getClientForLibrary throws when no server matches and no fallback online', (tester) async {
|
||||
late _ProbeState state;
|
||||
|
||||
final manager = MultiServerManager();
|
||||
final provider = testMultiServerProvider(manager);
|
||||
// provider.dispose() cascades to manager.dispose() — only register
|
||||
// the outer teardown to avoid a double-close on the manager's stream.
|
||||
addTearDown(provider.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<MultiServerProvider>.value(
|
||||
value: provider,
|
||||
child: _Probe(
|
||||
library: _lib(serverId: ServerId('srv-missing')),
|
||||
onState: (s) => state = s,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
expect(() => state.getClientForLibrary(), throwsA(isA<Exception>()));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -82,25 +82,5 @@ void main() {
|
||||
|
||||
expect(() => state.toServerBoundGlobalKey('rk-1'), throwsStateError);
|
||||
});
|
||||
|
||||
testWidgets('getServerBoundPlexClient returns null in offline mode regardless of providers', (tester) async {
|
||||
late _ProbeState state;
|
||||
late BuildContext ctx;
|
||||
await tester.pumpWidget(
|
||||
_Probe(
|
||||
metadata: _meta(serverId: ServerId('srv-A')),
|
||||
offline: true,
|
||||
onState: (s, c) {
|
||||
state = s;
|
||||
ctx = c;
|
||||
},
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
// The provider extension short-circuits to null when isOffline is true,
|
||||
// so no MultiServerProvider is required to exercise this branch.
|
||||
expect(state.getServerBoundPlexClient(ctx), isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -57,8 +57,7 @@ WatchStateEvent _ev({
|
||||
required String itemId,
|
||||
List<String> parentChain = const [],
|
||||
WatchStateChangeType type = WatchStateChangeType.watched,
|
||||
}) =>
|
||||
WatchStateEvent(itemId: itemId, serverId: serverId, changeType: type, parentChain: parentChain, mediaType: 'movie');
|
||||
}) => WatchStateEvent(itemId: itemId, serverId: serverId, changeType: type, parentChain: parentChain);
|
||||
|
||||
/// Drain microtasks the broadcast stream uses to deliver events.
|
||||
Future<void> _settle(WidgetTester tester) async {
|
||||
|
||||
@@ -11,8 +11,6 @@ void main() {
|
||||
MaterialApp(
|
||||
home: MainScreenFocusScope(
|
||||
focusSidebar: () => focusCalls++,
|
||||
focusContent: () {},
|
||||
isSidebarFocused: false,
|
||||
sideNavigationWidth: 0,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
@@ -47,7 +45,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('focusSidebarOf does not register an inherited dependency', (tester) async {
|
||||
var isSidebarFocused = false;
|
||||
var sideNavigationWidth = 0.0;
|
||||
var childBuilds = 0;
|
||||
var focusCalls = 0;
|
||||
late StateSetter rebuildScope;
|
||||
@@ -66,9 +64,7 @@ void main() {
|
||||
rebuildScope = setState;
|
||||
return MainScreenFocusScope(
|
||||
focusSidebar: () => focusCalls++,
|
||||
focusContent: () {},
|
||||
isSidebarFocused: isSidebarFocused,
|
||||
sideNavigationWidth: 0,
|
||||
sideNavigationWidth: sideNavigationWidth,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
@@ -79,7 +75,7 @@ void main() {
|
||||
expect(childBuilds, 1);
|
||||
expect(focusCalls, 1);
|
||||
|
||||
rebuildScope(() => isSidebarFocused = true);
|
||||
rebuildScope(() => sideNavigationWidth = 100);
|
||||
await tester.pump();
|
||||
|
||||
expect(childBuilds, 1);
|
||||
|
||||
@@ -1121,7 +1121,6 @@ PlexServer _server({
|
||||
),
|
||||
],
|
||||
owned: owned,
|
||||
presence: true,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ void main() {
|
||||
expect(await registry.list(), isEmpty);
|
||||
});
|
||||
|
||||
test('upsert + get round-trips a local profile', () async {
|
||||
test('upsert + list round-trips a local profile', () async {
|
||||
final profile = Profile.local(
|
||||
id: 'local-1',
|
||||
displayName: 'Owner',
|
||||
@@ -31,14 +31,14 @@ void main() {
|
||||
);
|
||||
await registry.upsert(profile);
|
||||
|
||||
final fetched = await registry.get('local-1');
|
||||
final fetched = await _profileById(registry, 'local-1');
|
||||
expect(fetched, isNotNull);
|
||||
expect(fetched!.kind, ProfileKind.local);
|
||||
expect(fetched.displayName, 'Owner');
|
||||
expect(fetched.pinHash, profile.pinHash);
|
||||
});
|
||||
|
||||
test('upsert + get round-trips a plex_home profile', () async {
|
||||
test('list never serves plex_home rows (Plex Home users are not persisted profiles)', () async {
|
||||
final profile = Profile.plexHome(
|
||||
id: 'plex-home-acct-uuid',
|
||||
displayName: 'Admin',
|
||||
@@ -50,13 +50,10 @@ void main() {
|
||||
);
|
||||
await registry.upsert(profile);
|
||||
|
||||
final fetched = await registry.get(profile.id);
|
||||
expect(fetched, isNotNull);
|
||||
expect(fetched!.kind, ProfileKind.plexHome);
|
||||
expect(fetched.avatarThumbUrl, profile.avatarThumbUrl);
|
||||
expect(fetched.parentConnectionId, 'acct');
|
||||
expect(fetched.plexAdmin, isTrue);
|
||||
expect(fetched.plexProtected, isTrue);
|
||||
// The read surface is deliberately local-only: Plex Home users are
|
||||
// materialized live by PlexHomeService and any leftover persisted rows
|
||||
// are dropped by [ProfileRegistry.dropAllPlexHomeRows].
|
||||
expect(await _profileById(registry, profile.id), isNull);
|
||||
});
|
||||
|
||||
test('list orders by sortOrder then createdAt', () async {
|
||||
@@ -69,21 +66,21 @@ void main() {
|
||||
test('remove deletes a profile', () async {
|
||||
await registry.upsert(Profile.local(id: 'p', displayName: 'P', createdAt: DateTime(2026, 1, 1)));
|
||||
await registry.remove('p');
|
||||
expect(await registry.get('p'), isNull);
|
||||
expect(await _profileById(registry, 'p'), isNull);
|
||||
});
|
||||
|
||||
test('markUsed updates lastUsedAt', () async {
|
||||
await registry.upsert(Profile.local(id: 'p', displayName: 'P', createdAt: DateTime(2026, 1, 1)));
|
||||
final ts = DateTime(2026, 1, 5, 12, 0);
|
||||
await registry.markUsed('p', ts);
|
||||
final fetched = await registry.get('p');
|
||||
final fetched = await _profileById(registry, 'p');
|
||||
expect(fetched!.lastUsedAt, ts);
|
||||
});
|
||||
|
||||
test('upsert is idempotent (replaces existing row)', () async {
|
||||
await registry.upsert(Profile.local(id: 'p', displayName: 'Original', createdAt: DateTime(2026, 1, 1)));
|
||||
await registry.upsert(Profile.local(id: 'p', displayName: 'Renamed', createdAt: DateTime(2026, 1, 1)));
|
||||
final fetched = await registry.get('p');
|
||||
final fetched = await _profileById(registry, 'p');
|
||||
expect(fetched!.displayName, 'Renamed');
|
||||
});
|
||||
|
||||
@@ -107,3 +104,10 @@ void main() {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<Profile?> _profileById(ProfileRegistry registry, String id) async {
|
||||
for (final profile in await registry.list()) {
|
||||
if (profile.id == id) return profile;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -153,8 +153,6 @@ void main() {
|
||||
theme: monoTheme(dark: true),
|
||||
home: MainScreenFocusScope(
|
||||
focusSidebar: () {},
|
||||
focusContent: () {},
|
||||
isSidebarFocused: false,
|
||||
sideNavigationWidth: targetSidebarOffset,
|
||||
reservedSideNavigationWidth: SideNavigationRailState.tvCollapsedWidth,
|
||||
foregroundLeft: currentForegroundLeft,
|
||||
@@ -321,8 +319,6 @@ void main() {
|
||||
theme: monoTheme(dark: true),
|
||||
home: MainScreenFocusScope(
|
||||
focusSidebar: () {},
|
||||
focusContent: () {},
|
||||
isSidebarFocused: false,
|
||||
sideNavigationWidth: SideNavigationRailState.expandedWidth,
|
||||
reservedSideNavigationWidth: SideNavigationRailState.tvCollapsedWidth,
|
||||
foregroundLeft: 0,
|
||||
@@ -458,8 +454,6 @@ void main() {
|
||||
theme: monoTheme(dark: true),
|
||||
home: MainScreenFocusScope(
|
||||
focusSidebar: () {},
|
||||
focusContent: () {},
|
||||
isSidebarFocused: false,
|
||||
sideNavigationWidth: SideNavigationRailState.expandedWidth,
|
||||
reservedSideNavigationWidth: SideNavigationRailState.tvCollapsedWidth,
|
||||
foregroundLeft: 0,
|
||||
@@ -570,8 +564,6 @@ void main() {
|
||||
theme: monoTheme(dark: true),
|
||||
home: MainScreenFocusScope(
|
||||
focusSidebar: () {},
|
||||
focusContent: () {},
|
||||
isSidebarFocused: false,
|
||||
sideNavigationWidth: SideNavigationRailState.expandedWidth,
|
||||
reservedSideNavigationWidth: SideNavigationRailState.tvCollapsedWidth,
|
||||
foregroundLeft: 0,
|
||||
@@ -722,8 +714,6 @@ Future<void> _pumpDiscoverShell(WidgetTester tester, {required bool isTv}) async
|
||||
theme: monoTheme(dark: true),
|
||||
home: MainScreenFocusScope(
|
||||
focusSidebar: () {},
|
||||
focusContent: () {},
|
||||
isSidebarFocused: false,
|
||||
sideNavigationWidth: SideNavigationRailState.expandedWidth,
|
||||
reservedSideNavigationWidth: SideNavigationRailState.tvCollapsedWidth,
|
||||
foregroundLeft: 0,
|
||||
|
||||
@@ -335,8 +335,6 @@ Future<void> _pumpScreen(
|
||||
theme: monoTheme(dark: true).copyWith(platform: TargetPlatform.macOS),
|
||||
home: MainScreenFocusScope(
|
||||
focusSidebar: onSidebar ?? () {},
|
||||
focusContent: () {},
|
||||
isSidebarFocused: false,
|
||||
sideNavigationWidth: 0,
|
||||
child: DownloadsScreen(key: screenKey),
|
||||
),
|
||||
|
||||
@@ -555,9 +555,6 @@ class _ArtworkAdapter extends MetadataEditAdapter {
|
||||
int applyCalls = 0;
|
||||
int uploadCalls = 0;
|
||||
|
||||
@override
|
||||
MediaBackend get backend => MediaBackend.plex;
|
||||
|
||||
@override
|
||||
MediaServerClient get mediaClient => _client;
|
||||
|
||||
|
||||
@@ -32,8 +32,6 @@ Future<void> pumpLibraryTab(
|
||||
theme: monoTheme(dark: true),
|
||||
home: MainScreenFocusScope(
|
||||
focusSidebar: focusSidebar ?? () {},
|
||||
focusContent: () {},
|
||||
isSidebarFocused: false,
|
||||
sideNavigationWidth: 0,
|
||||
child: Scaffold(
|
||||
body: NestedScrollView(
|
||||
|
||||
Reference in New Issue
Block a user