fix(tv): pass root menu to system
This commit is contained in:
@@ -9,6 +9,7 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
import '../i18n/strings.g.dart';
|
import '../i18n/strings.g.dart';
|
||||||
import '../services/app_exit_service.dart';
|
import '../services/app_exit_service.dart';
|
||||||
|
import '../services/tvos_system_navigation_service.dart';
|
||||||
import '../services/update_service.dart';
|
import '../services/update_service.dart';
|
||||||
import '../utils/app_logger.dart';
|
import '../utils/app_logger.dart';
|
||||||
import '../widgets/auth_error_banner.dart';
|
import '../widgets/auth_error_banner.dart';
|
||||||
@@ -176,6 +177,25 @@ bool shouldRenderMainScreenOffline({
|
|||||||
return providerOffline || (startupOfflineUntilConnected && !hasVisibleConnectedServers);
|
return providerOffline || (startupOfflineUntilConnected && !hasVisibleConnectedServers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
bool shouldPassTvosMenuToSystem({
|
||||||
|
required bool isAppleTV,
|
||||||
|
required bool isShowingProfileSelection,
|
||||||
|
required bool isOverlaySheetOpen,
|
||||||
|
required bool isRouteCurrent,
|
||||||
|
required bool isSidebarFocused,
|
||||||
|
required bool hasVisibleTabs,
|
||||||
|
required bool isCurrentTabRoot,
|
||||||
|
}) {
|
||||||
|
return isAppleTV &&
|
||||||
|
isSidebarFocused &&
|
||||||
|
!isShowingProfileSelection &&
|
||||||
|
!isOverlaySheetOpen &&
|
||||||
|
isRouteCurrent &&
|
||||||
|
hasVisibleTabs &&
|
||||||
|
isCurrentTabRoot;
|
||||||
|
}
|
||||||
|
|
||||||
class MainScreen extends StatefulWidget {
|
class MainScreen extends StatefulWidget {
|
||||||
final bool isOfflineMode;
|
final bool isOfflineMode;
|
||||||
|
|
||||||
@@ -241,6 +261,7 @@ class _MainScreenState extends State<MainScreen>
|
|||||||
final FocusScopeNode _contentFocusScope = FocusScopeNode(debugLabel: 'Content');
|
final FocusScopeNode _contentFocusScope = FocusScopeNode(debugLabel: 'Content');
|
||||||
bool _isSidebarFocused = false;
|
bool _isSidebarFocused = false;
|
||||||
bool _isSidebarInteractionExpanded = false;
|
bool _isSidebarInteractionExpanded = false;
|
||||||
|
bool _isOverlaySheetOpen = false;
|
||||||
|
|
||||||
/// The binder is now owned by a top-level [Provider] (see main.dart) so
|
/// 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
|
/// the splash can await its first settle before navigating here. We just
|
||||||
@@ -352,6 +373,8 @@ class _MainScreenState extends State<MainScreen>
|
|||||||
_contentFocusScope.requestFocus();
|
_contentFocusScope.requestFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_updateTvosMenuPassthrough();
|
||||||
|
|
||||||
// Check for updates on startup
|
// Check for updates on startup
|
||||||
unawaited(_checkForUpdatesOnStartup());
|
unawaited(_checkForUpdatesOnStartup());
|
||||||
});
|
});
|
||||||
@@ -549,10 +572,12 @@ class _MainScreenState extends State<MainScreen>
|
|||||||
if (!hasNoActive && !requireOnOpen) return;
|
if (!hasNoActive && !requireOnOpen) return;
|
||||||
|
|
||||||
_isShowingProfileSelection = true;
|
_isShowingProfileSelection = true;
|
||||||
|
_setTvosMenuPassthrough(false);
|
||||||
await Navigator.of(
|
await Navigator.of(
|
||||||
context,
|
context,
|
||||||
).push(MaterialPageRoute(builder: (context) => const ProfileSwitchScreen(requireSelection: true)));
|
).push(MaterialPageRoute(builder: (context) => const ProfileSwitchScreen(requireSelection: true)));
|
||||||
_isShowingProfileSelection = false;
|
_isShowingProfileSelection = false;
|
||||||
|
_updateTvosMenuPassthrough();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _checkForUpdatesOnStartup() async {
|
Future<void> _checkForUpdatesOnStartup() async {
|
||||||
@@ -785,6 +810,7 @@ class _MainScreenState extends State<MainScreen>
|
|||||||
_startupSettleTimeout = null;
|
_startupSettleTimeout = null;
|
||||||
_sidebarFocusScope.dispose();
|
_sidebarFocusScope.dispose();
|
||||||
_contentFocusScope.dispose();
|
_contentFocusScope.dispose();
|
||||||
|
_setTvosMenuPassthrough(false);
|
||||||
|
|
||||||
// Clean up companion remote callbacks
|
// Clean up companion remote callbacks
|
||||||
if (_companionRemoteSetup) {
|
if (_companionRemoteSetup) {
|
||||||
@@ -829,10 +855,12 @@ class _MainScreenState extends State<MainScreen>
|
|||||||
if (!activeProfile.hasMultipleProfiles) return;
|
if (!activeProfile.hasMultipleProfiles) return;
|
||||||
|
|
||||||
_isShowingProfileSelection = true;
|
_isShowingProfileSelection = true;
|
||||||
|
_setTvosMenuPassthrough(false);
|
||||||
await Navigator.of(
|
await Navigator.of(
|
||||||
context,
|
context,
|
||||||
).push(MaterialPageRoute(builder: (context) => const ProfileSwitchScreen(requireSelection: true)));
|
).push(MaterialPageRoute(builder: (context) => const ProfileSwitchScreen(requireSelection: true)));
|
||||||
_isShowingProfileSelection = false;
|
_isShowingProfileSelection = false;
|
||||||
|
_updateTvosMenuPassthrough();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// IndexedStack that disables tickers for offscreen children to prevent
|
/// IndexedStack that disables tickers for offscreen children to prevent
|
||||||
@@ -932,6 +960,7 @@ class _MainScreenState extends State<MainScreen>
|
|||||||
_screens = _buildScreens(_isOffline);
|
_screens = _buildScreens(_isOffline);
|
||||||
_currentTab = _normalizeTabForMode(_currentTab, _isOffline);
|
_currentTab = _normalizeTabForMode(_currentTab, _isOffline);
|
||||||
});
|
});
|
||||||
|
_updateTvosMenuPassthrough();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleOfflineStatusChanged() {
|
void _handleOfflineStatusChanged() {
|
||||||
@@ -977,6 +1006,7 @@ class _MainScreenState extends State<MainScreen>
|
|||||||
_autoSwitchedToDownloads = false;
|
_autoSwitchedToDownloads = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
_updateTvosMenuPassthrough();
|
||||||
|
|
||||||
// Refresh sidebar focus after rebuilding navigation
|
// Refresh sidebar focus after rebuilding navigation
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
@@ -1005,6 +1035,7 @@ class _MainScreenState extends State<MainScreen>
|
|||||||
// and overwrites lastFocusedKey (e.g. to the Libraries toggle button).
|
// and overwrites lastFocusedKey (e.g. to the Libraries toggle button).
|
||||||
final targetKey = _sideNavKey.currentState?.lastFocusedKey;
|
final targetKey = _sideNavKey.currentState?.lastFocusedKey;
|
||||||
setState(() => _isSidebarFocused = true);
|
setState(() => _isSidebarFocused = true);
|
||||||
|
_updateTvosMenuPassthrough();
|
||||||
_sidebarFocusScope.requestFocus();
|
_sidebarFocusScope.requestFocus();
|
||||||
// Focus the active item after the focus scope has focus
|
// Focus the active item after the focus scope has focus
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
@@ -1014,6 +1045,7 @@ class _MainScreenState extends State<MainScreen>
|
|||||||
|
|
||||||
void _focusContent({bool restorePreviousFocus = true}) {
|
void _focusContent({bool restorePreviousFocus = true}) {
|
||||||
setState(() => _isSidebarFocused = false);
|
setState(() => _isSidebarFocused = false);
|
||||||
|
_updateTvosMenuPassthrough();
|
||||||
if (restorePreviousFocus) {
|
if (restorePreviousFocus) {
|
||||||
_contentFocusScope.requestFocus();
|
_contentFocusScope.requestFocus();
|
||||||
}
|
}
|
||||||
@@ -1040,6 +1072,12 @@ class _MainScreenState extends State<MainScreen>
|
|||||||
setState(() => _isSidebarInteractionExpanded = expanded);
|
setState(() => _isSidebarInteractionExpanded = expanded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleOverlaySheetOpenChanged(bool open) {
|
||||||
|
if (_isOverlaySheetOpen == open) return;
|
||||||
|
_isOverlaySheetOpen = open;
|
||||||
|
_updateTvosMenuPassthrough();
|
||||||
|
}
|
||||||
|
|
||||||
double _sideNavigationWidth(BuildContext context, {required bool alwaysExpanded}) {
|
double _sideNavigationWidth(BuildContext context, {required bool alwaysExpanded}) {
|
||||||
final isExpanded = alwaysExpanded || _isSidebarFocused || _isSidebarInteractionExpanded;
|
final isExpanded = alwaysExpanded || _isSidebarFocused || _isSidebarInteractionExpanded;
|
||||||
return isExpanded
|
return isExpanded
|
||||||
@@ -1047,12 +1085,34 @@ class _MainScreenState extends State<MainScreen>
|
|||||||
: SideNavigationRailState.collapsedWidthForContext(context);
|
: SideNavigationRailState.collapsedWidthForContext(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get _shouldPassTvosMenuToSystem {
|
||||||
|
final tabs = _getVisibleTabs(_isOffline);
|
||||||
|
return shouldPassTvosMenuToSystem(
|
||||||
|
isAppleTV: PlatformDetector.isAppleTV(),
|
||||||
|
isShowingProfileSelection: _isShowingProfileSelection,
|
||||||
|
isOverlaySheetOpen: _isOverlaySheetOpen,
|
||||||
|
isRouteCurrent: ModalRoute.of(context)?.isCurrent == true,
|
||||||
|
isSidebarFocused: _isSidebarFocused,
|
||||||
|
hasVisibleTabs: tabs.isNotEmpty,
|
||||||
|
isCurrentTabRoot: tabs.isNotEmpty && _currentTab == tabs.first.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setTvosMenuPassthrough(bool enabled) {
|
||||||
|
if (!PlatformDetector.isAppleTV()) return;
|
||||||
|
unawaited(TvosSystemNavigationService.setMenuPassthroughEnabled(enabled));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateTvosMenuPassthrough() {
|
||||||
|
_setTvosMenuPassthrough(_shouldPassTvosMenuToSystem);
|
||||||
|
}
|
||||||
|
|
||||||
/// Suppress stray back events after a child route pops.
|
/// Suppress stray back events after a child route pops.
|
||||||
/// On Android TV the platform popRoute can arrive before the key events,
|
/// On Android TV the platform popRoute can arrive before the key events,
|
||||||
/// so BackKeySuppressorObserver misses them and they leak into _handleBackKey.
|
/// so BackKeySuppressorObserver misses them and they leak into _handleBackKey.
|
||||||
bool _suppressBackAfterPop = false;
|
bool _suppressBackAfterPop = false;
|
||||||
|
|
||||||
KeyEventResult _handleMainBack({bool allowTvSystemExit = false}) {
|
KeyEventResult _handleMainBack() {
|
||||||
final tabs = _getVisibleTabs(_isOffline);
|
final tabs = _getVisibleTabs(_isOffline);
|
||||||
if (tabs.isEmpty) return KeyEventResult.handled;
|
if (tabs.isEmpty) return KeyEventResult.handled;
|
||||||
|
|
||||||
@@ -1063,11 +1123,18 @@ class _MainScreenState extends State<MainScreen>
|
|||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The tvOS engine normally passes root Menu presses through to UIKit. If a
|
||||||
|
// stale event still reaches Flutter, avoid showing an exit prompt that
|
||||||
|
// cannot be honored app-side.
|
||||||
|
if (PlatformDetector.isAppleTV()) {
|
||||||
|
_lastBackPressAt = null;
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final lastBackPressAt = _lastBackPressAt;
|
final lastBackPressAt = _lastBackPressAt;
|
||||||
if (lastBackPressAt != null && now.difference(lastBackPressAt) < _backExitWindow) {
|
if (lastBackPressAt != null && now.difference(lastBackPressAt) < _backExitWindow) {
|
||||||
_lastBackPressAt = null;
|
_lastBackPressAt = null;
|
||||||
if (allowTvSystemExit && PlatformDetector.isAppleTV()) return KeyEventResult.skipRemainingHandlers;
|
|
||||||
unawaited(AppExitService.requestExit());
|
unawaited(AppExitService.requestExit());
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
}
|
}
|
||||||
@@ -1088,7 +1155,7 @@ class _MainScreenState extends State<MainScreen>
|
|||||||
// matching comment in handleBackKeyAction for why the suppressor pattern
|
// matching comment in handleBackKeyAction for why the suppressor pattern
|
||||||
// doesn't fit here.
|
// doesn't fit here.
|
||||||
if (PlatformDetector.isAppleTV() && event is KeyDownEvent) {
|
if (PlatformDetector.isAppleTV() && event is KeyDownEvent) {
|
||||||
final result = _handleMainBack(allowTvSystemExit: true);
|
final result = _handleMainBack();
|
||||||
if (result == KeyEventResult.handled) {
|
if (result == KeyEventResult.handled) {
|
||||||
BackKeyCoordinator.markHandled();
|
BackKeyCoordinator.markHandled();
|
||||||
}
|
}
|
||||||
@@ -1099,7 +1166,7 @@ class _MainScreenState extends State<MainScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event is KeyUpEvent) {
|
if (event is KeyUpEvent) {
|
||||||
final result = _handleMainBack(allowTvSystemExit: PlatformDetector.isAppleTV());
|
final result = _handleMainBack();
|
||||||
if (result == KeyEventResult.handled) {
|
if (result == KeyEventResult.handled) {
|
||||||
BackKeyCoordinator.markHandled();
|
BackKeyCoordinator.markHandled();
|
||||||
}
|
}
|
||||||
@@ -1176,6 +1243,7 @@ class _MainScreenState extends State<MainScreen>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void didPushNext() {
|
void didPushNext() {
|
||||||
|
_setTvosMenuPassthrough(false);
|
||||||
// Called when a child route is pushed on top (e.g., video player)
|
// Called when a child route is pushed on top (e.g., video player)
|
||||||
if (_currentTab == NavigationTabId.discover) {
|
if (_currentTab == NavigationTabId.discover) {
|
||||||
if (_discoverKey.currentState case final TabVisibilityAware aware) {
|
if (_discoverKey.currentState case final TabVisibilityAware aware) {
|
||||||
@@ -1196,6 +1264,7 @@ class _MainScreenState extends State<MainScreen>
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Called when returning to this route from a child route (e.g., from video player)
|
// Called when returning to this route from a child route (e.g., from video player)
|
||||||
|
_updateTvosMenuPassthrough();
|
||||||
if (_currentTab == NavigationTabId.discover) {
|
if (_currentTab == NavigationTabId.discover) {
|
||||||
if (_discoverKey.currentState case final TabVisibilityAware aware) {
|
if (_discoverKey.currentState case final TabVisibilityAware aware) {
|
||||||
aware.onTabShown();
|
aware.onTabShown();
|
||||||
@@ -1282,6 +1351,7 @@ class _MainScreenState extends State<MainScreen>
|
|||||||
_autoSwitchedToDownloads = false;
|
_autoSwitchedToDownloads = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
_updateTvosMenuPassthrough();
|
||||||
|
|
||||||
if (previousTab != tab) {
|
if (previousTab != tab) {
|
||||||
// Notify previous screen it's being hidden
|
// Notify previous screen it's being hidden
|
||||||
@@ -1371,6 +1441,7 @@ class _MainScreenState extends State<MainScreen>
|
|||||||
: SideNavigationRailState.collapsedWidthForContext(context);
|
: SideNavigationRailState.collapsedWidthForContext(context);
|
||||||
|
|
||||||
return OverlaySheetHost(
|
return OverlaySheetHost(
|
||||||
|
onOpenChanged: _handleOverlaySheetOpenChanged,
|
||||||
child: PopScope(
|
child: PopScope(
|
||||||
canPop: false, // Prevent system back from popping on Android TV
|
canPop: false, // Prevent system back from popping on Android TV
|
||||||
// ignore: no-empty-block - required callback, back navigation handled by _handleBackKey
|
// ignore: no-empty-block - required callback, back navigation handled by _handleBackKey
|
||||||
@@ -1474,6 +1545,7 @@ class _MainScreenState extends State<MainScreen>
|
|||||||
_handleMainBack();
|
_handleMainBack();
|
||||||
},
|
},
|
||||||
child: OverlaySheetHost(
|
child: OverlaySheetHost(
|
||||||
|
onOpenChanged: _handleOverlaySheetOpenChanged,
|
||||||
child: ScaffoldMessenger(
|
child: ScaffoldMessenger(
|
||||||
key: mainScaffoldMessengerKey,
|
key: mainScaffoldMessengerKey,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import '../utils/platform_detector.dart';
|
||||||
|
|
||||||
|
class TvosSystemNavigationService {
|
||||||
|
static const BasicMessageChannel<Object?> _channel = BasicMessageChannel<Object?>(
|
||||||
|
'flutter/tvos_system_navigation',
|
||||||
|
JSONMessageCodec(),
|
||||||
|
);
|
||||||
|
|
||||||
|
static bool? _menuPassthroughEnabled;
|
||||||
|
|
||||||
|
static Future<void> setMenuPassthroughEnabled(bool enabled) async {
|
||||||
|
if (!PlatformDetector.isAppleTV()) return;
|
||||||
|
if (_menuPassthroughEnabled == enabled) return;
|
||||||
|
|
||||||
|
_menuPassthroughEnabled = enabled;
|
||||||
|
await _channel.send({'menuPassthroughEnabled': enabled});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -191,8 +191,9 @@ class OverlaySheetController {
|
|||||||
/// and skip their own back handling when a sheet is open.
|
/// and skip their own back handling when a sheet is open.
|
||||||
class OverlaySheetHost extends StatefulWidget {
|
class OverlaySheetHost extends StatefulWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
final ValueChanged<bool>? onOpenChanged;
|
||||||
|
|
||||||
const OverlaySheetHost({super.key, required this.child});
|
const OverlaySheetHost({super.key, required this.child, this.onOpenChanged});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<OverlaySheetHost> createState() => _OverlaySheetHostState();
|
State<OverlaySheetHost> createState() => _OverlaySheetHostState();
|
||||||
@@ -264,6 +265,7 @@ class _OverlaySheetHostState extends State<OverlaySheetHost> with SingleTickerPr
|
|||||||
bool showDragHandle = false,
|
bool showDragHandle = false,
|
||||||
}) {
|
}) {
|
||||||
// If already open, close first (instant)
|
// If already open, close first (instant)
|
||||||
|
final wasOpen = _isOpen;
|
||||||
if (_isOpen) {
|
if (_isOpen) {
|
||||||
for (final entry in _pageStack) {
|
for (final entry in _pageStack) {
|
||||||
if (!entry.completer.isCompleted) {
|
if (!entry.completer.isCompleted) {
|
||||||
@@ -291,6 +293,7 @@ class _OverlaySheetHostState extends State<OverlaySheetHost> with SingleTickerPr
|
|||||||
_dragOffset = 0;
|
_dragOffset = 0;
|
||||||
_isDragging = false;
|
_isDragging = false;
|
||||||
});
|
});
|
||||||
|
if (!wasOpen) widget.onOpenChanged?.call(true);
|
||||||
|
|
||||||
BackKeyUpSuppressor.clearSuppression();
|
BackKeyUpSuppressor.clearSuppression();
|
||||||
_animationController.forward(from: 0);
|
_animationController.forward(from: 0);
|
||||||
@@ -351,6 +354,7 @@ class _OverlaySheetHostState extends State<OverlaySheetHost> with SingleTickerPr
|
|||||||
_isDragging = false;
|
_isDragging = false;
|
||||||
_sheetHorizontalAnchor = null;
|
_sheetHorizontalAnchor = null;
|
||||||
});
|
});
|
||||||
|
widget.onOpenChanged?.call(false);
|
||||||
// Clear stale back-key flags. handleBackKeyAction sets
|
// Clear stale back-key flags. handleBackKeyAction sets
|
||||||
// markClosedViaBackKey() expecting a route pop, but the overlay
|
// markClosedViaBackKey() expecting a route pop, but the overlay
|
||||||
// doesn't pop a route. Without clearing, the flag leaks into the
|
// doesn't pop a route. Without clearing, the flag leaks into the
|
||||||
|
|||||||
@@ -41,6 +41,35 @@ void main() {
|
|||||||
expect(expanded.left + expanded.width, viewportWidth);
|
expect(expanded.left + expanded.width, viewportWidth);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('tvOS Menu pass-through only enables at root with sidebar focus', () {
|
||||||
|
bool shouldPass({
|
||||||
|
bool isAppleTV = true,
|
||||||
|
bool isShowingProfileSelection = false,
|
||||||
|
bool isOverlaySheetOpen = false,
|
||||||
|
bool isRouteCurrent = true,
|
||||||
|
bool isSidebarFocused = true,
|
||||||
|
bool hasVisibleTabs = true,
|
||||||
|
bool isCurrentTabRoot = true,
|
||||||
|
}) {
|
||||||
|
return shouldPassTvosMenuToSystem(
|
||||||
|
isAppleTV: isAppleTV,
|
||||||
|
isShowingProfileSelection: isShowingProfileSelection,
|
||||||
|
isOverlaySheetOpen: isOverlaySheetOpen,
|
||||||
|
isRouteCurrent: isRouteCurrent,
|
||||||
|
isSidebarFocused: isSidebarFocused,
|
||||||
|
hasVisibleTabs: hasVisibleTabs,
|
||||||
|
isCurrentTabRoot: isCurrentTabRoot,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(shouldPass(), isTrue);
|
||||||
|
expect(shouldPass(isSidebarFocused: false), isFalse);
|
||||||
|
expect(shouldPass(isCurrentTabRoot: false), isFalse);
|
||||||
|
expect(shouldPass(isOverlaySheetOpen: true), isFalse);
|
||||||
|
expect(shouldPass(isRouteCurrent: false), isFalse);
|
||||||
|
expect(shouldPass(isAppleTV: false), isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('side navigation bleed animates from the previous value', (tester) async {
|
testWidgets('side navigation bleed animates from the previous value', (tester) async {
|
||||||
Widget build(double targetBleed) {
|
Widget build(double targetBleed) {
|
||||||
return Directionality(
|
return Directionality(
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
3.44.0
|
3.44.0+1
|
||||||
|
|||||||
Reference in New Issue
Block a user