diff --git a/lib/screens/channel/channel.dart b/lib/screens/channel/channel.dart index 335d643f..64f1a64a 100644 --- a/lib/screens/channel/channel.dart +++ b/lib/screens/channel/channel.dart @@ -99,6 +99,10 @@ class _VideoChatState extends State { ? () => settingsStore.fullScreen = !settingsStore.fullScreen : null, onTap: () { + if (_videoStore.miniVedioMode) { + _videoStore.setMiniVedioMode(false); + return; + } if (_chatStore.assetsStore.showEmoteMenu) { _chatStore.assetsStore.showEmoteMenu = false; } else { @@ -204,9 +208,8 @@ class _VideoChatState extends State { final data = await Clipboard.getData(Clipboard.kTextPlain); - if (data != null) { + if (data != null) _chatStore.textController.text = data.text!; - } _chatStore.updateNotification(''); }, @@ -219,102 +222,151 @@ class _VideoChatState extends State { }, ); - final videoChat = Scaffold( - body: Observer( - builder: (context) { - if (orientation == Orientation.landscape && - !settingsStore.landscapeForceVerticalChat) { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); - - final landscapeChat = AnimatedContainer( - curve: Curves.ease, - duration: const Duration(milliseconds: 200), - width: _chatStore.expandChat - ? MediaQuery.of(context).size.width / 2 - : MediaQuery.of(context).size.width * - _chatStore.settings.chatWidth, - color: _chatStore.settings.fullScreen - ? Colors.black.withOpacity( - _chatStore.settings.fullScreenChatOverlayOpacity) - : Theme.of(context).scaffoldBackgroundColor, - child: chat, - ); - - final overlayChat = Visibility( - visible: settingsStore.fullScreenChatOverlay, - maintainState: true, - child: Theme( - data: darkTheme, - child: DefaultTextStyle( - style: DefaultTextStyle.of(context) - .style - .copyWith(color: Colors.white), - child: landscapeChat, + final videoChat = Observer(builder: (_) { + return _videoStore.miniVedioMode + ? Positioned( + right: 10, + bottom: 60, + child: SizedBox( + width: 180, + height: 100, + child: Dismissible( + key: ValueKey('${widget.key}_mini'), + direction: DismissDirection.horizontal, + onDismissed: (direction) { + Navigator.of(context).pop(); + }, + child: Scaffold( + body: Observer(builder: (context) { + SystemChrome.setEnabledSystemUIMode( + SystemUiMode.manual, + overlays: SystemUiOverlay.values, + ); + return AspectRatio(aspectRatio: 16 / 9, child: video); + }), + ), ), ), - ); - - return ColoredBox( - color: settingsStore.showVideo - ? Colors.black - : Theme.of(context).scaffoldBackgroundColor, - child: SafeArea( - bottom: false, - left: (settingsStore.landscapeCutout == - LandscapeCutoutType.both || - settingsStore.landscapeCutout == - LandscapeCutoutType.left) - ? false - : true, - right: (settingsStore.landscapeCutout == - LandscapeCutoutType.both || - settingsStore.landscapeCutout == - LandscapeCutoutType.right) - ? false - : true, - child: settingsStore.showVideo - ? settingsStore.fullScreen - ? Stack( - children: [ - player, - if (settingsStore.showOverlay) - Row( - children: settingsStore.landscapeChatLeftSide - ? [overlayChat, Expanded(child: overlay)] - : [Expanded(child: overlay), overlayChat], - ) - ], - ) - : Row( - children: settingsStore.landscapeChatLeftSide - ? [landscapeChat, Expanded(child: video)] - : [Expanded(child: video), landscapeChat], - ) - : Column( - children: [appBar, Expanded(child: chat)], + ) + : Dismissible( + key: ValueKey(widget.key), + direction: DismissDirection.vertical, + onDismissed: (direction) { + _videoStore.setMiniVedioMode(true); + }, + child: Scaffold( + body: Observer( + builder: (context) { + if (orientation == Orientation.landscape && + !settingsStore.landscapeForceVerticalChat) { + SystemChrome.setEnabledSystemUIMode( + SystemUiMode.immersiveSticky); + + final landscapeChat = AnimatedContainer( + curve: Curves.ease, + duration: const Duration(milliseconds: 200), + width: _chatStore.expandChat + ? MediaQuery.of(context).size.width / 2 + : MediaQuery.of(context).size.width * + _chatStore.settings.chatWidth, + color: _chatStore.settings.fullScreen + ? Colors.black.withOpacity(_chatStore + .settings.fullScreenChatOverlayOpacity) + : Theme.of(context).scaffoldBackgroundColor, + child: chat, + ); + + final overlayChat = Visibility( + visible: settingsStore.fullScreenChatOverlay, + maintainState: true, + child: Theme( + data: darkTheme, + child: DefaultTextStyle( + style: DefaultTextStyle.of(context) + .style + .copyWith(color: Colors.white), + child: landscapeChat, + ), + ), + ); + + return ColoredBox( + color: settingsStore.showVideo + ? Colors.black + : Theme.of(context).scaffoldBackgroundColor, + child: SafeArea( + bottom: false, + left: (settingsStore.landscapeCutout == + LandscapeCutoutType.both || + settingsStore.landscapeCutout == + LandscapeCutoutType.left) + ? false + : true, + right: (settingsStore.landscapeCutout == + LandscapeCutoutType.both || + settingsStore.landscapeCutout == + LandscapeCutoutType.right) + ? false + : true, + child: settingsStore.showVideo + ? settingsStore.fullScreen + ? Stack( + children: [ + player, + if (settingsStore.showOverlay) + Row( + children: settingsStore + .landscapeChatLeftSide + ? [ + overlayChat, + Expanded(child: overlay) + ] + : [ + Expanded(child: overlay), + overlayChat + ], + ) + ], + ) + : Row( + children: + settingsStore.landscapeChatLeftSide + ? [ + landscapeChat, + Expanded(child: video) + ] + : [ + Expanded(child: video), + landscapeChat + ], + ) + : Column( + children: [appBar, Expanded(child: chat)], + ), + ), + ); + } + + SystemChrome.setEnabledSystemUIMode( + SystemUiMode.manual, + overlays: SystemUiOverlay.values, + ); + return SafeArea( + child: Column( + children: [ + if (!settingsStore.showVideo) + appBar + else + AspectRatio(aspectRatio: 16 / 9, child: video), + Expanded(child: chat), + ], ), + ); + }, + ), ), ); - } - - SystemChrome.setEnabledSystemUIMode( - SystemUiMode.manual, - overlays: SystemUiOverlay.values, - ); - return SafeArea( - child: Column( - children: [ - if (!settingsStore.showVideo) - appBar - else - AspectRatio(aspectRatio: 16 / 9, child: video), - Expanded(child: chat), - ], - ), - ); - }, - ), - ); + }); // If on Android, use PiPSwitcher to enable PiP functionality. if (Platform.isAndroid) { diff --git a/lib/screens/channel/video/video_bar.dart b/lib/screens/channel/video/video_bar.dart index 2080573c..4e19bfd0 100644 --- a/lib/screens/channel/video/video_bar.dart +++ b/lib/screens/channel/video/video_bar.dart @@ -78,7 +78,11 @@ class VideoBar extends StatelessWidget { const SizedBox(height: 5.0), InkWell( onTap: tappableCategory && streamInfo.gameName.isNotEmpty - ? () => Navigator.push( + ? () { + // remove until this page is the top level + Navigator.popUntil( + context, (route) => route.isFirst); + Navigator.push( context, MaterialPageRoute( builder: (context) => CategoryStreams( @@ -86,7 +90,8 @@ class VideoBar extends StatelessWidget { categoryId: streamInfo.gameId, ), ), - ) + ); + } : null, child: Tooltip( message: diff --git a/lib/screens/channel/video/video_store.dart b/lib/screens/channel/video/video_store.dart index f5b9d1fd..ce82601e 100644 --- a/lib/screens/channel/video/video_store.dart +++ b/lib/screens/channel/video/video_store.dart @@ -77,6 +77,10 @@ abstract class VideoStoreBase with Store { @readonly var _isIPad = false; + /// For pip mode while in app. + @readonly + var _miniVedioMode = false; + /// The current stream info, used for displaying relevant info on the overlay. @readonly StreamTwitch? _streamInfo; @@ -174,6 +178,19 @@ abstract class VideoStoreBase with Store { } } + /// Allows to switch between full mode and pip mode. + @action + void setMiniVedioMode(bool mode) { + // close overlay if open for mini vedio mode + if (mode) { + _overlayTimer.cancel(); + if (_overlayVisible) { + _overlayVisible = false; + } + } + _miniVedioMode = mode; + } + /// Updates the stream info from the Twitch API. /// /// If the stream is offline, disables the overlay. diff --git a/lib/screens/channel/video/video_store.g.dart b/lib/screens/channel/video/video_store.g.dart index 549f0cfc..3322213f 100644 --- a/lib/screens/channel/video/video_store.g.dart +++ b/lib/screens/channel/video/video_store.g.dart @@ -71,6 +71,24 @@ mixin _$VideoStore on VideoStoreBase, Store { }); } + late final _$_miniVedioModeAtom = + Atom(name: 'VideoStoreBase._miniVedioMode', context: context); + + bool get miniVedioMode { + _$_miniVedioModeAtom.reportRead(); + return super._miniVedioMode; + } + + @override + bool get _miniVedioMode => miniVedioMode; + + @override + set _miniVedioMode(bool value) { + _$_miniVedioModeAtom.reportWrite(value, super._miniVedioMode, () { + super._miniVedioMode = value; + }); + } + late final _$_streamInfoAtom = Atom(name: 'VideoStoreBase._streamInfo', context: context); @@ -119,6 +137,17 @@ mixin _$VideoStore on VideoStoreBase, Store { } } + @override + void setMiniVedioMode(bool mode) { + final _$actionInfo = _$VideoStoreBaseActionController.startAction( + name: 'VideoStoreBase.setMiniVedioMode'); + try { + return super.setMiniVedioMode(mode); + } finally { + _$VideoStoreBaseActionController.endAction(_$actionInfo); + } + } + @override void handleToggleOverlay() { final _$actionInfo = _$VideoStoreBaseActionController.startAction( diff --git a/lib/screens/home/search/search_results_channels.dart b/lib/screens/home/search/search_results_channels.dart index cb780765..90587138 100644 --- a/lib/screens/home/search/search_results_channels.dart +++ b/lib/screens/home/search/search_results_channels.dart @@ -11,6 +11,7 @@ import 'package:frosty/widgets/block_report_modal.dart'; import 'package:frosty/widgets/list_tile.dart'; import 'package:frosty/widgets/loading_indicator.dart'; import 'package:frosty/widgets/profile_picture.dart'; +import 'package:frosty/widgets/translucent_overlay_route.dart'; import 'package:frosty/widgets/uptime.dart'; import 'package:mobx/mobx.dart'; @@ -34,9 +35,12 @@ class _SearchResultsChannelsState extends State { final channelInfo = await widget.searchStore.searchChannel(search); if (!mounted) return; + // remove until this page is the top level + Navigator.popUntil(context, (route) => route.isFirst); + // push new VedioChat Navigator.push( context, - MaterialPageRoute( + TranslucentOverlayRoute( builder: (context) => VideoChat( userId: channelInfo.broadcasterId, userName: channelInfo.broadcasterName, @@ -91,16 +95,21 @@ class _SearchResultsChannelsState extends State { : '${channel.displayName} (${channel.broadcasterLogin})'; return AnimateScale( - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => VideoChat( - userId: channel.id, - userName: channel.displayName, - userLogin: channel.broadcasterLogin, + onTap: () { + // remove until this page is the top level + Navigator.popUntil(context, (route) => route.isFirst); + // push new VedioChat + Navigator.push( + context, + TranslucentOverlayRoute( + builder: (context) => VideoChat( + userId: channel.id, + userName: channel.displayName, + userLogin: channel.broadcasterLogin, + ), ), - ), - ), + ); + }, onLongPress: () { HapticFeedback.lightImpact(); diff --git a/lib/screens/home/stream_list/large_stream_card.dart b/lib/screens/home/stream_list/large_stream_card.dart index e1ed3a53..eb41cea7 100644 --- a/lib/screens/home/stream_list/large_stream_card.dart +++ b/lib/screens/home/stream_list/large_stream_card.dart @@ -12,6 +12,7 @@ import 'package:frosty/widgets/animate_scale.dart'; import 'package:frosty/widgets/block_report_modal.dart'; import 'package:frosty/widgets/cached_image.dart'; import 'package:frosty/widgets/loading_indicator.dart'; +import 'package:frosty/widgets/translucent_overlay_route.dart'; import 'package:frosty/widgets/uptime.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; @@ -136,16 +137,21 @@ class LargeStreamCard extends StatelessWidget { : '${streamInfo.userName} (${streamInfo.userLogin})'; return AnimateScale( - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => VideoChat( - userId: streamInfo.userId, - userName: streamInfo.userName, - userLogin: streamInfo.userLogin, + onTap: () { + // remove until this page is the top level + Navigator.popUntil(context, (route) => route.isFirst); + // push new VedioChat + Navigator.push( + context, + TranslucentOverlayRoute( + builder: (context) => VideoChat( + userId: streamInfo.userId, + userName: streamInfo.userName, + userLogin: streamInfo.userLogin, + ), ), - ), - ), + ); + }, onLongPress: () { HapticFeedback.mediumImpact(); diff --git a/lib/screens/home/stream_list/stream_card.dart b/lib/screens/home/stream_list/stream_card.dart index 949788a8..f477e6f8 100644 --- a/lib/screens/home/stream_list/stream_card.dart +++ b/lib/screens/home/stream_list/stream_card.dart @@ -13,6 +13,7 @@ import 'package:frosty/widgets/block_report_modal.dart'; import 'package:frosty/widgets/cached_image.dart'; import 'package:frosty/widgets/loading_indicator.dart'; import 'package:frosty/widgets/profile_picture.dart'; +import 'package:frosty/widgets/translucent_overlay_route.dart'; import 'package:frosty/widgets/uptime.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; @@ -140,7 +141,10 @@ class StreamCard extends StatelessWidget { if (showCategory) ...[ InkWell( onTap: streamInfo.gameName.isNotEmpty - ? () => Navigator.push( + ? () { + // remove until this page is the top level + Navigator.popUntil(context, (route) => route.isFirst); + Navigator.push( context, MaterialPageRoute( builder: (context) => CategoryStreams( @@ -148,7 +152,8 @@ class StreamCard extends StatelessWidget { categoryId: streamInfo.gameId, ), ), - ) + ); + } : null, child: Tooltip( message: @@ -180,16 +185,21 @@ class StreamCard extends StatelessWidget { ); return AnimateScale( - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => VideoChat( - userId: streamInfo.userId, - userName: streamInfo.userName, - userLogin: streamInfo.userLogin, + onTap: () { + // remove until this page is the top level + Navigator.popUntil(context, (route) => route.isFirst); + // push new VedioChat + Navigator.push( + context, + TranslucentOverlayRoute( + builder: (context) => VideoChat( + userId: streamInfo.userId, + userName: streamInfo.userName, + userLogin: streamInfo.userLogin, + ), ), - ), - ), + ); + }, onLongPress: () { HapticFeedback.mediumImpact(); diff --git a/lib/screens/home/top/categories/category_card.dart b/lib/screens/home/top/categories/category_card.dart index a4fd584d..48da10bc 100644 --- a/lib/screens/home/top/categories/category_card.dart +++ b/lib/screens/home/top/categories/category_card.dart @@ -23,15 +23,19 @@ class CategoryCard extends StatelessWidget { final artHeight = (artWidth * (4 / 3)).toInt(); return InkWell( - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => CategoryStreams( - categoryName: category.name, - categoryId: category.id, + onTap: () { + // remove until this page is the top level + Navigator.popUntil(context, (route) => route.isFirst); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CategoryStreams( + categoryName: category.name, + categoryId: category.id, + ), ), - ), - ), + ); + }, child: Padding( padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 20.0), child: Column( diff --git a/lib/widgets/translucent_overlay_route.dart b/lib/widgets/translucent_overlay_route.dart new file mode 100644 index 00000000..a558013f --- /dev/null +++ b/lib/widgets/translucent_overlay_route.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +class TranslucentOverlayRoute extends TransitionRoute { + final Duration _transitionDuration; + final WidgetBuilder builder; + + TranslucentOverlayRoute({ + required this.builder, + Duration transitionDuration = Duration.zero, + }) : _transitionDuration = transitionDuration; + + @override + Iterable createOverlayEntries() { + return [OverlayEntry(builder: builder, maintainState: true)]; + } + + @override + bool get opaque => false; + + @override + Duration get transitionDuration => _transitionDuration; +}