fix(tv): displace content beside sidebar
This commit is contained in:
@@ -13,6 +13,7 @@ import 'package:plezy/media/media_kind.dart';
|
||||
import 'package:plezy/media/media_server_client.dart';
|
||||
import 'package:plezy/media/server_capabilities.dart';
|
||||
import 'package:plezy/mixins/refreshable.dart';
|
||||
import 'package:plezy/mixins/tab_visibility_aware.dart';
|
||||
import 'package:plezy/profiles/active_profile_provider.dart';
|
||||
import 'package:plezy/profiles/plex_home_service.dart';
|
||||
import 'package:plezy/profiles/profile.dart';
|
||||
@@ -55,7 +56,14 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('TV tab focus returns to discover browse rail instead of reload action', (tester) async {
|
||||
await SettingsService.getInstance();
|
||||
final settings = await SettingsService.getInstance();
|
||||
await settings.write(SettingsService.libraryDensity, LibraryDensity.max);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
tester.view.physicalSize = const Size(1280, 720);
|
||||
addTearDown(() {
|
||||
tester.view.resetDevicePixelRatio();
|
||||
tester.view.resetPhysicalSize();
|
||||
});
|
||||
|
||||
final item = MediaItem(
|
||||
id: 'movie_1',
|
||||
@@ -92,6 +100,7 @@ void main() {
|
||||
storage: storage,
|
||||
);
|
||||
final discoverKey = GlobalKey<State<DiscoverScreen>>();
|
||||
const sidebarOffset = SideNavigationRailState.expandedWidth;
|
||||
|
||||
addTearDown(() async {
|
||||
activeProfileProvider.dispose();
|
||||
@@ -121,7 +130,7 @@ void main() {
|
||||
focusSidebar: () {},
|
||||
focusContent: () {},
|
||||
isSidebarFocused: false,
|
||||
sideNavigationWidth: SideNavigationRailState.expandedWidth,
|
||||
sideNavigationWidth: sidebarOffset,
|
||||
child: SizedBox(width: 1280, height: 720, child: DiscoverScreen(key: discoverKey)),
|
||||
),
|
||||
),
|
||||
@@ -135,21 +144,50 @@ void main() {
|
||||
final scale = TvLayoutConstants.scaleForSize(const Size(1280, 720));
|
||||
final spotlightLeft = (24 * scale).clamp(18.0, 40.0).toDouble();
|
||||
final spotlightBackground = tester.widget<TvSpotlightBackground>(find.byType(TvSpotlightBackground));
|
||||
expect(spotlightBackground.contentLeft, closeTo(spotlightLeft + SideNavigationRailState.expandedWidth, 0.001));
|
||||
expect(
|
||||
tester.widget<TvBrowseRail>(find.byType(TvBrowseRail)).backgroundBleedLeft,
|
||||
SideNavigationRailState.expandedWidth,
|
||||
expect(spotlightBackground.contentLeft, closeTo(spotlightLeft + sidebarOffset, 0.001));
|
||||
|
||||
final railHeight = TvBrowseRailLayout.estimateHeight(
|
||||
size: const Size(1280, 720),
|
||||
hubs: [hub],
|
||||
density: LibraryDensity.max,
|
||||
episodePosterMode: settings.read(SettingsService.episodePosterMode),
|
||||
tallPosterScale: TvBrowseRailLayout.compactTallPosterScale,
|
||||
);
|
||||
final minimumSpotlightBottom = railHeight + (8 * scale);
|
||||
final baseSpotlightBottom = (720 * 0.48).clamp(160.0, 820.0).toDouble();
|
||||
final desiredSpotlightBottom = minimumSpotlightBottom > baseSpotlightBottom
|
||||
? minimumSpotlightBottom
|
||||
: baseSpotlightBottom;
|
||||
final maxSpotlightBottom = (720 - ((720 * 0.075).clamp(64.0 * scale, 120.0 * scale)) - (96 * scale))
|
||||
.clamp(0.0, double.infinity)
|
||||
.toDouble();
|
||||
final expectedSpotlightBottom = desiredSpotlightBottom > maxSpotlightBottom
|
||||
? maxSpotlightBottom
|
||||
: desiredSpotlightBottom;
|
||||
expect(spotlightBackground.contentBottom, closeTo(expectedSpotlightBottom, 0.001));
|
||||
|
||||
final browseRail = tester.widget<TvBrowseRail>(find.byType(TvBrowseRail));
|
||||
expect(browseRail.backgroundBleedLeft, sidebarOffset);
|
||||
expect(browseRail.visibleRightInset, sidebarOffset);
|
||||
|
||||
final backgroundPosition = tester.widget<Positioned>(
|
||||
find.ancestor(of: find.byType(TvSpotlightBackground), matching: find.byType(Positioned)).first,
|
||||
);
|
||||
expect(backgroundPosition.left, -SideNavigationRailState.expandedWidth);
|
||||
expect(backgroundPosition.left, -sidebarOffset);
|
||||
expect(backgroundPosition.width, 1280);
|
||||
|
||||
tester.state<FocusableActionBarState>(find.byType(FocusableActionBar)).requestFocusOnFirst();
|
||||
await tester.pump();
|
||||
expect(FocusManager.instance.primaryFocus?.debugLabel, 'ActionBar[0]');
|
||||
|
||||
(discoverKey.currentState! as FocusableTab).focusActiveTabIfReady();
|
||||
(discoverKey.currentState! as TabVisibilityAware).onTabHidden();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(FocusManager.instance.primaryFocus?.debugLabel, 'ActionBar[0]');
|
||||
|
||||
(discoverKey.currentState! as TabVisibilityAware).onTabShown();
|
||||
(discoverKey.currentState! as FocusableTab).focusActiveTabIfReady();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
@@ -130,17 +130,109 @@ void main() {
|
||||
expect(find.text('Season 1'), findsOneWidget);
|
||||
expect(find.text('Season 2'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('TV detail falls back to per-season episodes when descendant cache fails', (tester) async {
|
||||
await SettingsService.getInstance();
|
||||
|
||||
final show = MediaItem(
|
||||
id: 'show_1',
|
||||
backend: MediaBackend.jellyfin,
|
||||
kind: MediaKind.show,
|
||||
title: 'The Show',
|
||||
serverId: 'server_1',
|
||||
serverName: 'Server',
|
||||
);
|
||||
final season1 = MediaItem(
|
||||
id: 'season_1',
|
||||
backend: MediaBackend.jellyfin,
|
||||
kind: MediaKind.season,
|
||||
title: 'Season 1',
|
||||
index: 1,
|
||||
parentId: show.id,
|
||||
serverId: show.serverId,
|
||||
serverName: show.serverName,
|
||||
);
|
||||
final season2 = MediaItem(
|
||||
id: 'season_2',
|
||||
backend: MediaBackend.jellyfin,
|
||||
kind: MediaKind.season,
|
||||
title: 'Season 2',
|
||||
index: 2,
|
||||
parentId: show.id,
|
||||
serverId: show.serverId,
|
||||
serverName: show.serverName,
|
||||
);
|
||||
final episode1 = MediaItem(
|
||||
id: 'episode_1',
|
||||
backend: MediaBackend.jellyfin,
|
||||
kind: MediaKind.episode,
|
||||
title: 'Episode 1',
|
||||
index: 1,
|
||||
parentId: season1.id,
|
||||
parentIndex: season1.index,
|
||||
grandparentId: show.id,
|
||||
serverId: show.serverId,
|
||||
serverName: show.serverName,
|
||||
);
|
||||
final episode2 = MediaItem(
|
||||
id: 'episode_2',
|
||||
backend: MediaBackend.jellyfin,
|
||||
kind: MediaKind.episode,
|
||||
title: 'Episode 2',
|
||||
index: 1,
|
||||
parentId: season2.id,
|
||||
parentIndex: season2.index,
|
||||
grandparentId: show.id,
|
||||
serverId: show.serverId,
|
||||
serverName: show.serverName,
|
||||
);
|
||||
|
||||
final client = _FakeMediaServerClient(
|
||||
show: show,
|
||||
childrenByParent: {
|
||||
show.id: [season1, season2],
|
||||
season1.id: [episode1],
|
||||
season2.id: [episode2],
|
||||
},
|
||||
playableDescendantsError: Exception('descendant cache failed'),
|
||||
);
|
||||
final manager = MultiServerManager()..debugRegisterClientForTesting(client);
|
||||
final provider = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
addTearDown(provider.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
TranslationProvider(
|
||||
child: ChangeNotifierProvider<MultiServerProvider>.value(
|
||||
value: provider,
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
home: SizedBox(width: 1280, height: 720, child: MediaDetailScreen(metadata: show)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
|
||||
expect(find.text('Season 1'), findsOneWidget);
|
||||
expect(find.text('Season 2'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
class _FakeMediaServerClient implements MediaServerClient {
|
||||
final MediaItem show;
|
||||
final Map<String, List<MediaItem>> childrenByParent;
|
||||
final Future<List<MediaItem>> pendingPlayableDescendants;
|
||||
final Future<List<MediaItem>>? pendingPlayableDescendants;
|
||||
final Object? playableDescendantsError;
|
||||
|
||||
_FakeMediaServerClient({
|
||||
required this.show,
|
||||
required this.childrenByParent,
|
||||
required this.pendingPlayableDescendants,
|
||||
this.pendingPlayableDescendants,
|
||||
this.playableDescendantsError,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -169,7 +261,9 @@ class _FakeMediaServerClient implements MediaServerClient {
|
||||
int? size,
|
||||
AbortController? abort,
|
||||
}) async {
|
||||
final items = await pendingPlayableDescendants;
|
||||
final error = playableDescendantsError;
|
||||
if (error != null) throw error;
|
||||
final items = await pendingPlayableDescendants!;
|
||||
return LibraryPage(items: items, totalCount: items.length, offset: start ?? 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -239,6 +239,64 @@ void main() {
|
||||
expect(reports.last, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('Apple TV D-pad focus skips hidden downloads item', (tester) async {
|
||||
TvDetectionService.debugSetAppleTVOverride(true);
|
||||
addTearDown(() => TvDetectionService.debugSetAppleTVOverride(null));
|
||||
await SettingsService.getInstance();
|
||||
|
||||
final librariesProvider = LibrariesProvider();
|
||||
addTearDown(librariesProvider.dispose);
|
||||
|
||||
final hiddenLibrariesProvider = HiddenLibrariesProvider();
|
||||
await hiddenLibrariesProvider.ensureInitialized();
|
||||
addTearDown(hiddenLibrariesProvider.dispose);
|
||||
|
||||
final manager = MultiServerManager();
|
||||
final aggregation = DataAggregationService(manager);
|
||||
final multiServerProvider = MultiServerProvider(manager, aggregation);
|
||||
addTearDown(multiServerProvider.dispose);
|
||||
|
||||
final sideNavKey = GlobalKey<SideNavigationRailState>();
|
||||
NavigationTabId? selectedTab;
|
||||
|
||||
await tester.pumpWidget(
|
||||
TranslationProvider(
|
||||
child: MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<LibrariesProvider>.value(value: librariesProvider),
|
||||
ChangeNotifierProvider<HiddenLibrariesProvider>.value(value: hiddenLibrariesProvider),
|
||||
ChangeNotifierProvider<MultiServerProvider>.value(value: multiServerProvider),
|
||||
],
|
||||
child: MaterialApp(
|
||||
theme: ThemeData(extensions: const [_testTokens]),
|
||||
home: Scaffold(
|
||||
body: SideNavigationRail(
|
||||
key: sideNavKey,
|
||||
selectedTab: NavigationTabId.discover,
|
||||
isSidebarFocused: true,
|
||||
alwaysExpanded: true,
|
||||
onDestinationSelected: (tab) => selectedTab = tab,
|
||||
onLibrarySelected: (_) {},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
sideNavKey.currentState!.focusActiveItem();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Home -> Libraries -> Search -> Settings. Downloads is hidden on Apple TV.
|
||||
await _press(tester, LogicalKeyboardKey.arrowDown);
|
||||
await _press(tester, LogicalKeyboardKey.arrowDown);
|
||||
await _press(tester, LogicalKeyboardKey.arrowDown);
|
||||
await _press(tester, LogicalKeyboardKey.enter);
|
||||
|
||||
expect(selectedTab, NavigationTabId.settings);
|
||||
});
|
||||
|
||||
testWidgets('D-pad down from a hidden server header focuses that hidden server library', (tester) async {
|
||||
await SettingsService.getInstance();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user