diff --git a/lib/screens/channel/channel.dart b/lib/screens/channel/channel.dart index 5fd78f96..ea4dcf67 100644 --- a/lib/screens/channel/channel.dart +++ b/lib/screens/channel/channel.dart @@ -58,6 +58,8 @@ class _VideoChatState extends State late AnimationController _animationController; late Animation _springBackAnimation; + bool? _isImmersiveSystemUiEnabled; + late final ChatTabsStore _chatTabsStore = ChatTabsStore( twitchApi: context.twitchApi, bttvApi: context.bttvApi, @@ -197,14 +199,38 @@ class _VideoChatState extends State }); } + void _scheduleSystemUiModeSync({required bool immersive}) { + if (_isImmersiveSystemUiEnabled == immersive) return; + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted || _isImmersiveSystemUiEnabled == immersive) return; + + _isImmersiveSystemUiEnabled = immersive; + + if (immersive) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); + } else { + SystemChrome.setEnabledSystemUIMode( + SystemUiMode.manual, + overlays: SystemUiOverlay.values, + ); + } + }); + } + /// Wraps a video widget with PiP swipe-down gesture handling. /// /// Provides visual feedback (translate + scale), haptic feedback, /// and an instructional overlay during the drag gesture. Widget _buildPipGestureWrapper({required Widget child, double? aspectRatio}) { + final wrappedChild = aspectRatio != null + ? AspectRatio(aspectRatio: aspectRatio, child: child) + : child; + return AnimatedBuilder( animation: _animationController, - builder: (context, _) { + child: wrappedChild, + builder: (context, animatedChild) { final currentDragDistance = _isPipDragging ? _pipDragDistance : _springBackAnimation.value; @@ -212,11 +238,6 @@ class _VideoChatState extends State final scaleFactor = 1.0 - (currentDragDistance / _pipMaxDragDistance * 0.1); - Widget content = child; - if (aspectRatio != null) { - content = AspectRatio(aspectRatio: aspectRatio, child: child); - } - return Transform.translate( offset: Offset(0, currentDragDistance), child: Transform.scale( @@ -228,7 +249,7 @@ class _VideoChatState extends State onPanUpdate: _handlePipDragUpdate, onPanEnd: _handlePipDragEnd, onPanCancel: _handlePipDragCancel, - child: content, + child: animatedChild, ), if (!_videoStore.isInPipMode) Positioned.fill( @@ -272,6 +293,11 @@ class _VideoChatState extends State Widget build(BuildContext context) { final settingsStore = _chatTabsStore.settingsStore; + _scheduleSystemUiModeSync( + immersive: + context.isLandscape && !settingsStore.landscapeForceVerticalChat, + ); + final player = GestureDetector( onLongPress: _videoStore.handleToggleOverlay, child: Video(key: _videoKey, videoStore: _videoStore), @@ -415,10 +441,6 @@ class _VideoChatState extends State builder: (context) { if (context.isLandscape && !settingsStore.landscapeForceVerticalChat) { - SystemChrome.setEnabledSystemUIMode( - SystemUiMode.immersiveSticky, - ); - final landscapeChat = AnimatedContainer( curve: Curves.ease, duration: _isDividerDragging @@ -539,124 +561,114 @@ class _VideoChatState extends State ) : SafeArea( bottom: false, - left: settingsStore.landscapeCutout != + left: + settingsStore.landscapeCutout != LandscapeCutoutType.left && settingsStore.landscapeCutout != LandscapeCutoutType.both, - right: settingsStore.landscapeCutout != + right: + settingsStore.landscapeCutout != LandscapeCutoutType.right && settingsStore.landscapeCutout != LandscapeCutoutType.both, child: LayoutBuilder( - builder: (context, constraints) { - final availableWidth = - constraints.maxWidth; - final chatWidth = _chatStore.expandChat - ? 0.5 - : settingsStore.chatWidth; + builder: (context, constraints) { + final availableWidth = constraints.maxWidth; + final chatWidth = _chatStore.expandChat + ? 0.5 + : settingsStore.chatWidth; - // Create the landscape chat container with proper styling - final chatContainer = AnimatedContainer( - curve: Curves.ease, - duration: _isDividerDragging - ? Duration.zero - : const Duration( - milliseconds: 200, - ), - width: availableWidth * chatWidth, - color: settingsStore.fullScreen - ? Colors.black.withValues( - alpha: settingsStore - .fullScreenChatOverlayOpacity, - ) - : context.scaffoldColor, - child: chat, - ); + // Create the landscape chat container with proper styling + final chatContainer = AnimatedContainer( + curve: Curves.ease, + duration: _isDividerDragging + ? Duration.zero + : const Duration(milliseconds: 200), + width: availableWidth * chatWidth, + color: settingsStore.fullScreen + ? Colors.black.withValues( + alpha: settingsStore + .fullScreenChatOverlayOpacity, + ) + : context.scaffoldColor, + child: chat, + ); - final draggableDivider = Observer( - builder: (_) => DraggableDivider( - currentWidth: chatWidth, - maxWidth: 0.6, - isResizableOnLeft: settingsStore - .landscapeChatLeftSide, - showHandle: - _videoStore.overlayVisible, - onDragStart: () { - setState(() { - _isDividerDragging = true; - }); - }, - onDrag: (newWidth) { - if (!_chatStore.expandChat) { - settingsStore.chatWidth = - newWidth; - } - }, - onDragEnd: () { - setState(() { - _isDividerDragging = false; - }); - }, - ), - ); + final draggableDivider = Observer( + builder: (_) => DraggableDivider( + currentWidth: chatWidth, + maxWidth: 0.6, + isResizableOnLeft: + settingsStore.landscapeChatLeftSide, + showHandle: _videoStore.overlayVisible, + onDragStart: () { + setState(() { + _isDividerDragging = true; + }); + }, + onDrag: (newWidth) { + if (!_chatStore.expandChat) { + settingsStore.chatWidth = newWidth; + } + }, + onDragEnd: () { + setState(() { + _isDividerDragging = false; + }); + }, + ), + ); - return Stack( - children: [ - Row( - children: - settingsStore - .landscapeChatLeftSide - ? [ - chatContainer, - Expanded( - child: - _buildPipGestureWrapper( - child: video, - ), - ), - ] - : [ - Expanded( - child: - _buildPipGestureWrapper( - child: video, - ), - ), - chatContainer, - ], - ), - Positioned( - top: 0, - bottom: 0, - left: - settingsStore - .landscapeChatLeftSide - ? (availableWidth * - chatWidth) - - 12 - : null, - right: - !settingsStore - .landscapeChatLeftSide - ? (availableWidth * - chatWidth) - - 12 - : null, - child: draggableDivider, - ), - ], - ); - }, - ), - ) + return Stack( + children: [ + Row( + children: + settingsStore + .landscapeChatLeftSide + ? [ + chatContainer, + Expanded( + child: + _buildPipGestureWrapper( + child: video, + ), + ), + ] + : [ + Expanded( + child: + _buildPipGestureWrapper( + child: video, + ), + ), + chatContainer, + ], + ), + Positioned( + top: 0, + bottom: 0, + left: + settingsStore + .landscapeChatLeftSide + ? (availableWidth * chatWidth) - + 12 + : null, + right: + !settingsStore + .landscapeChatLeftSide + ? (availableWidth * chatWidth) - + 12 + : null, + child: draggableDivider, + ), + ], + ); + }, + ), + ) : SafeArea(child: chat), ); } - - SystemChrome.setEnabledSystemUIMode( - SystemUiMode.manual, - overlays: SystemUiOverlay.values, - ); return SafeArea( // Keep chat-only scrolling under the blurred app bar, but // ensure video does not bleed into the system status bar. diff --git a/lib/screens/channel/video/video_overlay.dart b/lib/screens/channel/video/video_overlay.dart index ba082199..886d60b8 100644 --- a/lib/screens/channel/video/video_overlay.dart +++ b/lib/screens/channel/video/video_overlay.dart @@ -172,16 +172,16 @@ class VideoOverlay extends StatelessWidget { if (context.isPortrait) { // Detect physical device tilt to rotate to optimal orientation final physicalOrientation = - await NativeDeviceOrientationCommunicator() - .orientation(useSensor: true); + await NativeDeviceOrientationCommunicator().orientation( + useSensor: true, + ); // Map native orientation to Flutter's DeviceOrientation // iOS: native landscapeLeft = notch left, needs swap to Flutter's landscapeRight // Android: direct mapping works correctly final needsSwap = Platform.isIOS; - if (physicalOrientation == - NativeDeviceOrientation.landscapeLeft) { + if (physicalOrientation == NativeDeviceOrientation.landscapeLeft) { SystemChrome.setPreferredOrientations([ needsSwap ? DeviceOrientation.landscapeRight diff --git a/lib/screens/home/home.dart b/lib/screens/home/home.dart index e89c233a..f3ecdcaf 100644 --- a/lib/screens/home/home.dart +++ b/lib/screens/home/home.dart @@ -52,13 +52,12 @@ class _HomeState extends State { @override void initState() { super.initState(); + SystemChrome.setPreferredOrientations([]); checkAndShowReleaseNotes(); } @override Widget build(BuildContext context) { - SystemChrome.setPreferredOrientations([]); - final theme = Theme.of(context); return GestureDetector(