diff --git a/lib/screens/channel/chat/chat.dart b/lib/screens/channel/chat/chat.dart index be5aabcd..b5848f27 100644 --- a/lib/screens/channel/chat/chat.dart +++ b/lib/screens/channel/chat/chat.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:frosty/screens/channel/chat/emote_menu/emote_menu.dart'; import 'package:frosty/screens/channel/chat/widgets/chat_bottom_bar.dart'; @@ -13,6 +14,10 @@ class Chat extends StatelessWidget { @override Widget build(BuildContext context) { + // Add a post-frame callback to scroll to bottom when rotating or entering full-screen. + SchedulerBinding.instance?.addPostFrameCallback((_) { + if (chatStore.scrollController.hasClients) chatStore.resumeScroll(); + }); return Observer( builder: (context) => Column( children: [ diff --git a/lib/screens/channel/stores/video_store.dart b/lib/screens/channel/stores/video_store.dart index ca657e86..f0479f12 100644 --- a/lib/screens/channel/stores/video_store.dart +++ b/lib/screens/channel/stores/video_store.dart @@ -21,14 +21,34 @@ abstract class _VideoStoreBase with Store { late Timer updateTimer; @readonly - var _overlayVisible = true; + var _paused = true; - @observable - var paused = true; + @readonly + var _overlayVisible = true; @readonly StreamTwitch? _streamInfo; + @computed + String get videoUrl => settingsStore.showOverlay + ? 'https://player.twitch.tv/?channel=$userLogin&controls=false&muted=false&parent=frosty' + : 'https://player.twitch.tv/?channel=$userLogin&muted=false&parent=frosty'; + + Set get javascriptChannels => { + JavascriptChannel( + name: 'Pause', + onMessageReceived: (message) { + _paused = true; + }, + ), + JavascriptChannel( + name: 'Play', + onMessageReceived: (message) { + _paused = false; + }, + ), + }; + final String userLogin; final AuthStore authStore; final SettingsStore settingsStore; @@ -46,13 +66,13 @@ abstract class _VideoStoreBase with Store { @action void handlePausePlay() { try { - if (paused) { + if (_paused) { controller?.runJavascript('document.getElementsByTagName("video")[0].play();'); } else { controller?.runJavascript('document.getElementsByTagName("video")[0].pause();'); } - paused = !paused; + _paused = !_paused; } catch (e) { debugPrint(e.toString()); } @@ -91,11 +111,26 @@ abstract class _VideoStoreBase with Store { overlayTimer = Timer(const Duration(seconds: 5), () => _overlayVisible = false); } + @action + void handleToggleOverlay() { + if (settingsStore.toggleableOverlay) { + settingsStore.showOverlay = !settingsStore.showOverlay; + controller?.loadUrl(videoUrl); + } + } + void handleRefresh() async { HapticFeedback.lightImpact(); controller?.reload(); } + FutureOr handleNavigation(NavigationRequest navigation) { + if (navigation.url.startsWith('https://player.twitch.tv')) { + return NavigationDecision.navigate; + } + return NavigationDecision.prevent; + } + void initVideo() { try { controller?.runJavascript('document.getElementsByTagName("video")[0].addEventListener("pause", () => Pause.postMessage("video paused"));'); diff --git a/lib/screens/channel/stores/video_store.g.dart b/lib/screens/channel/stores/video_store.g.dart index d7004c3d..c9858517 100644 --- a/lib/screens/channel/stores/video_store.g.dart +++ b/lib/screens/channel/stores/video_store.g.dart @@ -9,6 +9,31 @@ part of 'video_store.dart'; // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic mixin _$VideoStore on _VideoStoreBase, Store { + Computed? _$videoUrlComputed; + + @override + String get videoUrl => + (_$videoUrlComputed ??= Computed(() => super.videoUrl, + name: '_VideoStoreBase.videoUrl')) + .value; + + final _$_pausedAtom = Atom(name: '_VideoStoreBase._paused'); + + bool get paused { + _$_pausedAtom.reportRead(); + return super._paused; + } + + @override + bool get _paused => paused; + + @override + set _paused(bool value) { + _$_pausedAtom.reportWrite(value, super._paused, () { + super._paused = value; + }); + } + final _$_overlayVisibleAtom = Atom(name: '_VideoStoreBase._overlayVisible'); bool get overlayVisible { @@ -26,21 +51,6 @@ mixin _$VideoStore on _VideoStoreBase, Store { }); } - final _$pausedAtom = Atom(name: '_VideoStoreBase.paused'); - - @override - bool get paused { - _$pausedAtom.reportRead(); - return super.paused; - } - - @override - set paused(bool value) { - _$pausedAtom.reportWrite(value, super.paused, () { - super.paused = value; - }); - } - final _$_streamInfoAtom = Atom(name: '_VideoStoreBase._streamInfo'); StreamTwitch? get streamInfo { @@ -102,10 +112,21 @@ mixin _$VideoStore on _VideoStoreBase, Store { } } + @override + void handleToggleOverlay() { + final _$actionInfo = _$_VideoStoreBaseActionController.startAction( + name: '_VideoStoreBase.handleToggleOverlay'); + try { + return super.handleToggleOverlay(); + } finally { + _$_VideoStoreBaseActionController.endAction(_$actionInfo); + } + } + @override String toString() { return ''' -paused: ${paused} +videoUrl: ${videoUrl} '''; } } diff --git a/lib/screens/channel/video/video.dart b/lib/screens/channel/video/video.dart index 13dfc5c3..c52cdba5 100644 --- a/lib/screens/channel/video/video.dart +++ b/lib/screens/channel/video/video.dart @@ -1,103 +1,27 @@ import 'package:flutter/material.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:frosty/screens/channel/stores/video_store.dart'; -import 'package:frosty/screens/channel/video/video_overlay.dart'; import 'package:webview_flutter/webview_flutter.dart'; class Video extends StatelessWidget { - final String userLogin; - final FocusNode textFieldFocus; final VideoStore videoStore; const Video({ Key? key, - required this.userLogin, - required this.textFieldFocus, required this.videoStore, }) : super(key: key); @override Widget build(BuildContext context) { - return Observer( - builder: (context) => GestureDetector( - onLongPress: - videoStore.settingsStore.toggleableOverlay ? () => videoStore.settingsStore.showOverlay = !videoStore.settingsStore.showOverlay : null, - child: AspectRatio( - aspectRatio: 16 / 9, - child: Observer( - builder: (context) { - if (videoStore.settingsStore.showOverlay) { - return Stack( - children: [ - WebView( - backgroundColor: Colors.black, - initialUrl: 'https://player.twitch.tv/?channel=$userLogin&controls=false&muted=false&parent=frosty', - javascriptMode: JavascriptMode.unrestricted, - allowsInlineMediaPlayback: true, - initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, - onWebViewCreated: (controller) => videoStore.controller = controller, - onPageFinished: (string) => videoStore.initVideo(), - navigationDelegate: (navigation) { - if (navigation.url.startsWith('https://player.twitch.tv')) { - return NavigationDecision.navigate; - } - return NavigationDecision.prevent; - }, - javascriptChannels: { - JavascriptChannel( - name: 'Pause', - onMessageReceived: (message) { - videoStore.paused = true; - }, - ), - JavascriptChannel( - name: 'Play', - onMessageReceived: (message) { - videoStore.paused = false; - }, - ), - }, - ), - Observer( - builder: (_) { - if (videoStore.paused) return VideoOverlay(videoStore: videoStore); - return SizedBox.expand( - child: Observer( - builder: (_) => AnimatedOpacity( - opacity: videoStore.overlayVisible ? 1.0 : 0.0, - duration: const Duration(milliseconds: 200), - child: ColoredBox( - color: const Color.fromRGBO(0, 0, 0, 0.5), - child: IgnorePointer( - ignoring: !videoStore.overlayVisible, - child: VideoOverlay(videoStore: videoStore), - ), - ), - ), - ), - ); - }, - ) - ], - ); - } - return WebView( - backgroundColor: Colors.black, - initialUrl: 'https://player.twitch.tv/?channel=$userLogin&muted=false&parent=frosty', - javascriptMode: JavascriptMode.unrestricted, - allowsInlineMediaPlayback: true, - initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, - navigationDelegate: (navigation) { - if (navigation.url.startsWith('https://player.twitch.tv')) { - return NavigationDecision.navigate; - } - return NavigationDecision.prevent; - }, - ); - }, - ), - ), - ), + return WebView( + backgroundColor: Colors.black, + initialUrl: videoStore.videoUrl, + javascriptMode: JavascriptMode.unrestricted, + allowsInlineMediaPlayback: true, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + onWebViewCreated: (controller) => videoStore.controller = controller, + onPageFinished: (string) => videoStore.initVideo(), + navigationDelegate: videoStore.handleNavigation, + javascriptChannels: videoStore.javascriptChannels, ); } } diff --git a/lib/screens/channel/video_chat.dart b/lib/screens/channel/video_chat.dart index 89370a18..afe5dfbc 100644 --- a/lib/screens/channel/video_chat.dart +++ b/lib/screens/channel/video_chat.dart @@ -7,19 +7,25 @@ import 'package:frosty/screens/channel/chat/chat.dart'; import 'package:frosty/screens/channel/stores/chat_store.dart'; import 'package:frosty/screens/channel/stores/video_store.dart'; import 'package:frosty/screens/channel/video/video.dart'; +import 'package:frosty/screens/channel/video/video_overlay.dart'; import 'package:frosty/screens/settings/settings.dart'; import 'package:provider/provider.dart'; class VideoChat extends StatefulWidget { final ChatStore chatStore; - const VideoChat({Key? key, required this.chatStore}) : super(key: key); + const VideoChat({ + Key? key, + required this.chatStore, + }) : super(key: key); @override _VideoChatState createState() => _VideoChatState(); } class _VideoChatState extends State { + final _videoKey = GlobalKey(); + @override Widget build(BuildContext context) { final chatStore = widget.chatStore; @@ -32,7 +38,13 @@ class _VideoChatState extends State { settingsStore: chatStore.settings, ); + final player = Video( + key: _videoKey, + videoStore: videoStore, + ); + final video = GestureDetector( + onLongPress: videoStore.handleToggleOverlay, onTap: () { if (chatStore.assetsStore.showEmoteMenu) { chatStore.assetsStore.showEmoteMenu = false; @@ -44,18 +56,39 @@ class _VideoChatState extends State { } } }, - child: Video( - key: GlobalKey(), - userLogin: chatStore.channelName, - textFieldFocus: chatStore.textFieldFocusNode, - videoStore: videoStore, + child: Observer( + builder: (context) { + if (videoStore.settingsStore.showOverlay) { + return Stack( + children: [ + player, + Observer( + builder: (_) { + if (videoStore.paused) return VideoOverlay(videoStore: videoStore); + return Observer( + builder: (_) => AnimatedOpacity( + opacity: videoStore.overlayVisible ? 1.0 : 0.0, + duration: const Duration(milliseconds: 200), + child: ColoredBox( + color: const Color.fromRGBO(0, 0, 0, 0.5), + child: IgnorePointer( + ignoring: !videoStore.overlayVisible, + child: VideoOverlay(videoStore: videoStore), + ), + ), + ), + ); + }, + ) + ], + ); + } + return player; + }, ), ); - final chat = Chat( - key: GlobalKey(), - chatStore: chatStore, - ); + final chat = Chat(chatStore: chatStore); final appBar = AppBar( title: Text( @@ -90,16 +123,7 @@ class _VideoChatState extends State { bottom: false, child: settingsStore.showVideo ? settingsStore.fullScreen - ? Stack( - children: [ - Visibility( - visible: false, - maintainState: true, - child: chat, - ), - Center(child: video), - ], - ) + ? video : Row( children: [ Flexible( @@ -136,14 +160,15 @@ class _VideoChatState extends State { Observer( builder: (_) { if (settingsStore.showVideo) { - return video; + return AspectRatio( + aspectRatio: 16 / 9, + child: video, + ); } return appBar; }, ), - Expanded( - child: chat, - ), + Expanded(child: chat), ], ), );