fix(tv): push sidebar content without resizing
This commit is contained in:
@@ -1526,10 +1526,12 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
context,
|
||||
alwaysKeepSidebarOpen: svc.read(SettingsService.alwaysKeepSidebarOpen),
|
||||
);
|
||||
final railSize = MainScreenFocusScope.foregroundSizeOf(context);
|
||||
final fullBleedWidth = MainScreenFocusScope.fullBleedWidthOf(context);
|
||||
final railHeight = browseHubs.isEmpty
|
||||
? 0.0
|
||||
: TvBrowseRailLayout.estimateHeight(
|
||||
size: size,
|
||||
size: railSize,
|
||||
hubs: browseHubs,
|
||||
density: svc.read(SettingsService.libraryDensity),
|
||||
episodePosterMode: svc.read(SettingsService.episodePosterMode),
|
||||
@@ -1554,7 +1556,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: -sidebarBleed,
|
||||
width: size.width,
|
||||
width: fullBleedWidth,
|
||||
child: TvSpotlightBackground(
|
||||
item: spotlight,
|
||||
client: _getMediaClientForItem(spotlight),
|
||||
@@ -1613,13 +1615,12 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
onNavigateToSidebar: _navigateToSidebar,
|
||||
tallPosterScale: TvBrowseRailLayout.compactTallPosterScale,
|
||||
backgroundBleedLeft: sidebarBleed,
|
||||
visibleRightInset: sidebarBleed,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: -sidebarBleed,
|
||||
width: size.width,
|
||||
width: fullBleedWidth,
|
||||
child: ExcludeFocusTraversal(child: _buildOverlaidAppBar()),
|
||||
),
|
||||
if (_switchingProfile) const ProfileSwitchingOverlay(),
|
||||
|
||||
@@ -306,10 +306,12 @@ class _LibraryRecommendedTabState extends BaseLibraryTabState<MediaHub, LibraryR
|
||||
context,
|
||||
alwaysKeepSidebarOpen: svc.read(SettingsService.alwaysKeepSidebarOpen),
|
||||
);
|
||||
final railSize = MainScreenFocusScope.foregroundSizeOf(context);
|
||||
final fullBleedWidth = MainScreenFocusScope.fullBleedWidthOf(context);
|
||||
final railHeight = tvHubs.isEmpty
|
||||
? 0.0
|
||||
: TvBrowseRailLayout.estimateHeight(
|
||||
size: size,
|
||||
size: railSize,
|
||||
hubs: tvHubs,
|
||||
density: svc.read(SettingsService.libraryDensity),
|
||||
episodePosterMode: svc.read(SettingsService.episodePosterMode),
|
||||
@@ -336,7 +338,7 @@ class _LibraryRecommendedTabState extends BaseLibraryTabState<MediaHub, LibraryR
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: -sidebarBleed,
|
||||
width: size.width,
|
||||
width: fullBleedWidth,
|
||||
child: TvSpotlightBackground(
|
||||
item: spotlight,
|
||||
client: client,
|
||||
@@ -366,7 +368,6 @@ class _LibraryRecommendedTabState extends BaseLibraryTabState<MediaHub, LibraryR
|
||||
onBack: widget.onBack,
|
||||
tallPosterScale: TvBrowseRailLayout.compactTallPosterScale,
|
||||
backgroundBleedLeft: sidebarBleed,
|
||||
visibleRightInset: sidebarBleed,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -61,6 +61,9 @@ class MainScreenFocusScope extends InheritedWidget {
|
||||
final VoidCallback focusContent;
|
||||
final bool isSidebarFocused;
|
||||
final double sideNavigationWidth;
|
||||
final double? reservedSideNavigationWidth;
|
||||
final double? foregroundWidth;
|
||||
final double? viewportWidth;
|
||||
final void Function(String libraryGlobalKey)? selectLibrary;
|
||||
|
||||
const MainScreenFocusScope({
|
||||
@@ -69,6 +72,9 @@ class MainScreenFocusScope extends InheritedWidget {
|
||||
required this.focusContent,
|
||||
required this.isSidebarFocused,
|
||||
required this.sideNavigationWidth,
|
||||
this.reservedSideNavigationWidth,
|
||||
this.foregroundWidth,
|
||||
this.viewportWidth,
|
||||
this.selectLibrary,
|
||||
required super.child,
|
||||
});
|
||||
@@ -83,16 +89,45 @@ class MainScreenFocusScope extends InheritedWidget {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
static double clippedContentRightInsetOf(BuildContext context) {
|
||||
return of(context)?.sideNavigationWidth ?? 0.0;
|
||||
static double foregroundWidthOf(BuildContext context) {
|
||||
final width = of(context)?.foregroundWidth;
|
||||
if (width != null && width > 0) return width;
|
||||
return MediaQuery.sizeOf(context).width;
|
||||
}
|
||||
|
||||
static Size foregroundSizeOf(BuildContext context) {
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
return Size(foregroundWidthOf(context), size.height);
|
||||
}
|
||||
|
||||
static double fullBleedWidthOf(BuildContext context) {
|
||||
final width = of(context)?.viewportWidth;
|
||||
if (width != null && width > 0) return width;
|
||||
return MediaQuery.sizeOf(context).width;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(MainScreenFocusScope oldWidget) {
|
||||
return isSidebarFocused != oldWidget.isSidebarFocused || sideNavigationWidth != oldWidget.sideNavigationWidth;
|
||||
return isSidebarFocused != oldWidget.isSidebarFocused ||
|
||||
sideNavigationWidth != oldWidget.sideNavigationWidth ||
|
||||
reservedSideNavigationWidth != oldWidget.reservedSideNavigationWidth ||
|
||||
foregroundWidth != oldWidget.foregroundWidth ||
|
||||
viewportWidth != oldWidget.viewportWidth;
|
||||
}
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
({double left, double width}) mainScreenSideNavigationContentLayout({
|
||||
required double viewportWidth,
|
||||
required double currentSideNavigationWidth,
|
||||
required double reservedSideNavigationWidth,
|
||||
}) {
|
||||
return (
|
||||
left: currentSideNavigationWidth,
|
||||
width: (viewportWidth - reservedSideNavigationWidth).clamp(0.0, double.infinity).toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
bool shouldRetryActiveProfileBindAfterReconnect({
|
||||
required bool hasActiveProfile,
|
||||
@@ -1237,6 +1272,9 @@ class _MainScreenState extends State<MainScreen>
|
||||
pref: SettingsService.alwaysKeepSidebarOpen,
|
||||
builder: (context, alwaysExpanded, _) {
|
||||
final targetContentOffset = _sideNavigationWidth(context, alwaysExpanded: alwaysExpanded);
|
||||
final reservedContentOffset = alwaysExpanded
|
||||
? SideNavigationRailState.expandedWidth
|
||||
: SideNavigationRailState.collapsedWidthForContext(context);
|
||||
|
||||
return OverlaySheetHost(
|
||||
child: PopScope(
|
||||
@@ -1262,24 +1300,32 @@ class _MainScreenState extends State<MainScreen>
|
||||
child: _buildTickerAwareStack(),
|
||||
),
|
||||
builder: (context, contentLeftPadding, contentChild) {
|
||||
return MainScreenFocusScope(
|
||||
focusSidebar: _focusSidebar,
|
||||
focusContent: _focusContent,
|
||||
isSidebarFocused: _isSidebarFocused,
|
||||
sideNavigationWidth: contentLeftPadding,
|
||||
selectLibrary: _selectLibrary,
|
||||
child: SideNavigationScope(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final contentWidth = constraints.maxWidth;
|
||||
return Stack(
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final viewportWidth = constraints.maxWidth;
|
||||
final contentLayout = mainScreenSideNavigationContentLayout(
|
||||
viewportWidth: viewportWidth,
|
||||
currentSideNavigationWidth: contentLeftPadding,
|
||||
reservedSideNavigationWidth: reservedContentOffset,
|
||||
);
|
||||
return MainScreenFocusScope(
|
||||
focusSidebar: _focusSidebar,
|
||||
focusContent: _focusContent,
|
||||
isSidebarFocused: _isSidebarFocused,
|
||||
sideNavigationWidth: contentLeftPadding,
|
||||
reservedSideNavigationWidth: reservedContentOffset,
|
||||
foregroundWidth: contentLayout.width,
|
||||
viewportWidth: viewportWidth,
|
||||
selectLibrary: _selectLibrary,
|
||||
child: SideNavigationScope(
|
||||
child: Stack(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
children: [
|
||||
Positioned(
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: contentLeftPadding,
|
||||
width: contentWidth,
|
||||
left: contentLayout.left,
|
||||
width: contentLayout.width,
|
||||
child: contentChild!,
|
||||
),
|
||||
Positioned(
|
||||
@@ -1311,10 +1357,10 @@ class _MainScreenState extends State<MainScreen>
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -136,7 +136,6 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab, Moun
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final rightInset = MainScreenFocusScope.clippedContentRightInsetOf(context);
|
||||
return Scaffold(
|
||||
body: Focus(
|
||||
onKeyEvent: _handleKeyEvent,
|
||||
@@ -144,42 +143,39 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab, Moun
|
||||
primary: false,
|
||||
slivers: [
|
||||
ExcludeFocus(child: CustomAppBar(title: Text(t.settings.title), pinned: true)),
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(right: rightInset),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
if (DonationService.isEnabled) _buildDonateTile(),
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
if (DonationService.isEnabled) _buildDonateTile(),
|
||||
|
||||
_buildAppearanceTile(),
|
||||
_buildAppearanceTile(),
|
||||
|
||||
_buildPlaybackTile(),
|
||||
_buildPlaybackTile(),
|
||||
|
||||
_buildTrackersTile(),
|
||||
_buildTrackersTile(),
|
||||
|
||||
_buildConnectionsSection(),
|
||||
_buildConnectionsSection(),
|
||||
|
||||
_buildProfilesSection(),
|
||||
_buildProfilesSection(),
|
||||
|
||||
if (!PlatformDetector.isAppleTV()) _buildDownloadsSection(),
|
||||
if (!PlatformDetector.isAppleTV()) _buildDownloadsSection(),
|
||||
|
||||
if (_keyboardShortcutsSupported) ...[_buildKeyboardShortcutsSection()],
|
||||
if (_keyboardShortcutsSupported) ...[_buildKeyboardShortcutsSection()],
|
||||
|
||||
_buildAdvancedSection(),
|
||||
_buildAdvancedSection(),
|
||||
|
||||
if (UpdateService.isUpdateCheckEnabled) ...[_buildUpdateSection()],
|
||||
if (UpdateService.isUpdateCheckEnabled) ...[_buildUpdateSection()],
|
||||
|
||||
if (!PlatformDetector.isTV()) _buildBackupSection(),
|
||||
if (!PlatformDetector.isTV()) _buildBackupSection(),
|
||||
|
||||
SettingNavigationTile(
|
||||
focusNode: _focusTracker.get(_kAbout),
|
||||
icon: Symbols.info_rounded,
|
||||
title: t.settings.about,
|
||||
subtitle: t.settings.aboutDescription,
|
||||
destinationBuilder: (context) => const AboutScreen(),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
]),
|
||||
),
|
||||
SettingNavigationTile(
|
||||
focusNode: _focusTracker.get(_kAbout),
|
||||
icon: Symbols.info_rounded,
|
||||
title: t.settings.about,
|
||||
subtitle: t.settings.aboutDescription,
|
||||
destinationBuilder: (context) => const AboutScreen(),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -59,7 +59,6 @@ void scrollListToIndex(
|
||||
int index, {
|
||||
required double itemExtent,
|
||||
double leadingPadding = 12.0,
|
||||
double visibleTrailingInset = 0.0,
|
||||
bool animate = true,
|
||||
}) {
|
||||
if (controller.positions.length != 1 || itemExtent <= 0) return;
|
||||
@@ -68,8 +67,7 @@ void scrollListToIndex(
|
||||
final maxExtent = controller.position.maxScrollExtent;
|
||||
if (!viewport.isFinite || !maxExtent.isFinite) return;
|
||||
final targetCenter = leadingPadding + (index * itemExtent) + (itemExtent / 2);
|
||||
final visibleViewport = (viewport - visibleTrailingInset).clamp(1.0, double.infinity).toDouble();
|
||||
final desiredOffset = (targetCenter - (visibleViewport / 2)).clamp(0.0, maxExtent);
|
||||
final desiredOffset = (targetCenter - (viewport / 2)).clamp(0.0, maxExtent);
|
||||
|
||||
if (animate) {
|
||||
controller.animateTo(desiredOffset, duration: const Duration(milliseconds: 150), curve: Curves.easeOut);
|
||||
|
||||
@@ -19,7 +19,6 @@ import '../media/media_hub.dart';
|
||||
import '../media/media_item.dart';
|
||||
import '../mixins/mounted_set_state_mixin.dart';
|
||||
import '../screens/hub_detail_screen.dart';
|
||||
import '../screens/main_screen.dart';
|
||||
import '../utils/media_navigation_helper.dart';
|
||||
import 'focus_builders.dart';
|
||||
import 'media_card.dart';
|
||||
@@ -107,7 +106,6 @@ class HubSectionState extends State<HubSection> with MountedSetStateMixin {
|
||||
? TvLayoutConstants.shelfHorizontalInset
|
||||
: 12.0;
|
||||
double get _leadingPadding => _leadingPaddingFor(PlatformDetector.isTV());
|
||||
double get _visibleRightInset => MainScreenFocusScope.clippedContentRightInsetOf(context);
|
||||
|
||||
Timer? _longPressTimer;
|
||||
bool _isSelectKeyDown = false;
|
||||
@@ -211,7 +209,6 @@ class HubSectionState extends State<HubSection> with MountedSetStateMixin {
|
||||
index,
|
||||
itemExtent: _itemExtent,
|
||||
leadingPadding: _leadingPadding,
|
||||
visibleTrailingInset: _visibleRightInset,
|
||||
animate: animate,
|
||||
);
|
||||
}
|
||||
@@ -487,13 +484,8 @@ class HubSectionState extends State<HubSection> with MountedSetStateMixin {
|
||||
scrollDirection: Axis.horizontal,
|
||||
clipBehavior: Clip.none,
|
||||
padding: widget.inset
|
||||
? EdgeInsets.fromLTRB(0, isTv ? 6 : 2, _visibleRightInset + 4, isTv ? 6 : 2)
|
||||
: EdgeInsets.fromLTRB(
|
||||
isTv ? leadingPadding : 8,
|
||||
isTv ? 6 : 2,
|
||||
(isTv ? leadingPadding : 8) + _visibleRightInset,
|
||||
isTv ? 6 : 2,
|
||||
),
|
||||
? EdgeInsets.symmetric(vertical: isTv ? 6 : 2)
|
||||
: EdgeInsets.symmetric(horizontal: isTv ? leadingPadding : 8, vertical: isTv ? 6 : 2),
|
||||
itemCount: isKeyboardMode ? _totalItemCount : widget.hub.items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final isItemFocused = hasFocus && index == _focusedIndex;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../screens/main_screen.dart';
|
||||
import 'focused_scroll_scaffold.dart';
|
||||
|
||||
/// Standard scaffold for settings pages made of ordinary list rows.
|
||||
@@ -39,23 +38,13 @@ class SettingsPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pageSlivers = slivers ?? [_buildListSliver()];
|
||||
final rightInset = MainScreenFocusScope.clippedContentRightInsetOf(context);
|
||||
final insetSlivers = rightInset == 0
|
||||
? pageSlivers
|
||||
: [
|
||||
for (final sliver in pageSlivers)
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(right: rightInset),
|
||||
sliver: sliver,
|
||||
),
|
||||
];
|
||||
return FocusedScrollScaffold(
|
||||
title: title,
|
||||
actions: actions,
|
||||
pinned: pinned,
|
||||
automaticallyImplyLeading: automaticallyImplyLeading,
|
||||
onBackPressed: onBackPressed,
|
||||
slivers: insetSlivers,
|
||||
slivers: pageSlivers,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -181,11 +182,10 @@ class TvBrowseRailLayout {
|
||||
required TvBrowseRailLayoutMetrics metrics,
|
||||
required double viewportWidth,
|
||||
required double scale,
|
||||
double visibleTrailingInset = 0.0,
|
||||
}) {
|
||||
final itemContentWidth = hub.items.length * (metrics.cardWidth + metrics.itemGap);
|
||||
final moreContentWidth = hub.more ? (132 * scale) + metrics.itemGap : 0.0;
|
||||
final contentWidth = (metrics.railEdgePadding * 2) + itemContentWidth + moreContentWidth + visibleTrailingInset;
|
||||
final contentWidth = (metrics.railEdgePadding * 2) + itemContentWidth + moreContentWidth;
|
||||
return (contentWidth - viewportWidth).clamp(0.0, double.infinity).toDouble();
|
||||
}
|
||||
|
||||
@@ -194,12 +194,10 @@ class TvBrowseRailLayout {
|
||||
required TvBrowseRailLayoutMetrics metrics,
|
||||
required double viewportWidth,
|
||||
required double maxScrollExtent,
|
||||
double visibleTrailingInset = 0.0,
|
||||
}) {
|
||||
final itemExtent = metrics.cardWidth + metrics.itemGap;
|
||||
final targetCenter = metrics.railEdgePadding + (index * itemExtent) + (itemExtent / 2);
|
||||
final visibleViewport = (viewportWidth - visibleTrailingInset).clamp(1.0, double.infinity).toDouble();
|
||||
return (targetCenter - (visibleViewport / 2)).clamp(0.0, maxScrollExtent).toDouble();
|
||||
return (targetCenter - (viewportWidth / 2)).clamp(0.0, maxScrollExtent).toDouble();
|
||||
}
|
||||
|
||||
static double estimateHeight({
|
||||
@@ -260,7 +258,6 @@ class TvBrowseRail extends StatefulWidget {
|
||||
final EpisodePosterMode Function(MediaHub hub)? episodePosterModeForHub;
|
||||
final double Function(MediaHub hub)? widePosterScaleForHub;
|
||||
final double backgroundBleedLeft;
|
||||
final double visibleRightInset;
|
||||
|
||||
const TvBrowseRail({
|
||||
super.key,
|
||||
@@ -285,7 +282,6 @@ class TvBrowseRail extends StatefulWidget {
|
||||
this.episodePosterModeForHub,
|
||||
this.widePosterScaleForHub,
|
||||
this.backgroundBleedLeft = 0,
|
||||
this.visibleRightInset = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -630,7 +626,6 @@ class TvBrowseRailState extends State<TvBrowseRail> {
|
||||
_itemIndex,
|
||||
itemExtent: _itemExtent,
|
||||
leadingPadding: _railLeadingPadding,
|
||||
visibleTrailingInset: widget.visibleRightInset,
|
||||
animate: animate,
|
||||
);
|
||||
}
|
||||
@@ -655,14 +650,12 @@ class TvBrowseRailState extends State<TvBrowseRail> {
|
||||
metrics: metrics,
|
||||
viewportWidth: viewportWidth,
|
||||
scale: scale,
|
||||
visibleTrailingInset: widget.visibleRightInset,
|
||||
);
|
||||
final initialScrollOffset = TvBrowseRailLayout.scrollOffsetForIndex(
|
||||
index: initialItemIndex,
|
||||
metrics: metrics,
|
||||
viewportWidth: viewportWidth,
|
||||
maxScrollExtent: maxScrollExtent,
|
||||
visibleTrailingInset: widget.visibleRightInset,
|
||||
);
|
||||
return ScrollController(initialScrollOffset: initialScrollOffset);
|
||||
});
|
||||
@@ -733,6 +726,7 @@ class TvBrowseRailState extends State<TvBrowseRail> {
|
||||
scale,
|
||||
).clamp(0.0, horizontalInset).toDouble();
|
||||
final width = constraints.maxWidth.isFinite ? constraints.maxWidth : MediaQuery.sizeOf(context).width;
|
||||
final backgroundWidth = math.max(width + widget.backgroundBleedLeft, MediaQuery.sizeOf(context).width);
|
||||
final availableWidth = (width - horizontalInset).clamp(1.0, double.infinity).toDouble();
|
||||
final railViewportWidth = (availableWidth + interactionExpansion).clamp(1.0, double.infinity).toDouble();
|
||||
final density = svc.read(SettingsService.libraryDensity);
|
||||
@@ -798,7 +792,7 @@ class TvBrowseRailState extends State<TvBrowseRail> {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: -widget.backgroundBleedLeft,
|
||||
width: width,
|
||||
width: backgroundWidth,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
@@ -987,12 +981,7 @@ class TvBrowseRailState extends State<TvBrowseRail> {
|
||||
controller: scrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
clipBehavior: Clip.none,
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
metrics.railEdgePadding,
|
||||
2 * scale,
|
||||
metrics.railEdgePadding + widget.visibleRightInset,
|
||||
6 * scale,
|
||||
),
|
||||
padding: EdgeInsets.fromLTRB(metrics.railEdgePadding, 2 * scale, metrics.railEdgePadding, 6 * scale),
|
||||
itemCount: totalCount,
|
||||
itemBuilder: (context, itemIndex) {
|
||||
final isFocused = hasFocus && isActiveHub && itemIndex == _itemIndex;
|
||||
|
||||
@@ -131,7 +131,17 @@ void main() {
|
||||
focusContent: () {},
|
||||
isSidebarFocused: false,
|
||||
sideNavigationWidth: sidebarOffset,
|
||||
child: SizedBox(width: 1280, height: 720, child: DiscoverScreen(key: discoverKey)),
|
||||
reservedSideNavigationWidth: sidebarOffset,
|
||||
foregroundWidth: 1280 - sidebarOffset,
|
||||
viewportWidth: 1280,
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: SizedBox(
|
||||
width: 1280 - sidebarOffset,
|
||||
height: 720,
|
||||
child: DiscoverScreen(key: discoverKey),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -147,7 +157,7 @@ void main() {
|
||||
expect(spotlightBackground.contentLeft, closeTo(spotlightLeft + sidebarOffset, 0.001));
|
||||
|
||||
final railHeight = TvBrowseRailLayout.estimateHeight(
|
||||
size: const Size(1280, 720),
|
||||
size: const Size(1280 - sidebarOffset, 720),
|
||||
hubs: [hub],
|
||||
density: LibraryDensity.max,
|
||||
episodePosterMode: settings.read(SettingsService.episodePosterMode),
|
||||
@@ -168,7 +178,6 @@ void main() {
|
||||
|
||||
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,
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/screens/main_screen.dart';
|
||||
import 'package:plezy/widgets/side_navigation_rail.dart';
|
||||
|
||||
void main() {
|
||||
test('side navigation pushes stable foreground off-screen while temporarily expanded', () {
|
||||
const viewportWidth = 1280.0;
|
||||
const reservedWidth = SideNavigationRailState.tvCollapsedWidth;
|
||||
|
||||
final collapsed = mainScreenSideNavigationContentLayout(
|
||||
viewportWidth: viewportWidth,
|
||||
currentSideNavigationWidth: SideNavigationRailState.tvCollapsedWidth,
|
||||
reservedSideNavigationWidth: reservedWidth,
|
||||
);
|
||||
final expanded = mainScreenSideNavigationContentLayout(
|
||||
viewportWidth: viewportWidth,
|
||||
currentSideNavigationWidth: SideNavigationRailState.expandedWidth,
|
||||
reservedSideNavigationWidth: reservedWidth,
|
||||
);
|
||||
|
||||
expect(collapsed.width, viewportWidth - SideNavigationRailState.tvCollapsedWidth);
|
||||
expect(expanded.width, collapsed.width);
|
||||
expect(collapsed.left, SideNavigationRailState.tvCollapsedWidth);
|
||||
expect(expanded.left, SideNavigationRailState.expandedWidth);
|
||||
expect(collapsed.left + collapsed.width, viewportWidth);
|
||||
expect(expanded.left + expanded.width, viewportWidth + SideNavigationRailState.expandedWidth - reservedWidth);
|
||||
});
|
||||
|
||||
test('side navigation reserves expanded width when always open', () {
|
||||
const viewportWidth = 1280.0;
|
||||
|
||||
final expanded = mainScreenSideNavigationContentLayout(
|
||||
viewportWidth: viewportWidth,
|
||||
currentSideNavigationWidth: SideNavigationRailState.expandedWidth,
|
||||
reservedSideNavigationWidth: SideNavigationRailState.expandedWidth,
|
||||
);
|
||||
|
||||
expect(expanded.left, SideNavigationRailState.expandedWidth);
|
||||
expect(expanded.width, viewportWidth - SideNavigationRailState.expandedWidth);
|
||||
expect(expanded.left + expanded.width, viewportWidth);
|
||||
});
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
import 'package:plezy/services/settings_service.dart';
|
||||
import 'package:plezy/theme/mono_theme.dart';
|
||||
import 'package:plezy/widgets/side_navigation_rail.dart';
|
||||
import 'package:plezy/widgets/tv_browse_rail.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -648,6 +649,53 @@ void main() {
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('background gradient stays full bleed beside pushed foreground', (tester) async {
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
tester.view.physicalSize = const Size(1280, 720);
|
||||
addTearDown(() {
|
||||
tester.view.resetDevicePixelRatio();
|
||||
tester.view.resetPhysicalSize();
|
||||
});
|
||||
|
||||
final serverManager = MultiServerManager();
|
||||
final item = MediaItem(id: 'movie_1', backend: MediaBackend.plex, kind: MediaKind.movie, title: 'Movie');
|
||||
final hub = MediaHub(id: 'hub_1', title: 'Hub', type: 'movie', items: [item], size: 1);
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<MultiServerProvider>(
|
||||
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
home: Scaffold(
|
||||
body: SizedBox(
|
||||
width: 1060,
|
||||
height: 720,
|
||||
child: TvBrowseRail(
|
||||
hubs: [hub],
|
||||
iconForHub: (_, _) => Icons.movie_rounded,
|
||||
backgroundBleedLeft: SideNavigationRailState.expandedWidth,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
final gradient = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is DecoratedBox &&
|
||||
widget.decoration is BoxDecoration &&
|
||||
(widget.decoration as BoxDecoration).gradient is LinearGradient,
|
||||
);
|
||||
final backgroundPosition = tester.widget<Positioned>(
|
||||
find.ancestor(of: gradient.first, matching: find.byType(Positioned)).first,
|
||||
);
|
||||
|
||||
expect(backgroundPosition.left, -SideNavigationRailState.expandedWidth);
|
||||
expect(backgroundPosition.width, 1280);
|
||||
});
|
||||
}
|
||||
|
||||
ScrollPosition _activeRailPosition(WidgetTester tester) {
|
||||
|
||||
Reference in New Issue
Block a user