diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 2dc3d0c5..375b5d84 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -22,8 +22,9 @@ + { androidNotificationOngoing: true, ), ); - BackButtonInterceptor.add(handleBackButton, name: 'miniPlayer', zIndex: 2); + BackButtonInterceptor.add(handleBackButton, name: 'miniPlayer', zIndex: 2, ifNotYetIntercepted: true); } else if (isTv && state.videos.isNotEmpty) { switchToVideo(state.videos[0]); } @@ -109,6 +109,11 @@ class PlayerCubit extends Cubit { playNext(); _setPlaying(false); break; + case MediaState.enteredPip: + _setPip(true); + break; + case MediaState.exitedPip: + _setPip(false); default: break; } @@ -127,6 +132,12 @@ class PlayerCubit extends Cubit { } } + _setPip(bool pip) { + var state = this.state.copyWith(); + state.isPip = pip; + emit(state); + } + @override close() async { BackButtonInterceptor.removeByName('miniPlayer'); diff --git a/lib/player/states/video_player.dart b/lib/player/states/video_player.dart index 4126b3dd..a34e5d4f 100644 --- a/lib/player/states/video_player.dart +++ b/lib/player/states/video_player.dart @@ -1,3 +1,4 @@ +import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'package:better_player/better_player.dart'; import 'package:better_player/src/video_player/video_player_platform_interface.dart'; import 'package:copy_with_extension/copy_with_extension.dart'; @@ -11,6 +12,7 @@ import 'package:invidious/settings/states/settings.dart'; import 'package:invidious/videos/models/base_video.dart'; import 'package:logging/logging.dart'; import 'package:pretty_bytes/pretty_bytes.dart'; +import 'package:simple_pip_mode/simple_pip.dart'; import 'package:wakelock/wakelock.dart'; import '../../globals.dart'; @@ -94,8 +96,10 @@ class VideoPlayerCubit extends MediaPlayerCubit { mediaState = MediaState.loading; break; case BetterPlayerEventType.pipStop: + mediaState = MediaState.exitedPip; break; case BetterPlayerEventType.pipStart: + mediaState = MediaState.enteredPip; break; case BetterPlayerEventType.overflowOpened: break; @@ -201,7 +205,6 @@ class VideoPlayerCubit extends MediaPlayerCubit { @override playVideo(bool offline, {Duration? startAt}) async { - bool wasFullscreen = this.state.videoController?.isFullScreen ?? false; var state = this.state.copyWith(); // only used if the player is currently close because it is onReady that will actually play the video // need better way of handling this @@ -300,7 +303,7 @@ class VideoPlayerCubit extends MediaPlayerCubit { subtitlesConfiguration: BetterPlayerSubtitlesConfiguration( fontSize: settings.state.subtitleSize, ), - controlsConfiguration: BetterPlayerControlsConfiguration( + controlsConfiguration: const BetterPlayerControlsConfiguration( showControls: false, // customControlsBuilder: (controller, onPlayerVisibilityChanged) => const PlayerControls(), // enablePlayPause: false, @@ -374,11 +377,18 @@ class VideoPlayerCubit extends MediaPlayerCubit { setFullScreen(bool fullScreen) { if (fullScreen) { state.videoController?.enterFullScreen(); + BackButtonInterceptor.add(backButtonInterceptor, zIndex: 10, name: 'full screen player'); } else { state.videoController?.exitFullScreen(); + BackButtonInterceptor.remove(backButtonInterceptor); } } + bool backButtonInterceptor(bool stopDefaultButtonEvent, RouteInfo info) { + setFullScreen(false); + return true; + } + String _videoTrackToString(BetterPlayerAsmsTrack? track) { return '${track?.height}p - ${prettyBytes((track?.bitrate ?? 0).toDouble(), bits: true)}/s'; } @@ -487,12 +497,20 @@ class VideoPlayerCubit extends MediaPlayerCubit { @override bool supportsPip() { - return isFullScreen() == FullScreenState.notFullScreen; + return true; } @override void enterPip() { - state.videoController?.enablePictureInPicture(state.key); + player.setEvent(MediaEvent(state: MediaState.enteredPip)); + setFullScreen(true); + SimplePip( + onPipExited: () { + player.setEvent(MediaEvent(state: MediaState.exitedPip)); + setFullScreen(false); + }, + ).enterPipMode(); + // state.videoController?.enablePictureInPicture(state.key); } @override diff --git a/lib/player/views/components/player_controls.dart b/lib/player/views/components/player_controls.dart index 4751ab6f..06a48826 100644 --- a/lib/player/views/components/player_controls.dart +++ b/lib/player/views/components/player_controls.dart @@ -181,6 +181,11 @@ class PlayerControls extends StatelessWidget { create: (context) => PlayerControlsCubit(PlayerControlsState(), player), child: BlocBuilder( builder: (context, _) { + bool isMini = context.select((PlayerCubit cubit) => cubit.state.isMini); + bool hasQueue = context.select((PlayerCubit cubit) => cubit.state.hasQueue); + bool isPip = context.select((PlayerCubit cubit) => cubit.state.isPip); + String videoTitle = context.select((PlayerCubit cubit) => cubit.state.currentlyPlaying?.title ?? cubit.state.offlineCurrentlyPlaying?.title ?? ''); + late MediaPlayerCubit pc; if (mediaPlayerCubit != null) { pc = mediaPlayerCubit!; @@ -189,7 +194,7 @@ class PlayerControls extends StatelessWidget { } else { pc = context.read(); } - PlayerState mpc = player.state; + // PlayerState mpc = player.state; var event = _.event; var cubit = context.read(); return BlocListener( @@ -204,7 +209,7 @@ class PlayerControls extends StatelessWidget { onVerticalDragUpdate: pc.isFullScreen() == FullScreenState.fullScreen ? null : player.videoDragged, onVerticalDragStart: pc.isFullScreen() == FullScreenState.fullScreen ? null : player.videoDragStarted, child: Padding( - padding: EdgeInsets.all(mpc.isMini ? 8 : 0.0), + padding: EdgeInsets.all(isMini ? 8 : 0.0), child: AspectRatio( aspectRatio: 16 / 9, child: Stack( @@ -215,16 +220,26 @@ class PlayerControls extends StatelessWidget { right: 0, bottom: 0, top: 0, - child: mpc.isMini + child: isMini || isPip ? const SizedBox.shrink() - : _.displayControls && !mpc.isMini + : _.displayControls ? Container( - decoration: BoxDecoration(borderRadius: BorderRadius.circular(0), color: Colors.black.withOpacity(0.6)), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(0), color: Colors.black.withOpacity(0.4)), child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.end, children: [ + if (pc.isFullScreen() == FullScreenState.fullScreen) + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text( + videoTitle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + )), if (pc.supportsPip()) IconButton(onPressed: pc.enterPip, icon: const Icon(Icons.picture_in_picture)), IconButton(onPressed: () => showOptionMenu(context, _, pc), icon: const Icon(Icons.more_vert)) ], @@ -273,7 +288,7 @@ class PlayerControls extends StatelessWidget { ) : const SizedBox.expand(), ), - if (_.displayControls) + if (!isMini && !isPip && _.displayControls) Positioned( top: 0, left: 0, @@ -283,7 +298,7 @@ class PlayerControls extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.center, children: [ - if (mpc.hasQueue) + if (hasQueue) IconButton( onPressed: () => player.playPrevious(), icon: const Icon( @@ -306,7 +321,7 @@ class PlayerControls extends StatelessWidget { Icons.fast_forward, size: 30, )), - if (mpc.hasQueue) + if (hasQueue) IconButton( onPressed: () => player.playNext(), icon: const Icon( diff --git a/lib/settings/views/components/manager_server_inner.dart b/lib/settings/views/components/manager_server_inner.dart index 436989c2..b3e70b48 100644 --- a/lib/settings/views/components/manager_server_inner.dart +++ b/lib/settings/views/components/manager_server_inner.dart @@ -5,6 +5,7 @@ import 'package:invidious/app/states/app.dart'; import 'package:invidious/main.dart'; import 'package:invidious/myRouteObserver.dart'; import 'package:invidious/settings/states/server_list_settings.dart'; +import 'package:invidious/settings/states/settings.dart'; import 'package:invidious/settings/views/screens/manage_single_server.dart'; import 'package:settings_ui/settings_ui.dart'; @@ -139,6 +140,7 @@ class ManagerServersView extends StatelessWidget { return BlocBuilder( builder: (ctx, _) { + SettingsCubit settings = context.watch(); ServerListSettingsCubit cubit = context.read(); var app = context.read(); var filteredPublicServers = _.publicServers.where((s) => _.dbServers.indexWhere((element) => element.url == s.url) == -1).toList(); @@ -148,6 +150,16 @@ class ManagerServersView extends StatelessWidget { lightTheme: theme, darkTheme: theme, sections: [ + SettingsSection( + tiles: [ + SettingsTile.switchTile( + title: Text(locals.skipSslVerification), + description: Text(locals.skipSslVerificationDescription), + initialValue: settings.state.skipSslVerification, + onToggle: settings.toggleSslVerification, + ) + ], + ), SettingsSection( title: Text(locals.yourServers), tiles: _.dbServers.isNotEmpty diff --git a/lib/settings/views/screens/settings.dart b/lib/settings/views/screens/settings.dart index 25ab2fcc..e2036cbb 100644 --- a/lib/settings/views/screens/settings.dart +++ b/lib/settings/views/screens/settings.dart @@ -198,12 +198,6 @@ class Settings extends StatelessWidget { description: BlocBuilder( buildWhen: (previous, current) => previous.server != current.server, builder: (context, app) => Text(app.server != null ? locals.currentServer(app.server!.url) : "")), onPressed: manageServers, - ), - SettingsTile.switchTile( - title: Text(locals.skipSslVerification), - description: Text(locals.skipSslVerificationDescription), - initialValue: _.skipSslVerification, - onToggle: cubit.toggleSslVerification, ) ]), SettingsSection(title: Text(locals.videoPlayer), tiles: [ diff --git a/lib/settings/views/tv/components/manage_server_inner.dart b/lib/settings/views/tv/components/manage_server_inner.dart index f98685e7..fdae3ead 100644 --- a/lib/settings/views/tv/components/manage_server_inner.dart +++ b/lib/settings/views/tv/components/manage_server_inner.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:invidious/settings/states/settings.dart'; import 'package:invidious/settings/views/tv/screens/manage_single_server.dart'; import 'package:invidious/utils.dart'; import 'package:invidious/utils/views/tv/components/tv_button.dart'; @@ -96,8 +97,15 @@ class TvManageServersInner extends StatelessWidget { AppLocalizations locals = AppLocalizations.of(context)!; return BlocBuilder(builder: (context, _) { var cubit = context.read(); + var settings = context.watch(); var filteredPublicServers = _.publicServers.where((s) => _.dbServers.indexWhere((element) => element.url == s.url) == -1).toList(); return ListView(children: [ + SettingsTile( + title: locals.skipSslVerification, + description: locals.skipSslVerification, + onSelected: (context) => settings.toggleSslVerification(!settings.state.skipSslVerification), + trailing: Switch(onChanged: (value) {}, value: settings.state.skipSslVerification), + ), SettingsTitle(title: locals.yourServers), ..._.dbServers.map((s) => SettingsTile( leading: InkWell( diff --git a/lib/settings/views/tv/screens/settings.dart b/lib/settings/views/tv/screens/settings.dart index 31be50c1..a19343e8 100644 --- a/lib/settings/views/tv/screens/settings.dart +++ b/lib/settings/views/tv/screens/settings.dart @@ -166,12 +166,6 @@ class TVSettings extends StatelessWidget { buildWhen: (previous, current) => previous.server != current.server, builder: (context, app) => SettingsTile(title: locals.manageServers, description: app.server != null ? locals.currentServer(db.getCurrentlySelectedServer().url) : "", onSelected: openManageServers)), - SettingsTile( - title: locals.skipSslVerification, - description: locals.skipSslVerification, - onSelected: (context) => cubit.toggleSslVerification(!_.skipSslVerification), - trailing: Switch(onChanged: (value) {}, value: _.skipSslVerification), - ), SettingsTitle(title: locals.videoPlayer), SettingsTile( title: locals.useDash, diff --git a/pubspec.lock b/pubspec.lock index fad2af8e..acf4e37b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1065,6 +1065,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + simple_pip_mode: + dependency: "direct main" + description: + name: simple_pip_mode + sha256: "89f8137fa5a8d113f39c61007d4b658048a9359362447b8cfb8bce93631882ad" + url: "https://pub.dev" + source: hosted + version: "0.8.0" sky_engine: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 0b04aa9f..bcee5bc1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.14.6+4022 +version: 1.14.7+4023 environment: sdk: '>=3.0.0 <4.0.0' @@ -83,6 +83,7 @@ dependencies: flutter_bloc: 8.1.3 copy_with_extension: 5.0.4 pretty_bytes: 6.1.0 + simple_pip_mode: ^0.8.0 dependency_overrides: # wakelock_windows: # git: # see https://github.com/creativecreatorormaybenot/wakelock/pull/203 for updates