Compare commits

...
Sign in to create a new pull request.

14 commits

Author SHA1 Message Date
Tommy Chow
1d9654663d Add native setting disclaimer 2023-04-23 23:35:28 -04:00
Tommy Chow
0b23bf5834 Fix native video not disposing when changing setting 2023-04-23 21:04:47 -04:00
Tommy Chow
3aa0ce5870 Fix video pausing when changing quality 2023-04-23 21:04:07 -04:00
Tommy Chow
c0ec9efa29 Fix native player not disposing properly 2023-04-20 00:30:28 -04:00
Tommy Chow
781463a456 Use responsive bottom sheet for quality options 2023-04-18 22:25:17 -04:00
Tommy Chow
d536f0706c Use adaptive indicator 2023-04-18 17:49:58 -04:00
Tommy Chow
9a8e51c332 Sort quality options from highest to lowest 2023-04-17 21:35:44 -04:00
Tommy Chow
c4266aee0e Fix player background color 2023-04-17 18:40:22 -04:00
Tommy Chow
b1d27de3ea Fix stream token retrieval on iOS 2023-04-15 16:32:52 -04:00
Tommy Chow
fe4084ba35 Fix off-center native video player in landscape mode 2023-04-15 13:30:20 -04:00
Tommy Chow
8bde136eb2 Add quality options to native video player 2023-04-15 13:29:50 -04:00
Tommy Chow
1b5bcf19f5 Add initial native player implementation 2023-04-15 11:19:41 -04:00
Tommy Chow
16715957ab Add getStreamLinks request 2023-04-15 11:13:47 -04:00
Tommy Chow
4ca3139fb3 Add video_player package 2023-04-15 11:12:11 -04:00
13 changed files with 481 additions and 124 deletions

View file

@ -266,6 +266,18 @@ class TwitchApi {
}
}
/// Returns a map containing the stream links associated with the given [userLogin].
Future<Map<String, String>> getStreamLinks({required String userLogin, String? token}) async {
final uri = Uri.parse('https://z4qz5trca4plcyhcjvld2yhrme0jgdxw.lambda-url.us-east-2.on.aws/?channel=$userLogin');
final response = await _client.get(uri, headers: token != null ? {'frosty-token': token} : null);
if (response.statusCode == 200) {
return Map.castFrom(jsonDecode(response.body));
} else {
return Future.error('Failed to get stream links');
}
}
/// Returns a [UserTwitch] object containing the user info associated with the given [userLogin].
Future<UserTwitch> getUser({
String? userLogin,

View file

@ -311,8 +311,6 @@ class _VideoChatState extends State<VideoChat> {
void dispose() {
_chatStore.dispose();
_videoStore.dispose();
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
overlays: SystemUiOverlay.values,

View file

@ -1,11 +1,12 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:frosty/screens/channel/video/video_store.dart';
import 'package:simple_pip_mode/simple_pip.dart';
import 'package:video_player/video_player.dart';
import 'package:webview_flutter/webview_flutter.dart';
/// Creates a [WebView] widget that shows a channel's video stream.
class Video extends StatefulWidget {
final VideoStore videoStore;
@ -34,22 +35,101 @@ class _VideoState extends State<Video> with WidgetsBindingObserver {
@override
Widget build(BuildContext context) {
return WebView(
backgroundColor: Colors.black,
initialUrl: widget.videoStore.videoUrl,
javascriptMode: JavascriptMode.unrestricted,
allowsInlineMediaPlayback: true,
initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
onWebViewCreated: (controller) => widget.videoStore.controller = controller,
onPageFinished: (string) => widget.videoStore.initVideo(),
navigationDelegate: widget.videoStore.handleNavigation,
javascriptChannels: widget.videoStore.javascriptChannels,
);
return Observer(builder: (context) {
if (widget.videoStore.settingsStore.useNativePlayer) return NativeVideo(videoStore: widget.videoStore);
return WebVideo(videoStore: widget.videoStore);
});
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
widget.videoStore.dispose();
super.dispose();
}
}
/// Creates a [WebView] widget that shows a channel's video stream.
class WebVideo extends StatelessWidget {
final VideoStore videoStore;
const WebVideo({
Key? key,
required this.videoStore,
}) : super(key: key);
@override
Widget build(BuildContext context) {
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,
);
}
}
class NativeVideo extends StatefulWidget {
final VideoStore videoStore;
const NativeVideo({
Key? key,
required this.videoStore,
}) : super(key: key);
@override
State<NativeVideo> createState() => _NativeVideoState();
}
class _NativeVideoState extends State<NativeVideo> {
@override
void initState() {
super.initState();
widget.videoStore.initVideo().then((value) {
if (mounted) {
widget.videoStore.videoPlayerController?.play();
} else {
widget.videoStore.videoPlayerController?.dispose();
}
});
}
@override
Widget build(BuildContext context) {
return Center(
child: AspectRatio(
aspectRatio: 16 / 9,
child: ColoredBox(
color: Colors.black,
child: Observer(
builder: (context) {
final videoPlayerController = widget.videoStore.videoPlayerController;
if (widget.videoStore.streamLinks == null || videoPlayerController == null) {
return const Center(child: CircularProgressIndicator.adaptive());
}
return VideoPlayer(videoPlayerController);
},
),
),
),
);
}
@override
dispose() {
widget.videoStore.videoPlayerController?.dispose();
super.dispose();
}
}

View file

@ -120,6 +120,19 @@ class VideoOverlay extends StatelessWidget {
return Observer(
builder: (context) {
final qualities = videoStore.streamLinks?.keys
.map(
(quality) => ListTile(
title: Text(quality),
trailing: videoStore.selectedQuality == quality ? const Icon(Icons.check_rounded) : null,
onTap: () {
videoStore.handleQualityChange(quality);
Navigator.of(context).pop();
},
),
)
.toList();
return Stack(
children: [
Row(
@ -135,6 +148,43 @@ class VideoOverlay extends StatelessWidget {
subtitleTextWeight: FontWeight.w500,
),
),
const Spacer(),
if (videoStore.settingsStore.useNativePlayer && videoStore.streamLinks != null)
IconButton(
onPressed: () => showModalBottomSheet(
backgroundColor: Colors.transparent,
isScrollControlled: true,
context: context,
builder: (context) => FrostyBottomSheet(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.all(16.0),
child: Text(
'Select quality',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
),
MediaQuery.of(context).orientation == Orientation.landscape
? SizedBox(
height: MediaQuery.of(context).size.height * 0.5,
child: ListView(
children: qualities!,
),
)
: Column(
children: qualities!,
)
],
),
),
),
icon: const Icon(
Icons.settings_rounded,
color: Colors.white,
),
),
if (videoStore.settingsStore.fullScreen && orientation == Orientation.landscape) chatOverlayButton,
],
),

View file

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:collection';
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
@ -10,6 +11,7 @@ import 'package:frosty/screens/settings/stores/auth_store.dart';
import 'package:frosty/screens/settings/stores/settings_store.dart';
import 'package:mobx/mobx.dart';
import 'package:simple_pip_mode/simple_pip.dart';
import 'package:video_player/video_player.dart';
import 'package:webview_flutter/webview_flutter.dart';
part 'video_store.g.dart';
@ -32,6 +34,9 @@ abstract class VideoStoreBase with Store {
/// The webview controller used for injecting JavaScript to control the webview and video player.
WebViewController? controller;
@readonly
VideoPlayerController? _videoPlayerController;
/// The timer that handles hiding the overlay automatically
late Timer _overlayTimer;
@ -84,6 +89,13 @@ abstract class VideoStoreBase with Store {
@readonly
StreamTwitch? _streamInfo;
/// A map of stream qualities to their respective URLs for the current channel.
@readonly
Map<String, String>? _streamLinks;
@readonly
var _selectedQuality = '';
/// The video URL to use for the webview. Controls will be disabled when custom overlay is enabled.
@computed
String get videoUrl => settingsStore.showOverlay
@ -115,17 +127,50 @@ abstract class VideoStoreBase with Store {
updateStreamInfo();
}
Future<VideoPlayerController> createVideoPlayerController(String url) async {
final controller = VideoPlayerController.network(
url,
videoPlayerOptions: VideoPlayerOptions(allowBackgroundPlayback: true),
);
controller.addListener(() {
if (_videoPlayerController?.value.isPlaying == true) {
_paused = false;
} else {
_paused = true;
}
});
await controller.initialize();
await controller.play();
return controller;
}
/// Initializes the video webview.
@action
Future<void> initVideo() async {
// Add event listeners to notify the JavaScript channels when the video plays and pauses.
try {
controller?.runJavascript(
'document.getElementsByTagName("video")[0].addEventListener("pause", () => VideoPause.postMessage("video paused"));');
controller?.runJavascript(
'document.getElementsByTagName("video")[0].addEventListener("playing", () => VideoPlaying.postMessage("video playing"));');
} catch (e) {
debugPrint(e.toString());
if (settingsStore.useNativePlayer) {
_streamLinks = await twitchApi.getStreamLinks(userLogin: userLogin, token: authStore.streamLinkToken);
SplayTreeMap<String, String> sortedStreamLinks = SplayTreeMap<String, String>(
(a, b) => int.parse(b.split('p').first).compareTo(int.parse(a.split('p').first)),
);
sortedStreamLinks.addAll(_streamLinks!);
_streamLinks = sortedStreamLinks;
_selectedQuality = _streamLinks!.keys.first;
_videoPlayerController = await createVideoPlayerController(_streamLinks?[_selectedQuality] ?? '');
} else {
// Add event listeners to notify the JavaScript channels when the video plays and pauses.
try {
controller?.runJavascript(
'document.getElementsByTagName("video")[0].addEventListener("pause", () => VideoPause.postMessage("video paused"));');
controller?.runJavascript(
'document.getElementsByTagName("video")[0].addEventListener("playing", () => VideoPlaying.postMessage("video playing"));');
} catch (e) {
debugPrint(e.toString());
}
}
// Determine whether the device is an iPad or not.
@ -142,6 +187,15 @@ abstract class VideoStoreBase with Store {
}
}
@action
Future<void> handleQualityChange(String quality) async {
await _videoPlayerController?.dispose();
_selectedQuality = quality;
_videoPlayerController = await createVideoPlayerController(_streamLinks?[_selectedQuality] ?? '');
}
/// Called whenever the video/overlay is tapped.
@action
void handleVideoTap() {
@ -158,8 +212,6 @@ abstract class VideoStoreBase with Store {
}
/// Updates the stream info from the Twitch API.
///
/// If the stream is offline, disables the overlay.
@action
Future<void> updateStreamInfo() async {
try {
@ -167,6 +219,7 @@ abstract class VideoStoreBase with Store {
} catch (e) {
debugPrint(e.toString());
// If the stream is offline or if there is an error, disable the overlay.
_overlayTimer.cancel();
_streamInfo = null;
_paused = true;
@ -204,8 +257,12 @@ abstract class VideoStoreBase with Store {
void handlePausePlay() {
try {
if (_paused) {
_videoPlayerController?.play();
controller?.runJavascript('document.getElementsByTagName("video")[0].play();');
} else {
_videoPlayerController?.pause();
controller?.runJavascript('document.getElementsByTagName("video")[0].pause();');
}
} catch (e) {
@ -242,6 +299,8 @@ abstract class VideoStoreBase with Store {
// Will both prevent the video from continuing to play when dismissed and closes PiP on iOS.
if (Platform.isIOS) controller?.reload();
_videoPlayerController?.dispose();
_disposeOverlayReaction();
}
}

View file

@ -17,6 +17,25 @@ mixin _$VideoStore on VideoStoreBase, Store {
name: 'VideoStoreBase.videoUrl'))
.value;
late final _$_videoPlayerControllerAtom =
Atom(name: 'VideoStoreBase._videoPlayerController', context: context);
VideoPlayerController? get videoPlayerController {
_$_videoPlayerControllerAtom.reportRead();
return super._videoPlayerController;
}
@override
VideoPlayerController? get _videoPlayerController => videoPlayerController;
@override
set _videoPlayerController(VideoPlayerController? value) {
_$_videoPlayerControllerAtom
.reportWrite(value, super._videoPlayerController, () {
super._videoPlayerController = value;
});
}
late final _$_pausedAtom =
Atom(name: 'VideoStoreBase._paused', context: context);
@ -89,6 +108,42 @@ mixin _$VideoStore on VideoStoreBase, Store {
});
}
late final _$_streamLinksAtom =
Atom(name: 'VideoStoreBase._streamLinks', context: context);
Map<String, String>? get streamLinks {
_$_streamLinksAtom.reportRead();
return super._streamLinks;
}
@override
Map<String, String>? get _streamLinks => streamLinks;
@override
set _streamLinks(Map<String, String>? value) {
_$_streamLinksAtom.reportWrite(value, super._streamLinks, () {
super._streamLinks = value;
});
}
late final _$_selectedQualityAtom =
Atom(name: 'VideoStoreBase._selectedQuality', context: context);
String get selectedQuality {
_$_selectedQualityAtom.reportRead();
return super._selectedQuality;
}
@override
String get _selectedQuality => selectedQuality;
@override
set _selectedQuality(String value) {
_$_selectedQualityAtom.reportWrite(value, super._selectedQuality, () {
super._selectedQuality = value;
});
}
late final _$initVideoAsyncAction =
AsyncAction('VideoStoreBase.initVideo', context: context);
@ -97,6 +152,15 @@ mixin _$VideoStore on VideoStoreBase, Store {
return _$initVideoAsyncAction.run(() => super.initVideo());
}
late final _$handleQualityChangeAsyncAction =
AsyncAction('VideoStoreBase.handleQualityChange', context: context);
@override
Future<void> handleQualityChange(String quality) {
return _$handleQualityChangeAsyncAction
.run(() => super.handleQualityChange(quality));
}
late final _$updateStreamInfoAsyncAction =
AsyncAction('VideoStoreBase.updateStreamInfo', context: context);

View file

@ -37,6 +37,7 @@ class ProfileCard extends StatelessWidget {
initialUrl: authStore.loginUri.toString(),
navigationDelegate: (navigation) => authStore.handleNavigation(navigation: navigation),
javascriptMode: JavascriptMode.unrestricted,
onWebViewCreated: (controller) => authStore.webViewController = controller,
),
);
},

View file

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
@ -26,6 +27,9 @@ abstract class AuthBase with Store {
/// The shared_preferences key for the user token.
static const _userTokenKey = 'user_token';
/// The shared_preferences key for the user token.
static const _streamLinkTokenKey = 'streamlink_token';
/// The Twitch API service for making requests.
final TwitchApi twitchApi;
@ -35,6 +39,8 @@ abstract class AuthBase with Store {
/// The MobX store containing information relevant to the current user.
final UserStore user;
String? streamLinkToken;
/// The user token used to authenticate with the Twitch API.
@readonly
String? _token;
@ -65,8 +71,10 @@ abstract class AuthBase with Store {
},
);
late WebViewController webViewController;
/// Navigation handler for the login webview. Fires on every navigation request (whenever the URL changes).
FutureOr<NavigationDecision> handleNavigation({required NavigationRequest navigation, Widget? routeAfter}) {
FutureOr<NavigationDecision> handleNavigation({required NavigationRequest navigation, Widget? routeAfter}) async {
// Check if the URL is the redirect URI.
if (navigation.url.startsWith('https://twitch.tv/login')) {
// Extract the token from the query parameters.
@ -81,6 +89,14 @@ abstract class AuthBase with Store {
// When redirected to the redirect_uri, there will be another redirect to "https://www.twitch.tv/?no-reload=true".
// Checking for this will ensure that the user has automatically logged in to Twitch on the WebView itself.
if (navigation.url == 'https://www.twitch.tv/?no-reload=true') {
streamLinkToken = await webViewController.runJavascriptReturningResult(
'document.cookie.split("; ").find(item=>item.startsWith("auth-token="))?.split("=")[1]',
);
// Android returns the token with double quotes around it, so we need to remove them.
if (Platform.isAndroid) streamLinkToken = streamLinkToken?.replaceAll('"', '');
_storage.write(key: _streamLinkTokenKey, value: streamLinkToken);
if (routeAfter != null) {
navigatorKey.currentState?.pop();
navigatorKey.currentState?.push(MaterialPageRoute(builder: (context) => routeAfter));
@ -147,6 +163,8 @@ abstract class AuthBase with Store {
@action
Future<void> init() async {
try {
streamLinkToken = await _storage.read(key: _streamLinkTokenKey);
// Read and set the currently stored user token, if any.
_token = await _storage.read(key: _userTokenKey);
@ -220,6 +238,9 @@ abstract class AuthBase with Store {
@action
Future<void> logout() async {
try {
await _storage.delete(key: _streamLinkTokenKey);
streamLinkToken = null;
// Delete the existing user token.
await _storage.delete(key: _userTokenKey);
_token = null;

View file

@ -55,6 +55,7 @@ abstract class _SettingsStoreBase with Store {
// * Video Settings
// Player defaults
static const defaultShowVideo = true;
static const defaultUseNativePlayer = false;
// Overlay defaults
static const defaultShowOverlay = true;
@ -66,6 +67,10 @@ abstract class _SettingsStoreBase with Store {
@observable
var showVideo = defaultShowVideo;
@JsonKey(defaultValue: defaultUseNativePlayer)
@observable
var useNativePlayer = defaultUseNativePlayer;
// Overlay options
@JsonKey(defaultValue: defaultShowOverlay)
@observable
@ -82,6 +87,7 @@ abstract class _SettingsStoreBase with Store {
@action
void resetVideoSettings() {
showVideo = defaultShowVideo;
useNativePlayer = defaultUseNativePlayer;
showOverlay = defaultShowOverlay;
toggleableOverlay = defaultToggleableOverlay;

View file

@ -15,6 +15,7 @@ SettingsStore _$SettingsStoreFromJson(Map<String, dynamic> json) =>
..largeStreamCard = json['largeStreamCard'] as bool? ?? false
..launchUrlExternal = json['launchUrlExternal'] as bool? ?? false
..showVideo = json['showVideo'] as bool? ?? true
..useNativePlayer = json['useNativePlayer'] as bool? ?? false
..showOverlay = json['showOverlay'] as bool? ?? true
..toggleableOverlay = json['toggleableOverlay'] as bool? ?? false
..overlayOpacity = (json['overlayOpacity'] as num?)?.toDouble() ?? 0.5
@ -61,6 +62,7 @@ Map<String, dynamic> _$SettingsStoreToJson(SettingsStore instance) =>
'largeStreamCard': instance.largeStreamCard,
'launchUrlExternal': instance.launchUrlExternal,
'showVideo': instance.showVideo,
'useNativePlayer': instance.useNativePlayer,
'showOverlay': instance.showOverlay,
'toggleableOverlay': instance.toggleableOverlay,
'overlayOpacity': instance.overlayOpacity,
@ -199,6 +201,22 @@ mixin _$SettingsStore on _SettingsStoreBase, Store {
});
}
late final _$useNativePlayerAtom =
Atom(name: '_SettingsStoreBase.useNativePlayer', context: context);
@override
bool get useNativePlayer {
_$useNativePlayerAtom.reportRead();
return super.useNativePlayer;
}
@override
set useNativePlayer(bool value) {
_$useNativePlayerAtom.reportWrite(value, super.useNativePlayer, () {
super.useNativePlayer = value;
});
}
late final _$showOverlayAtom =
Atom(name: '_SettingsStoreBase.showOverlay', context: context);
@ -734,6 +752,7 @@ showThumbnails: ${showThumbnails},
largeStreamCard: ${largeStreamCard},
launchUrlExternal: ${launchUrlExternal},
showVideo: ${showVideo},
useNativePlayer: ${useNativePlayer},
showOverlay: ${showOverlay},
toggleableOverlay: ${toggleableOverlay},
overlayOpacity: ${overlayOpacity},

View file

@ -21,6 +21,12 @@ class VideoSettings extends StatelessWidget {
value: settingsStore.showVideo,
onChanged: (newValue) => settingsStore.showVideo = newValue,
),
SettingsListSwitch(
title: 'Use native player (experimental)',
subtitle: const Text('Enables quality options. May cause an inconsistent stream delay.'),
value: settingsStore.useNativePlayer,
onChanged: (newValue) => settingsStore.useNativePlayer = newValue,
),
const SectionHeader('Overlay'),
SettingsListSwitch(
title: 'Use custom video overlay',

View file

@ -5,34 +5,34 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "0c80aeab9bc807ab10022cd3b2f4cf2ecdf231949dc1ddd9442406a003f19201"
sha256: a36ec4843dc30ea6bf652bf25e3448db6c5e8bcf4aa55f063a5d1dad216d8214
url: "https://pub.dev"
source: hosted
version: "52.0.0"
version: "58.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: cd8ee83568a77f3ae6b913a36093a1c9b1264e7cb7f834d9ddd2311dade9c1f4
sha256: cc4242565347e98424ce9945c819c192ec0838cb9d1f6aa4a97cc96becbc5b27
url: "https://pub.dev"
source: hosted
version: "5.4.0"
version: "5.10.0"
archive:
dependency: transitive
description:
name: archive
sha256: ed7cc591a948744994714375caf9a2ce89e1d82e8243997c8a2994d57181c212
sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a"
url: "https://pub.dev"
source: hosted
version: "3.3.5"
version: "3.3.7"
args:
dependency: transitive
description:
name: args
sha256: "139d809800a412ebb26a3892da228b2d0ba36f0ef5d9a82166e5e52ec8d61611"
sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
version: "2.4.0"
async:
dependency: transitive
description:
@ -69,18 +69,18 @@ packages:
dependency: transitive
description:
name: build_daemon
sha256: "6bc5544ea6ce4428266e7ea680e945c68806c4aae2da0eb5e9ccf38df8d6acbf"
sha256: "757153e5d9cd88253cb13f28c2fb55a537dc31fefd98137549895b5beb7c6169"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
version: "3.1.1"
build_resolvers:
dependency: transitive
description:
name: build_resolvers
sha256: "7c35a3a7868626257d8aee47b51c26b9dba11eaddf3431117ed2744951416aab"
sha256: db49b8609ef8c81cca2b310618c3017c00f03a92af44c04d310b907b2d692d95
url: "https://pub.dev"
source: hosted
version: "2.1.0"
version: "2.2.0"
build_runner:
dependency: "direct dev"
description:
@ -109,10 +109,10 @@ packages:
dependency: transitive
description:
name: built_value
sha256: "169565c8ad06adb760c3645bf71f00bff161b00002cace266cad42c5d22a7725"
sha256: "31b7c748fd4b9adf8d25d72a4c4a59ef119f12876cf414f94f8af5131d5fa2b0"
url: "https://pub.dev"
source: hosted
version: "8.4.3"
version: "8.4.4"
cached_network_image:
dependency: "direct main"
description:
@ -221,10 +221,10 @@ packages:
dependency: transitive
description:
name: dart_style
sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4"
sha256: "6d691edde054969f0e0f26abb1b30834b5138b963793e56f69d3a9a4435e6352"
url: "https://pub.dev"
source: hosted
version: "2.2.4"
version: "2.3.0"
device_frame:
dependency: transitive
description:
@ -237,10 +237,10 @@ packages:
dependency: "direct main"
description:
name: device_info_plus
sha256: "7ff671ed0a6356fa8f2e1ae7d3558d3fb7b6a41e24455e4f8df75b811fb8e4ab"
sha256: "435383ca05f212760b0a70426b5a90354fe6bd65992b3a5e27ab6ede74c02f5c"
url: "https://pub.dev"
source: hosted
version: "8.0.0"
version: "8.2.0"
device_info_plus_platform_interface:
dependency: transitive
description:
@ -285,10 +285,10 @@ packages:
dependency: transitive
description:
name: fixnum
sha256: "04be3e934c52e082558cc9ee21f42f5c1cd7a1262f4c63cd0357c08d5bba81ec"
sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
version: "1.1.0"
flutter:
dependency: "direct main"
description: flutter
@ -359,10 +359,10 @@ packages:
dependency: transitive
description:
name: flutter_secure_storage_linux
sha256: "736436adaf91552433823f51ce22e098c2f0551db06b6596f58597a25b8ea797"
sha256: "0912ae29a572230ad52d8a4697e5518d7f0f429052fd51df7e5a7952c7efe2a3"
url: "https://pub.dev"
source: hosted
version: "1.1.2"
version: "1.1.3"
flutter_secure_storage_macos:
dependency: transitive
description:
@ -449,10 +449,10 @@ packages:
dependency: transitive
description:
name: html
sha256: d9793e10dbe0e6c364f4c59bf3e01fb33a9b2a674bc7a1081693dba0614b6269
sha256: "79d498e6d6761925a34ee5ea8fa6dfef38607781d2fa91e37523474282af55cb"
url: "https://pub.dev"
source: hosted
version: "0.15.1"
version: "0.15.2"
http:
dependency: "direct main"
description:
@ -521,10 +521,10 @@ packages:
dependency: "direct dev"
description:
name: json_serializable
sha256: "040088d9eb2337f3a51ddabe35261e2fea1c351e3dd29d755ed1290b6b700a75"
sha256: dadc08bd61f72559f938dd08ec20dbfec6c709bba83515085ea943d2078d187a
url: "https://pub.dev"
source: hosted
version: "6.6.0"
version: "6.6.1"
lints:
dependency: transitive
description:
@ -577,18 +577,18 @@ packages:
dependency: "direct main"
description:
name: mobx
sha256: "41f814e90db4d5fe1303efc570db837a56507832c08c4f0842320fa3d40c14cd"
sha256: "6738620307a424d2c9ad8b873f4dce391c44e9135eb4e75668ac8202fec7a9b8"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
version: "2.1.4"
mobx_codegen:
dependency: "direct dev"
description:
name: mobx_codegen
sha256: "86122e410d8ea24dda0c69adb5c2a6ccadd5ce02ad46e144764e0d0184a06181"
sha256: "2748bf457e845af5bae47dceee93eb7336892e9dad811271630eccbb63a10ae7"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.2.0"
nested:
dependency: transitive
description:
@ -617,10 +617,10 @@ packages:
dependency: "direct main"
description:
name: package_info_plus
sha256: f619162573096d428ccde2e33f92e05b5a179cd6f0e3120c1005f181bee8ed16
sha256: cbff87676c352d97116af6dbea05aa28c4d65eb0f6d5677a520c11a69ca9a24d
url: "https://pub.dev"
source: hosted
version: "3.0.2"
version: "3.1.0"
package_info_plus_platform_interface:
dependency: transitive
description:
@ -657,50 +657,50 @@ packages:
dependency: transitive
description:
name: path_provider
sha256: dcea5feb97d8abf90cab9e9030b497fb7c3cbf26b7a1fe9e3ef7dcb0a1ddec95
sha256: c7edf82217d4b2952b2129a61d3ad60f1075b9299e629e149a8d2e39c2e6aad4
url: "https://pub.dev"
source: hosted
version: "2.0.12"
version: "2.0.14"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: a776c088d671b27f6e3aa8881d64b87b3e80201c64e8869b811325de7a76c15e
sha256: da97262be945a72270513700a92b39dd2f4a54dad55d061687e2e37a6390366a
url: "https://pub.dev"
source: hosted
version: "2.0.22"
version: "2.0.25"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "62a68e7e1c6c459f9289859e2fae58290c981ce21d1697faf54910fe1faa4c74"
sha256: ad4c4d011830462633f03eb34445a45345673dfd4faf1ab0b4735fbd93b19183
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.2.2"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379
sha256: "2ae08f2216225427e64ad224a24354221c2c7907e448e6e0e8b57b1eb9f10ad1"
url: "https://pub.dev"
source: hosted
version: "2.1.7"
version: "2.1.10"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76
sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec"
url: "https://pub.dev"
source: hosted
version: "2.0.5"
version: "2.0.6"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bcabbe399d4042b8ee687e17548d5d3f527255253b4a639f5f8d2094a9c2b45c
sha256: f53720498d5a543f9607db4b0e997c4b5438884de25b0f73098cc2671a51b130
url: "https://pub.dev"
source: hosted
version: "2.1.3"
version: "2.1.5"
pedantic:
dependency: transitive
description:
@ -729,18 +729,18 @@ packages:
dependency: transitive
description:
name: plugin_platform_interface
sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a
sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
version: "2.1.4"
pointycastle:
dependency: transitive
description:
name: pointycastle
sha256: db7306cf0249f838d1a24af52b5a5887c5bf7f31d8bb4e827d071dc0939ad346
sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c"
url: "https://pub.dev"
source: hosted
version: "3.6.2"
version: "3.7.3"
pool:
dependency: transitive
description:
@ -777,10 +777,10 @@ packages:
dependency: transitive
description:
name: pubspec_parse
sha256: "75f6614d6dde2dc68948dffbaa4fe5dae32cd700eb9fb763fe11dfb45a3c4d0a"
sha256: ec85d7d55339d85f44ec2b682a82fea340071e8978257e5a43e69f79e98ef50c
url: "https://pub.dev"
source: hosted
version: "1.2.1"
version: "1.2.2"
rxdart:
dependency: transitive
description:
@ -793,74 +793,74 @@ packages:
dependency: transitive
description:
name: sentry
sha256: c64db3444237ff747c5a68f5214897bcb078de248785d0d816e3c55ab94dd71d
sha256: a1529c545fcbc899e5dcc7c94ff1c6ad0c334dfc99a3cda366b1da98af7c5678
url: "https://pub.dev"
source: hosted
version: "6.19.0"
version: "6.22.0"
sentry_flutter:
dependency: "direct main"
description:
name: sentry_flutter
sha256: a76cf5180d571535fb8fc3bf10358ab385d78134fcf652d0e03ba7741525ab09
sha256: cab07e99a8f27af94f399cabceaff6968011660505b30a0e2286728a81bc476c
url: "https://pub.dev"
source: hosted
version: "6.19.0"
version: "6.22.0"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: "5949029e70abe87f75cfe59d17bf5c397619c4b74a099b10116baeb34786fad9"
sha256: "858aaa72d8f61637d64e776aca82e1c67e6d9ee07979123c5d17115031c1b13b"
url: "https://pub.dev"
source: hosted
version: "2.0.17"
version: "2.1.0"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "955e9736a12ba776bdd261cf030232b30eadfcd9c79b32a3250dd4a494e8c8f7"
sha256: "7fa90471a6875d26ad78c7e4a675874b2043874586891128dc5899662c97db46"
url: "https://pub.dev"
source: hosted
version: "2.0.15"
version: "2.1.2"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "1ffa239043ab8baf881ec3094a3c767af9d10399b2839020b9e4d44c0bb23951"
sha256: "0c1c16c56c9708aa9c361541a6f0e5cc6fc12a3232d866a687a7b7db30032b07"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.2.1"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: f8ea038aa6da37090093974ebdcf4397010605fd2ff65c37a66f9d28394cb874
sha256: "9d387433ca65717bbf1be88f4d5bb18f10508917a8fa2fb02e0fd0d7479a9afa"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
version: "2.2.0"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: da9431745ede5ece47bc26d5d73a9d3c6936ef6945c101a5aca46f62e52c1cf3
sha256: fb5cf25c0235df2d0640ac1b1174f6466bd311f621574997ac59018a6664548d
url: "https://pub.dev"
source: hosted
version: "2.1.0"
version: "2.2.0"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: a4b5bc37fe1b368bbc81f953197d55e12f49d0296e7e412dfe2d2d77d6929958
sha256: "74083203a8eae241e0de4a0d597dbedab3b8fef5563f33cf3c12d7e93c655ca5"
url: "https://pub.dev"
source: hosted
version: "2.0.4"
version: "2.1.0"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "5eaf05ae77658d3521d0e993ede1af962d4b326cd2153d312df716dc250f00c9"
sha256: "5e588e2efef56916a3b229c3bfe81e6a525665a454519ca51dbcc4236a274173"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
version: "2.2.0"
shelf:
dependency: transitive
description:
@ -903,10 +903,10 @@ packages:
dependency: transitive
description:
name: source_gen
sha256: "2d79738b6bbf38a43920e2b8d189e9a3ce6cc201f4b8fc76be5e4fe377b1c38d"
sha256: c2bea18c95cfa0276a366270afaa2850b09b4a76db95d546f3d003dcc7011298
url: "https://pub.dev"
source: hosted
version: "1.2.6"
version: "1.2.7"
source_helper:
dependency: transitive
description:
@ -927,18 +927,18 @@ packages:
dependency: transitive
description:
name: sqflite
sha256: "78324387dc81df14f78df06019175a86a2ee0437624166c382e145d0a7fd9a4f"
sha256: "500d6fec583d2c021f2d25a056d96654f910662c64f836cd2063167b8f1fa758"
url: "https://pub.dev"
source: hosted
version: "2.2.4+1"
version: "2.2.6"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
sha256: bfd6973aaeeb93475bc0d875ac9aefddf7965ef22ce09790eb963992ffc5183f
sha256: "963dad8c4aa2f814ce7d2d5b1da2f36f31bd1a439d8f27e3dc189bb9d26bc684"
url: "https://pub.dev"
source: hosted
version: "2.4.2+2"
version: "2.4.3"
stack_trace:
dependency: transitive
description:
@ -1023,74 +1023,74 @@ packages:
dependency: transitive
description:
name: universal_io
sha256: "79f78ddad839ee3aae3ec7c01eb4575faf0d5c860f8e5223bc9f9c17f7f03cef"
sha256: "06866290206d196064fd61df4c7aea1ffe9a4e7c4ccaa8fcded42dd41948005d"
url: "https://pub.dev"
source: hosted
version: "2.0.4"
version: "2.2.0"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
sha256: "698fa0b4392effdc73e9e184403b627362eb5fbf904483ac9defbb1c2191d809"
sha256: "75f2846facd11168d007529d6cd8fcb2b750186bea046af9711f10b907e1587e"
url: "https://pub.dev"
source: hosted
version: "6.1.8"
version: "6.1.10"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "3e2f6dfd2c7d9cd123296cab8ef66cfc2c1a13f5845f42c7a0f365690a8a7dd1"
sha256: a52628068d282d01a07cd86e6ba99e497aa45ce8c91159015b2416907d78e411
url: "https://pub.dev"
source: hosted
version: "6.0.23"
version: "6.0.27"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: bb328b24d3bccc20bdf1024a0990ac4f869d57663660de9c936fb8c043edefe3
sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2"
url: "https://pub.dev"
source: hosted
version: "6.0.18"
version: "6.1.4"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: "318c42cba924e18180c029be69caf0a1a710191b9ec49bb42b5998fdcccee3cc"
sha256: "206fb8334a700ef7754d6a9ed119e7349bc830448098f21a69bf1b4ed038cabc"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
version: "3.0.4"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "41988b55570df53b3dd2a7fc90c76756a963de6a8c5f8e113330cb35992e2094"
sha256: "91ee3e75ea9dadf38036200c5d3743518f4a5eb77a8d13fda1ee5764373f185e"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
version: "3.0.5"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "4eae912628763eb48fc214522e58e942fd16ce195407dbf45638239523c759a6"
sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.1.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "44d79408ce9f07052095ef1f9a693c258d6373dc3944249374e30eff7219ccb0"
sha256: "81fe91b6c4f84f222d186a9d23c73157dc4c8e1c71489c4d08be1ad3b228f1aa"
url: "https://pub.dev"
source: hosted
version: "2.0.14"
version: "2.0.16"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: b6217370f8eb1fd85c8890c539f5a639a01ab209a36db82c921ebeacefc7a615
sha256: a83ba3607a507758669cfafb03f9de09bf6e6280c14d9b9cb18f013e406dcacd
url: "https://pub.dev"
source: hosted
version: "3.0.3"
version: "3.0.5"
uuid:
dependency: transitive
description:
@ -1107,6 +1107,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
video_player:
dependency: "direct main"
description:
name: video_player
sha256: de95f0e9405f29b5582573d4166132e71f83b3158aac14e8ee5767a54f4f1fbd
url: "https://pub.dev"
source: hosted
version: "2.6.1"
video_player_android:
dependency: transitive
description:
name: video_player_android
sha256: a592048a711d5739d9cea2255d425779f138d41095b9149bda60ce4bc1af8871
url: "https://pub.dev"
source: hosted
version: "2.4.4"
video_player_avfoundation:
dependency: transitive
description:
name: video_player_avfoundation
sha256: "75c6d68cd479a25f34d635149ba6887bc8f1b2b2921841121fd44ea0c5bc1927"
url: "https://pub.dev"
source: hosted
version: "2.4.4"
video_player_platform_interface:
dependency: transitive
description:
name: video_player_platform_interface
sha256: a8c4dcae2a7a6e7cc1d7f9808294d968eca1993af34a98e95b9bdfa959bec684
url: "https://pub.dev"
source: hosted
version: "6.1.0"
video_player_web:
dependency: transitive
description:
name: video_player_web
sha256: "44ce41424d104dfb7cf6982cc6b84af2b007a24d126406025bf40de5d481c74c"
url: "https://pub.dev"
source: hosted
version: "2.0.16"
wakelock:
dependency: "direct main"
description:
@ -1159,10 +1199,10 @@ packages:
dependency: "direct main"
description:
name: web_socket_channel
sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b
sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
url: "https://pub.dev"
source: hosted
version: "2.3.0"
version: "2.4.0"
webview_flutter:
dependency: "direct main"
description:
@ -1199,18 +1239,18 @@ packages:
dependency: transitive
description:
name: win32
sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46
sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4
url: "https://pub.dev"
source: hosted
version: "3.1.3"
version: "3.1.4"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86
sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1
url: "https://pub.dev"
source: hosted
version: "0.2.0+3"
version: "1.0.0"
xml:
dependency: transitive
description:
@ -1228,5 +1268,5 @@ packages:
source: hosted
version: "3.1.1"
sdks:
dart: ">=2.18.0 <4.0.0"
dart: ">=2.19.0 <3.0.0"
flutter: ">=3.3.0"

View file

@ -59,6 +59,7 @@ dependencies:
simple_pip_mode:
git:
url: https://github.com/tommyxchow/simple_pip_mode_flutter
video_player: ^2.6.1
dev_dependencies:
flutter_test: