feat(tv): push content beside sidebar
This commit is contained in:
@@ -41,7 +41,6 @@ import '../providers/user_profile_provider.dart';
|
||||
import '../services/storage_service.dart';
|
||||
import '../services/settings_service.dart';
|
||||
import '../widgets/settings_builder.dart';
|
||||
import '../widgets/side_navigation_rail.dart';
|
||||
import '../widgets/tv_browse_rail.dart';
|
||||
import '../widgets/tv_spotlight_background.dart';
|
||||
import '../mixins/refreshable.dart';
|
||||
@@ -1535,9 +1534,10 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
final maxSpotlightBottom = (size.height - spotlightTop - (96 * scale)).clamp(0.0, double.infinity).toDouble();
|
||||
final spotlightBottom = desiredSpotlightBottom > maxSpotlightBottom ? maxSpotlightBottom : desiredSpotlightBottom;
|
||||
final spotlightLeft = (24 * scale).clamp(18.0, 40.0).toDouble();
|
||||
final sidebarBleed = svc.read(SettingsService.alwaysKeepSidebarOpen)
|
||||
? 0.0
|
||||
: SideNavigationRailState.collapsedWidthForContext(context);
|
||||
final sidebarBleed = MainScreenFocusScope.sideNavigationBleedOf(
|
||||
context,
|
||||
alwaysKeepSidebarOpen: svc.read(SettingsService.alwaysKeepSidebarOpen),
|
||||
);
|
||||
|
||||
return Material(
|
||||
color: theme.scaffoldBackgroundColor,
|
||||
|
||||
@@ -16,7 +16,6 @@ import '../../../utils/provider_extensions.dart';
|
||||
import '../../../utils/watch_state_notifier.dart';
|
||||
import '../../../widgets/hub_section.dart';
|
||||
import '../../../widgets/settings_builder.dart';
|
||||
import '../../../widgets/side_navigation_rail.dart';
|
||||
import '../../../widgets/tv_browse_rail.dart';
|
||||
import '../../../widgets/tv_spotlight_background.dart';
|
||||
import '../../main_screen.dart';
|
||||
@@ -321,9 +320,10 @@ class _LibraryRecommendedTabState extends BaseLibraryTabState<MediaHub, LibraryR
|
||||
final maxSpotlightBottom = (size.height - spotlightTop - (96 * scale)).clamp(0.0, double.infinity).toDouble();
|
||||
final spotlightBottom = desiredSpotlightBottom > maxSpotlightBottom ? maxSpotlightBottom : desiredSpotlightBottom;
|
||||
final spotlightLeft = (24 * scale).clamp(18.0, 40.0).toDouble();
|
||||
final sidebarBleed = svc.read(SettingsService.alwaysKeepSidebarOpen)
|
||||
? 0.0
|
||||
: SideNavigationRailState.collapsedWidthForContext(context);
|
||||
final sidebarBleed = MainScreenFocusScope.sideNavigationBleedOf(
|
||||
context,
|
||||
alwaysKeepSidebarOpen: svc.read(SettingsService.alwaysKeepSidebarOpen),
|
||||
);
|
||||
|
||||
return Material(
|
||||
color: theme.scaffoldBackgroundColor,
|
||||
|
||||
@@ -60,6 +60,7 @@ class MainScreenFocusScope extends InheritedWidget {
|
||||
final VoidCallback focusSidebar;
|
||||
final VoidCallback focusContent;
|
||||
final bool isSidebarFocused;
|
||||
final double sideNavigationWidth;
|
||||
final void Function(String libraryGlobalKey)? selectLibrary;
|
||||
|
||||
const MainScreenFocusScope({
|
||||
@@ -67,6 +68,7 @@ class MainScreenFocusScope extends InheritedWidget {
|
||||
required this.focusSidebar,
|
||||
required this.focusContent,
|
||||
required this.isSidebarFocused,
|
||||
required this.sideNavigationWidth,
|
||||
this.selectLibrary,
|
||||
required super.child,
|
||||
});
|
||||
@@ -75,9 +77,15 @@ class MainScreenFocusScope extends InheritedWidget {
|
||||
return context.dependOnInheritedWidgetOfExactType<MainScreenFocusScope>();
|
||||
}
|
||||
|
||||
static double sideNavigationBleedOf(BuildContext context, {required bool alwaysKeepSidebarOpen}) {
|
||||
final width = of(context)?.sideNavigationWidth;
|
||||
if (width != null) return width;
|
||||
return alwaysKeepSidebarOpen ? 0.0 : SideNavigationRailState.collapsedWidthForContext(context);
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(MainScreenFocusScope oldWidget) {
|
||||
return isSidebarFocused != oldWidget.isSidebarFocused;
|
||||
return isSidebarFocused != oldWidget.isSidebarFocused || sideNavigationWidth != oldWidget.sideNavigationWidth;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +158,7 @@ class _MainScreenState extends State<MainScreen>
|
||||
final FocusScopeNode _sidebarFocusScope = FocusScopeNode(debugLabel: 'Sidebar');
|
||||
final FocusScopeNode _contentFocusScope = FocusScopeNode(debugLabel: 'Content');
|
||||
bool _isSidebarFocused = false;
|
||||
bool _isSidebarInteractionExpanded = false;
|
||||
|
||||
/// The binder is now owned by a top-level [Provider] (see main.dart) so
|
||||
/// the splash can await its first settle before navigating here. We just
|
||||
@@ -893,6 +902,18 @@ class _MainScreenState extends State<MainScreen>
|
||||
});
|
||||
}
|
||||
|
||||
void _handleSidebarInteractionExpandedChanged(bool expanded) {
|
||||
if (_isSidebarInteractionExpanded == expanded) return;
|
||||
setState(() => _isSidebarInteractionExpanded = expanded);
|
||||
}
|
||||
|
||||
double _sideNavigationWidth(BuildContext context, {required bool alwaysExpanded}) {
|
||||
final isExpanded = alwaysExpanded || _isSidebarFocused || _isSidebarInteractionExpanded;
|
||||
return isExpanded
|
||||
? SideNavigationRailState.expandedWidth
|
||||
: SideNavigationRailState.collapsedWidthForContext(context);
|
||||
}
|
||||
|
||||
/// Suppress stray back events after a child route pops.
|
||||
/// On Android TV the platform popRoute can arrive before the key events,
|
||||
/// so BackKeySuppressorObserver misses them and they leak into _handleBackKey.
|
||||
@@ -1211,9 +1232,7 @@ class _MainScreenState extends State<MainScreen>
|
||||
return SettingValueBuilder<bool>(
|
||||
pref: SettingsService.alwaysKeepSidebarOpen,
|
||||
builder: (context, alwaysExpanded, _) {
|
||||
final contentLeftPadding = alwaysExpanded
|
||||
? SideNavigationRailState.expandedWidth
|
||||
: SideNavigationRailState.collapsedWidthForContext(context);
|
||||
final targetContentLeftPadding = _sideNavigationWidth(context, alwaysExpanded: alwaysExpanded);
|
||||
|
||||
return OverlaySheetHost(
|
||||
child: PopScope(
|
||||
@@ -1228,59 +1247,65 @@ class _MainScreenState extends State<MainScreen>
|
||||
if (searchResult == KeyEventResult.handled) return searchResult;
|
||||
return _handleBackKey(event);
|
||||
},
|
||||
child: MainScreenFocusScope(
|
||||
focusSidebar: _focusSidebar,
|
||||
focusContent: _focusContent,
|
||||
isSidebarFocused: _isSidebarFocused,
|
||||
selectLibrary: _selectLibrary,
|
||||
child: SideNavigationScope(
|
||||
child: Stack(
|
||||
children: [
|
||||
// Content with animated left padding based on sidebar state
|
||||
Positioned.fill(
|
||||
child: AnimatedPadding(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOutCubic,
|
||||
padding: EdgeInsets.only(left: contentLeftPadding),
|
||||
child: FocusScope(
|
||||
node: _contentFocusScope,
|
||||
// No autofocus - we control focus programmatically to prevent
|
||||
// autofocus from stealing focus back after setState() rebuilds
|
||||
child: _buildTickerAwareStack(),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Sidebar overlays content when expanded (unless always expanded)
|
||||
Positioned(
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
child: FocusScope(
|
||||
node: _sidebarFocusScope,
|
||||
child: SideNavigationRail(
|
||||
key: _sideNavKey,
|
||||
selectedTab: _currentTab,
|
||||
selectedLibraryKey: _selectedLibraryGlobalKey,
|
||||
isOfflineMode: _isOffline,
|
||||
isSidebarFocused: _isSidebarFocused,
|
||||
alwaysExpanded: alwaysExpanded,
|
||||
isReconnecting: _isReconnecting,
|
||||
onDestinationSelected: (tab) {
|
||||
_selectTab(tab);
|
||||
_focusContent();
|
||||
},
|
||||
onLibrarySelected: (key) {
|
||||
_selectLibrary(key);
|
||||
_focusContent();
|
||||
},
|
||||
onNavigateToContent: _focusContent,
|
||||
onReconnect: _triggerReconnect,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: TweenAnimationBuilder<double>(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOutCubic,
|
||||
tween: Tween<double>(end: targetContentLeftPadding),
|
||||
child: FocusScope(
|
||||
node: _contentFocusScope,
|
||||
// No autofocus - we control focus programmatically to prevent
|
||||
// autofocus from stealing focus back after setState() rebuilds
|
||||
child: _buildTickerAwareStack(),
|
||||
),
|
||||
builder: (context, contentLeftPadding, contentChild) {
|
||||
return MainScreenFocusScope(
|
||||
focusSidebar: _focusSidebar,
|
||||
focusContent: _focusContent,
|
||||
isSidebarFocused: _isSidebarFocused,
|
||||
sideNavigationWidth: contentLeftPadding,
|
||||
selectLibrary: _selectLibrary,
|
||||
child: SideNavigationScope(
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: contentLeftPadding),
|
||||
child: contentChild!,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
child: FocusScope(
|
||||
node: _sidebarFocusScope,
|
||||
child: SideNavigationRail(
|
||||
key: _sideNavKey,
|
||||
selectedTab: _currentTab,
|
||||
selectedLibraryKey: _selectedLibraryGlobalKey,
|
||||
isOfflineMode: _isOffline,
|
||||
isSidebarFocused: _isSidebarFocused,
|
||||
alwaysExpanded: alwaysExpanded,
|
||||
isReconnecting: _isReconnecting,
|
||||
onInteractionExpandedChanged: _handleSidebarInteractionExpandedChanged,
|
||||
onDestinationSelected: (tab) {
|
||||
_selectTab(tab);
|
||||
_focusContent();
|
||||
},
|
||||
onLibrarySelected: (key) {
|
||||
_selectLibrary(key);
|
||||
_focusContent();
|
||||
},
|
||||
onNavigateToContent: _focusContent,
|
||||
onReconnect: _triggerReconnect,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -169,6 +169,9 @@ class SideNavigationRail extends StatefulWidget {
|
||||
/// Called when RIGHT arrow is pressed to navigate to content without selecting.
|
||||
final VoidCallback? onNavigateToContent;
|
||||
|
||||
/// Called when hover/touch expansion changes, so the shell can reserve width.
|
||||
final ValueChanged<bool>? onInteractionExpandedChanged;
|
||||
|
||||
/// Called when the user taps the reconnect button in offline mode.
|
||||
final VoidCallback? onReconnect;
|
||||
|
||||
@@ -183,6 +186,7 @@ class SideNavigationRail extends StatefulWidget {
|
||||
required this.onDestinationSelected,
|
||||
required this.onLibrarySelected,
|
||||
this.onNavigateToContent,
|
||||
this.onInteractionExpandedChanged,
|
||||
this.onReconnect,
|
||||
});
|
||||
|
||||
@@ -195,6 +199,7 @@ class SideNavigationRailState extends State<SideNavigationRail> with MountedSetS
|
||||
|
||||
bool _isHovered = false;
|
||||
bool _isTouchExpanded = false;
|
||||
bool _lastReportedInteractionExpanded = false;
|
||||
Timer? _collapseTimer;
|
||||
static const double collapsedWidth = 80.0;
|
||||
static const double tvCollapsedWidth = 48.0;
|
||||
@@ -241,6 +246,8 @@ class SideNavigationRailState extends State<SideNavigationRail> with MountedSetS
|
||||
/// Whether the sidebar should be expanded (always, hover, or focus)
|
||||
bool get _shouldExpand => widget.alwaysExpanded || _isHovered || _isTouchExpanded || widget.isSidebarFocused;
|
||||
|
||||
bool get _interactionExpanded => _isHovered || _isTouchExpanded;
|
||||
|
||||
/// macOS has the system green button; mobile/TV have no OS fullscreen toggle.
|
||||
bool get _showFullscreenToggle => Platform.isWindows || Platform.isLinux;
|
||||
|
||||
@@ -268,16 +275,36 @@ class SideNavigationRailState extends State<SideNavigationRail> with MountedSetS
|
||||
super.didUpdateWidget(oldWidget);
|
||||
// Auto-collapse after navigation (selection changed)
|
||||
if (oldWidget.selectedTab != widget.selectedTab || oldWidget.selectedLibraryKey != widget.selectedLibraryKey) {
|
||||
final wasInteractionExpanded = _interactionExpanded;
|
||||
_isTouchExpanded = false;
|
||||
if (wasInteractionExpanded != _interactionExpanded) {
|
||||
_scheduleInteractionExpandedNotification();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _scheduleInteractionExpandedNotification() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
_notifyInteractionExpandedIfNeeded();
|
||||
});
|
||||
}
|
||||
|
||||
void _notifyInteractionExpandedIfNeeded() {
|
||||
final expanded = _interactionExpanded;
|
||||
if (_lastReportedInteractionExpanded == expanded) return;
|
||||
_lastReportedInteractionExpanded = expanded;
|
||||
widget.onInteractionExpandedChanged?.call(expanded);
|
||||
}
|
||||
|
||||
void _onHoverEnter() {
|
||||
_collapseTimer?.cancel();
|
||||
_isTouchExpanded = false; // Mouse takes over
|
||||
if (!_isHovered) {
|
||||
setState(() => _isHovered = true);
|
||||
}
|
||||
if (_isHovered && !_isTouchExpanded) return;
|
||||
setState(() {
|
||||
_isTouchExpanded = false; // Mouse takes over
|
||||
_isHovered = true;
|
||||
});
|
||||
_notifyInteractionExpandedIfNeeded();
|
||||
}
|
||||
|
||||
void _onHoverExit() {
|
||||
@@ -285,10 +312,17 @@ class SideNavigationRailState extends State<SideNavigationRail> with MountedSetS
|
||||
_collapseTimer = Timer(_collapseDelay, () {
|
||||
if (mounted && _isHovered) {
|
||||
setState(() => _isHovered = false);
|
||||
_notifyInteractionExpandedIfNeeded();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _expandForTouch() {
|
||||
if (_isTouchExpanded) return;
|
||||
setState(() => _isTouchExpanded = true);
|
||||
_notifyInteractionExpandedIfNeeded();
|
||||
}
|
||||
|
||||
/// The key of the last focused sidebar item (for pre-capture before focus shifts).
|
||||
String? get lastFocusedKey => _focusTracker.lastFocusedKey;
|
||||
|
||||
@@ -472,6 +506,7 @@ class SideNavigationRailState extends State<SideNavigationRail> with MountedSetS
|
||||
void collapse() {
|
||||
if (_isTouchExpanded) {
|
||||
setState(() => _isTouchExpanded = false);
|
||||
_notifyInteractionExpandedIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -539,7 +574,7 @@ class SideNavigationRailState extends State<SideNavigationRail> with MountedSetS
|
||||
final horizontalPadding = horizontalPaddingForContext(context, isCollapsed: isCollapsed);
|
||||
final itemHorizontalPadding = itemHorizontalPaddingForContext(context, isCollapsed: isCollapsed);
|
||||
final hasLiveTv = context.watch<MultiServerProvider>().hasLiveTv;
|
||||
final surfaceOpacity = isCollapsed && PlatformDetector.isTV() ? 0.0 : 1.0;
|
||||
final surfaceOpacity = PlatformDetector.isTV() ? 0.0 : 1.0;
|
||||
|
||||
// Listen to fullscreen + groupLibrariesByServer setting so the rail
|
||||
// rebuilds when the user toggles "Group libraries by server" in Appearance.
|
||||
@@ -584,6 +619,7 @@ class SideNavigationRailState extends State<SideNavigationRail> with MountedSetS
|
||||
onTapOutside: (_) {
|
||||
if (_isTouchExpanded) {
|
||||
setState(() => _isTouchExpanded = false);
|
||||
_notifyInteractionExpandedIfNeeded();
|
||||
}
|
||||
},
|
||||
child: MouseRegion(
|
||||
@@ -592,7 +628,7 @@ class SideNavigationRailState extends State<SideNavigationRail> with MountedSetS
|
||||
onExit: (_) => _onHoverExit(),
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: isCollapsed ? () => setState(() => _isTouchExpanded = true) : null,
|
||||
onTap: isCollapsed ? _expandForTouch : null,
|
||||
child: AnimatedContainer(
|
||||
duration: t.normal,
|
||||
curve: Curves.easeOutCubic,
|
||||
|
||||
@@ -24,14 +24,18 @@ import 'package:plezy/providers/hidden_libraries_provider.dart';
|
||||
import 'package:plezy/providers/libraries_provider.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/screens/discover_screen.dart';
|
||||
import 'package:plezy/screens/main_screen.dart';
|
||||
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/services/storage_service.dart';
|
||||
import 'package:plezy/theme/mono_theme.dart';
|
||||
import 'package:plezy/utils/layout_constants.dart';
|
||||
import 'package:plezy/utils/platform_detector.dart';
|
||||
import 'package:plezy/watch_together/watch_together.dart';
|
||||
import 'package:plezy/widgets/side_navigation_rail.dart';
|
||||
import 'package:plezy/widgets/tv_browse_rail.dart';
|
||||
import 'package:plezy/widgets/tv_spotlight_background.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../test_helpers/prefs.dart';
|
||||
@@ -113,7 +117,13 @@ void main() {
|
||||
],
|
||||
child: MaterialApp(
|
||||
theme: monoTheme(dark: true),
|
||||
home: SizedBox(width: 1280, height: 720, child: DiscoverScreen(key: discoverKey)),
|
||||
home: MainScreenFocusScope(
|
||||
focusSidebar: () {},
|
||||
focusContent: () {},
|
||||
isSidebarFocused: false,
|
||||
sideNavigationWidth: SideNavigationRailState.expandedWidth,
|
||||
child: SizedBox(width: 1280, height: 720, child: DiscoverScreen(key: discoverKey)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -122,6 +132,20 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(TvBrowseRail), findsOneWidget);
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
final backgroundPosition = tester.widget<Positioned>(
|
||||
find.ancestor(of: find.byType(TvSpotlightBackground), matching: find.byType(Positioned)).first,
|
||||
);
|
||||
expect(backgroundPosition.left, -SideNavigationRailState.expandedWidth);
|
||||
|
||||
tester.state<FocusableActionBarState>(find.byType(FocusableActionBar)).requestFocusOnFirst();
|
||||
await tester.pump();
|
||||
expect(FocusManager.instance.primaryFocus?.debugLabel, 'ActionBar[0]');
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:ui' show PointerDeviceKind;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@@ -122,6 +124,121 @@ void main() {
|
||||
expect((selectedItemContainer.decoration as BoxDecoration?)?.color, isNull);
|
||||
});
|
||||
|
||||
testWidgets('expanded TV rail keeps its surface transparent', (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);
|
||||
|
||||
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(
|
||||
selectedTab: NavigationTabId.discover,
|
||||
isSidebarFocused: true,
|
||||
alwaysExpanded: false,
|
||||
onDestinationSelected: (_) {},
|
||||
onLibrarySelected: (_) {},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final rail = find.descendant(of: find.byType(SideNavigationRail), matching: find.byType(AnimatedContainer)).first;
|
||||
expect(tester.getSize(rail).width, SideNavigationRailState.expandedWidth);
|
||||
|
||||
final surfaceOpacity = tester
|
||||
.widgetList<AnimatedOpacity>(
|
||||
find.descendant(of: find.byType(SideNavigationRail), matching: find.byType(AnimatedOpacity)),
|
||||
)
|
||||
.singleWhere((widget) => widget.child is ColoredBox);
|
||||
expect(surfaceOpacity.opacity, 0.0);
|
||||
});
|
||||
|
||||
testWidgets('reports interaction expansion for shell content push', (tester) async {
|
||||
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 reports = <bool>[];
|
||||
|
||||
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(
|
||||
selectedTab: NavigationTabId.discover,
|
||||
isSidebarFocused: false,
|
||||
alwaysExpanded: false,
|
||||
onInteractionExpandedChanged: reports.add,
|
||||
onDestinationSelected: (_) {},
|
||||
onLibrarySelected: (_) {},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final rail = find.descendant(of: find.byType(SideNavigationRail), matching: find.byType(AnimatedContainer)).first;
|
||||
expect(tester.getSize(rail).width, SideNavigationRailState.collapsedWidth);
|
||||
|
||||
final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||
addTearDown(gesture.removePointer);
|
||||
await gesture.addPointer(location: const Offset(799, 599));
|
||||
await tester.pump();
|
||||
await gesture.moveTo(tester.getCenter(rail));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(reports.last, isTrue);
|
||||
expect(tester.getSize(rail).width, SideNavigationRailState.expandedWidth);
|
||||
|
||||
await gesture.moveTo(tester.getBottomRight(rail) + const Offset(100, -10));
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
|
||||
expect(reports.last, isFalse);
|
||||
});
|
||||
|
||||
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