Add universal fullscreen option & minor fixes

Reverted to hybrid composition for webview due to pausing on Android
when rotating. Tradeoff is screen flicker/flash when toggling video.
This commit is contained in:
tommyxchow 2021-12-25 22:35:10 -05:00
parent ce20ab7256
commit 89b54ff08e
12 changed files with 142 additions and 115 deletions

View file

@ -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

View file

@ -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<void> init() async {
// Retrieve the instance that will allow us to store and persist settings.
final prefs = await SharedPreferences.getInstance();

View file

@ -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}
''';
}
}

View file

@ -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,
),
],
),

View file

@ -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<StreamTwitch>();
ObservableList<StreamTwitch> get streams => _streams;
/// The loading status for pagination.
bool _isLoading = false;

View file

@ -11,12 +11,14 @@ part of 'category_streams_store.dart';
mixin _$CategoryStreamsStore on _CategoryStreamsStoreBase, Store {
final _$_streamsAtom = Atom(name: '_CategoryStreamsStoreBase._streams');
@override
ObservableList<StreamTwitch> get _streams {
ObservableList<StreamTwitch> get streams {
_$_streamsAtom.reportRead();
return super._streams;
}
@override
ObservableList<StreamTwitch> get _streams => streams;
@override
set _streams(ObservableList<StreamTwitch> value) {
_$_streamsAtom.reportWrite(value, super._streams, () {

View file

@ -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<SettingsStore>().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<SettingsStore>().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,
);
},
),
),
);
}
}

View file

@ -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<SettingsStore>());
},
),
);
},
onPressed: () => showModalBottomSheet(
context: context,
builder: (context) {
return Settings(settingsStore: context.read<SettingsStore>());
},
),
),
],
),
@ -107,7 +102,7 @@ class VideoOverlay extends StatelessWidget {
Icons.fullscreen,
color: Color(0xFFFFFFFF),
),
onPressed: videoStore.requestFullscreen,
onPressed: () => context.read<SettingsStore>().fullScreen = !context.read<SettingsStore>().fullScreen,
)
],
)

View file

@ -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();');
}
}

View file

@ -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<AuthStore>(),
),
);
final chat = Chat(
key: GlobalKey(),
chatStore: ChatStore(
auth: context.read<AuthStore>(),
settings: context.read<SettingsStore>(),
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<SettingsStore>().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<SettingsStore>().videoEnabled) {
return AspectRatio(
aspectRatio: 16 / 9,
child: Video(
title: title,
userName: userName,
userLogin: userLogin,
videoStore: VideoStore(
userLogin: userLogin,
authStore: context.read<AuthStore>(),
),
),
);
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<SettingsStore>());
},
),
onPressed: () => showModalBottomSheet(
context: context,
builder: (context) {
return Settings(settingsStore: context.read<SettingsStore>());
},
),
),
],
@ -66,17 +103,11 @@ class VideoChat extends StatelessWidget {
},
),
Expanded(
child: Chat(
chatStore: ChatStore(
auth: context.read<AuthStore>(),
settings: context.read<SettingsStore>(),
channelName: userLogin,
),
),
child: chat,
),
],
),
),
);
}),
);
}
}

View file

@ -50,13 +50,14 @@ class _SearchState extends State<Search> {
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

View file

@ -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',