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:
parent
ce20ab7256
commit
89b54ff08e
12 changed files with 142 additions and 115 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
''';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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, () {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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();');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue