Add initial support for PiP on Android (#154)

This commit is contained in:
Tommy Chow 2022-07-25 16:31:14 -04:00
parent e91f231173
commit 402d75cc21
5 changed files with 105 additions and 95 deletions

View file

@ -19,6 +19,7 @@
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:supportsPictureInPicture="true"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">

View file

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:frosty/models/irc.dart';
import 'package:intl/intl.dart';
class ChatModes extends StatelessWidget {
final ROOMSTATE roomState;
@ -11,10 +10,6 @@ class ChatModes extends StatelessWidget {
@override
Widget build(BuildContext context) {
final test = Intl.plural(2, other: 'test');
debugPrint(test);
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:floating/floating.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:frosty/api/twitch_api.dart';
@ -24,6 +25,8 @@ abstract class VideoStoreBase with Store {
Timer? sleepTimer;
final floating = Floating();
@observable
var sleepHours = 0;
@ -184,7 +187,11 @@ abstract class VideoStoreBase with Store {
void requestPictureInPicture() {
try {
controller?.runJavascript('document.getElementsByTagName("video")[0].requestPictureInPicture();');
if (Platform.isAndroid) {
floating.enable();
} else if (Platform.isIOS) {
controller?.runJavascript('document.getElementsByTagName("video")[0].requestPictureInPicture();');
}
} catch (e) {
debugPrint(e.toString());
}
@ -228,4 +235,9 @@ abstract class VideoStoreBase with Store {
sleepTimer?.cancel();
timeRemaining = const Duration();
}
void dispose() {
floating.dispose();
sleepTimer?.cancel();
}
}

View file

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:io';
import 'package:floating/floating.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
@ -160,90 +161,94 @@ class _VideoChatState extends State<VideoChat> {
],
);
return Scaffold(
body: OrientationBuilder(
builder: (context, orientation) {
// Scroll to bottom when summoning keyboard or rotating.
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_chatStore.scrollController.hasClients) _chatStore.scrollController.jumpTo(_chatStore.scrollController.position.maxScrollExtent);
});
return PiPSwitcher(
floating: _videoStore.floating,
childWhenEnabled: player,
childWhenDisabled: Scaffold(
body: OrientationBuilder(
builder: (context, orientation) {
// Scroll to bottom when summoning keyboard or rotating.
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_chatStore.scrollController.hasClients) _chatStore.scrollController.jumpTo(_chatStore.scrollController.position.maxScrollExtent);
});
if (orientation == Orientation.landscape) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
if (orientation == Orientation.landscape) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
return Observer(
builder: (context) {
final landscapeChat = AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: _chatStore.expandChat ? MediaQuery.of(context).size.width / 2 : MediaQuery.of(context).size.width * _chatStore.settings.chatWidth,
curve: Curves.ease,
color: _chatStore.settings.fullScreen
? Colors.black.withOpacity(_chatStore.settings.fullScreenChatOverlayOpacity)
: Theme.of(context).scaffoldBackgroundColor,
child: chat,
);
return Observer(
builder: (context) {
final landscapeChat = AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: _chatStore.expandChat ? MediaQuery.of(context).size.width / 2 : MediaQuery.of(context).size.width * _chatStore.settings.chatWidth,
curve: Curves.ease,
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 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,
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)],
),
),
);
},
return ColoredBox(
color: settingsStore.showVideo ? Colors.black : Theme.of(context).scaffoldBackgroundColor,
child: SafeArea(
bottom: false,
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: [
Observer(
builder: (_) {
if (!settingsStore.showVideo) return appBar;
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
overlays: SystemUiOverlay.values,
);
return SafeArea(
child: Column(
children: [
Observer(
builder: (_) {
if (!settingsStore.showVideo) return appBar;
return AspectRatio(aspectRatio: 16 / 9, child: video);
},
),
Expanded(child: chat),
],
),
);
},
return AspectRatio(aspectRatio: 16 / 9, child: video);
},
),
Expanded(child: chat),
],
),
);
},
),
),
);
}
@ -252,7 +257,7 @@ class _VideoChatState extends State<VideoChat> {
void dispose() {
_chatStore.dispose();
_videoStore.cancelSleepTimer();
_videoStore.dispose();
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
@ -614,7 +619,7 @@ class _VideoOverlay extends StatelessWidget {
),
),
),
if (Platform.isIOS && videoStore.settingsStore.pictureInPicture)
if (videoStore.settingsStore.pictureInPicture)
IconButton(
tooltip: 'Picture-in-picture',
icon: const Icon(

View file

@ -1,5 +1,3 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:frosty/screens/settings/stores/settings_store.dart';
@ -34,14 +32,13 @@ class VideoSettings extends StatelessWidget {
value: settingsStore.showOverlay,
onChanged: settingsStore.showVideo ? (newValue) => settingsStore.showOverlay = newValue : null,
),
if (Platform.isIOS)
SwitchListTile.adaptive(
isThreeLine: true,
title: const Text('Picture-in-picture Button (experimental)'),
subtitle: const Text('Adds a button to enter PiP mode on the bottom right of the overlay (may cause freezes/crashes).'),
value: settingsStore.pictureInPicture,
onChanged: settingsStore.showVideo && settingsStore.showOverlay ? (newValue) => settingsStore.pictureInPicture = newValue : null,
),
SwitchListTile.adaptive(
isThreeLine: true,
title: const Text('Picture-in-picture Button (experimental)'),
subtitle: const Text('Adds a button to enter PiP mode on the bottom right of the overlay (may cause freezes/crashes).'),
value: settingsStore.pictureInPicture,
onChanged: settingsStore.showVideo && settingsStore.showOverlay ? (newValue) => settingsStore.pictureInPicture = newValue : null,
),
SwitchListTile.adaptive(
isThreeLine: true,
title: const Text('Long-press player to toggle overlay'),