diff --git a/android/app/build.gradle b/android/app/build.gradle index 1ce6b425..c6de7d9e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -44,7 +44,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.tommychow.frosty" - minSdkVersion 20 + minSdkVersion 19 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/lib/core/settings/settings_store.dart b/lib/core/settings/settings_store.dart index 6b340efd..86c10775 100644 --- a/lib/core/settings/settings_store.dart +++ b/lib/core/settings/settings_store.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -22,6 +21,9 @@ abstract class _SettingsStoreBase with Store { @observable var zeroWidthEnabled = false; + @observable + var fullScreen = false; + Future init() async { // Retrieve the instance that will allow us to store and persist settings. final prefs = await SharedPreferences.getInstance(); diff --git a/lib/core/settings/settings_store.g.dart b/lib/core/settings/settings_store.g.dart index 86b6054d..b7546570 100644 --- a/lib/core/settings/settings_store.g.dart +++ b/lib/core/settings/settings_store.g.dart @@ -86,6 +86,21 @@ mixin _$SettingsStore on _SettingsStoreBase, Store { }); } + final _$fullScreenAtom = Atom(name: '_SettingsStoreBase.fullScreen'); + + @override + bool get fullScreen { + _$fullScreenAtom.reportRead(); + return super.fullScreen; + } + + @override + set fullScreen(bool value) { + _$fullScreenAtom.reportWrite(value, super.fullScreen, () { + super.fullScreen = value; + }); + } + @override String toString() { return ''' @@ -93,7 +108,8 @@ videoEnabled: ${videoEnabled}, overlayEnabled: ${overlayEnabled}, messageLimit: ${messageLimit}, hideBannedMessages: ${hideBannedMessages}, -zeroWidthEnabled: ${zeroWidthEnabled} +zeroWidthEnabled: ${zeroWidthEnabled}, +fullScreen: ${fullScreen} '''; } } diff --git a/lib/screens/categories/category_card.dart b/lib/screens/categories/category_card.dart index c33798ea..eb5123ca 100644 --- a/lib/screens/categories/category_card.dart +++ b/lib/screens/categories/category_card.dart @@ -43,12 +43,10 @@ class CategoryCard extends StatelessWidget { ), ), const SizedBox(height: 5.0), - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Text( - category.name, - style: const TextStyle(fontWeight: FontWeight.bold), - ), + Text( + category.name, + style: const TextStyle(fontWeight: FontWeight.bold), + overflow: TextOverflow.ellipsis, ), ], ), diff --git a/lib/screens/categories/category_streams_list/category_streams_store.dart b/lib/screens/categories/category_streams_list/category_streams_store.dart index 69a1b2e1..1cd9206e 100644 --- a/lib/screens/categories/category_streams_list/category_streams_store.dart +++ b/lib/screens/categories/category_streams_list/category_streams_store.dart @@ -12,9 +12,8 @@ abstract class _CategoryStreamsStoreBase with Store { final CategoryTwitch categoryInfo; /// The list of the fetched streams under the category. - @observable + @readonly var _streams = ObservableList(); - ObservableList get streams => _streams; /// The loading status for pagination. bool _isLoading = false; diff --git a/lib/screens/categories/category_streams_list/category_streams_store.g.dart b/lib/screens/categories/category_streams_list/category_streams_store.g.dart index e942e6a9..475eb5ab 100644 --- a/lib/screens/categories/category_streams_list/category_streams_store.g.dart +++ b/lib/screens/categories/category_streams_list/category_streams_store.g.dart @@ -11,12 +11,14 @@ part of 'category_streams_store.dart'; mixin _$CategoryStreamsStore on _CategoryStreamsStoreBase, Store { final _$_streamsAtom = Atom(name: '_CategoryStreamsStoreBase._streams'); - @override - ObservableList get _streams { + ObservableList get streams { _$_streamsAtom.reportRead(); return super._streams; } + @override + ObservableList get _streams => streams; + @override set _streams(ObservableList value) { _$_streamsAtom.reportWrite(value, super._streams, () { diff --git a/lib/screens/channel/video/video.dart b/lib/screens/channel/video/video.dart index f9972738..6f39c636 100644 --- a/lib/screens/channel/video/video.dart +++ b/lib/screens/channel/video/video.dart @@ -9,49 +9,45 @@ import 'package:webview_flutter/webview_flutter.dart'; class Video extends StatelessWidget { final String userLogin; - final String userName; - final String title; final VideoStore videoStore; const Video({ Key? key, - required this.title, - required this.userName, required this.userLogin, required this.videoStore, }) : super(key: key); @override Widget build(BuildContext context) { - return Observer( - builder: (context) { - if (context.read().overlayEnabled) { - return Stack( - children: [ - WebView( - 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(); - }, - ), - VideoOverlay(videoStore: videoStore), - ], - ); - } else { - return WebView( - initialUrl: 'https://player.twitch.tv/?channel=$userLogin&muted=false&parent=frosty', - javascriptMode: JavascriptMode.unrestricted, - allowsInlineMediaPlayback: true, - initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, - ); - } - }, + return SafeArea( + child: AspectRatio( + aspectRatio: 16 / 9, + child: Observer( + builder: (context) { + if (context.read().overlayEnabled) { + return Stack( + children: [ + WebView( + 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(), + ), + VideoOverlay(videoStore: videoStore), + ], + ); + } + return WebView( + initialUrl: 'https://player.twitch.tv/?channel=$userLogin&muted=false&parent=frosty', + javascriptMode: JavascriptMode.unrestricted, + allowsInlineMediaPlayback: true, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ); + }, + ), + ), ); } } diff --git a/lib/screens/channel/video/video_overlay.dart b/lib/screens/channel/video/video_overlay.dart index 56ac2c9f..45098477 100644 --- a/lib/screens/channel/video/video_overlay.dart +++ b/lib/screens/channel/video/video_overlay.dart @@ -44,17 +44,12 @@ class VideoOverlay extends StatelessWidget { Icons.settings, color: Color(0xFFFFFFFF), ), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - fullscreenDialog: true, - builder: (context) { - return Settings(settingsStore: context.read()); - }, - ), - ); - }, + onPressed: () => showModalBottomSheet( + context: context, + builder: (context) { + return Settings(settingsStore: context.read()); + }, + ), ), ], ), @@ -107,7 +102,7 @@ class VideoOverlay extends StatelessWidget { Icons.fullscreen, color: Color(0xFFFFFFFF), ), - onPressed: videoStore.requestFullscreen, + onPressed: () => context.read().fullScreen = !context.read().fullScreen, ) ], ) diff --git a/lib/screens/channel/video/video_store.dart b/lib/screens/channel/video/video_store.dart index 92022029..b542a3cc 100644 --- a/lib/screens/channel/video/video_store.dart +++ b/lib/screens/channel/video/video_store.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:io'; import 'package:frosty/api/twitch_api.dart'; import 'package:frosty/core/auth/auth_store.dart'; import 'package:frosty/models/stream.dart'; @@ -11,7 +10,7 @@ part 'video_store.g.dart'; class VideoStore = _VideoStoreBase with _$VideoStore; abstract class _VideoStoreBase with Store { - late final WebViewController controller; + WebViewController? controller; late Timer overlayTimer; late Timer updateTimer; @@ -30,8 +29,6 @@ abstract class _VideoStoreBase with Store { final AuthStore authStore; _VideoStoreBase({required this.userLogin, required this.authStore}) { - if (Platform.isAndroid) WebView.platform = AndroidWebView(); - overlayTimer = Timer(const Duration(seconds: 3), () => _menuVisible = false); updateStreamInfo(); } @@ -39,9 +36,9 @@ abstract class _VideoStoreBase with Store { @action void handlePausePlay() { if (_paused) { - controller.runJavascript('document.getElementsByTagName("video")[0].play();'); + controller?.runJavascript('document.getElementsByTagName("video")[0].play();'); } else { - controller.runJavascript('document.getElementsByTagName("video")[0].pause();'); + controller?.runJavascript('document.getElementsByTagName("video")[0].pause();'); } _paused = !_paused; @@ -72,19 +69,11 @@ abstract class _VideoStoreBase with Store { } void initVideo() { - controller.runJavascript('document.getElementsByTagName("button")[0].click();'); - controller.runJavascript('document.getElementsByTagName("video")[0].muted = false;'); + controller?.runJavascript('document.getElementsByTagName("button")[0].click();'); + controller?.runJavascript('document.getElementsByTagName("video")[0].muted = false;'); } void requestPictureInPicture() { - controller.runJavascript('document.getElementsByTagName("video")[0].requestPictureInPicture();'); - } - - void requestFullscreen() { - if (Platform.isIOS) { - controller.runJavascript('document.getElementsByTagName("video")[0].webkitEnterFullscreen();'); - } else { - controller.runJavascript('document.getElementsByTagName("video")[0].requestFullscreen();'); - } + controller?.runJavascript('document.getElementsByTagName("video")[0].requestPictureInPicture();'); } } diff --git a/lib/screens/channel/video_chat.dart b/lib/screens/channel/video_chat.dart index 3a4ddd34..c10a26b4 100644 --- a/lib/screens/channel/video_chat.dart +++ b/lib/screens/channel/video_chat.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:frosty/core/auth/auth_store.dart'; import 'package:frosty/core/settings/settings.dart'; @@ -23,25 +24,64 @@ class VideoChat extends StatelessWidget { @override Widget build(BuildContext context) { + final video = Video( + key: GlobalKey(), + userLogin: userLogin, + videoStore: VideoStore( + userLogin: userLogin, + authStore: context.read(), + ), + ); + + final chat = Chat( + key: GlobalKey(), + chatStore: ChatStore( + auth: context.read(), + settings: context.read(), + channelName: userLogin, + ), + ); + return Scaffold( - body: SafeArea( - child: Column( + body: OrientationBuilder(builder: (context, orientation) { + if (orientation == Orientation.landscape) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); + return Observer( + builder: (context) { + if (context.read().fullScreen) { + return Stack( + children: [ + Visibility( + visible: false, + maintainState: true, + child: chat, + ), + Center(child: video), + ], + ); + } + return Row( + children: [ + Flexible( + flex: 2, + child: video, + ), + Flexible( + flex: 1, + child: chat, + ), + ], + ); + }, + ); + } + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: [SystemUiOverlay.bottom, SystemUiOverlay.top]); + return Column( children: [ Observer( builder: (context) { if (context.read().videoEnabled) { - return AspectRatio( - aspectRatio: 16 / 9, - child: Video( - title: title, - userName: userName, - userLogin: userLogin, - videoStore: VideoStore( - userLogin: userLogin, - authStore: context.read(), - ), - ), - ); + return video; } return AppBar( title: Text( @@ -51,14 +91,11 @@ class VideoChat extends StatelessWidget { actions: [ IconButton( icon: const Icon(Icons.settings), - onPressed: () => Navigator.push( - context, - MaterialPageRoute( - fullscreenDialog: true, - builder: (context) { - return Settings(settingsStore: context.read()); - }, - ), + onPressed: () => showModalBottomSheet( + context: context, + builder: (context) { + return Settings(settingsStore: context.read()); + }, ), ), ], @@ -66,17 +103,11 @@ class VideoChat extends StatelessWidget { }, ), Expanded( - child: Chat( - chatStore: ChatStore( - auth: context.read(), - settings: context.read(), - channelName: userLogin, - ), - ), + child: chat, ), ], - ), - ), + ); + }), ); } } diff --git a/lib/screens/search/search.dart b/lib/screens/search/search.dart index 740451d7..2a856570 100644 --- a/lib/screens/search/search.dart +++ b/lib/screens/search/search.dart @@ -50,13 +50,14 @@ class _SearchState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Padding( - padding: EdgeInsets.all(15.0), - child: Text( - 'HISTORY', - style: TextStyle(fontWeight: FontWeight.bold), + if (searchStore.searchHistory.isNotEmpty) + const Padding( + padding: EdgeInsets.all(15.0), + child: Text( + 'HISTORY', + style: TextStyle(fontWeight: FontWeight.bold), + ), ), - ), Expanded( child: ListView( children: searchStore.searchHistory diff --git a/lib/screens/stream_list/stream_card.dart b/lib/screens/stream_list/stream_card.dart index 0d161ae5..de216a08 100644 --- a/lib/screens/stream_list/stream_card.dart +++ b/lib/screens/stream_list/stream_card.dart @@ -78,9 +78,7 @@ class StreamCard extends StatelessWidget { child: Text(streamInfo.title.replaceAll('\n', '')), ), const SizedBox(height: 5.0), - Text( - streamInfo.gameName, - ), + Text(streamInfo.gameName), const SizedBox(height: 5.0), Text( '${NumberFormat().format(streamInfo.viewerCount)} viewers',