Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:supportsPictureInPicture="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
Expand Down
1 change: 1 addition & 0 deletions ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@

<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>fetch</string>
</array>

Expand Down
16 changes: 16 additions & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -1186,6 +1186,22 @@
"@videoScalingFillScreenNotif": {},
"videoScalingFillScreenTitle": "Fill screen",
"@videoScalingFillScreenTitle": {},
"pictureInPictureTitle": "Picture-in-Picture",
"@pictureInPictureTitle": {
"description": "Tooltip on the in-player PiP button"
},
"pictureInPictureAutoTitle": "Auto Picture-in-Picture",
"@pictureInPictureAutoTitle": {
"description": "Title of the auto-enter Picture-in-Picture setting toggle"
},
"pictureInPictureSubtitle": "Automatically enter Picture-in-Picture when you leave the player. The PiP button in the controls always works regardless of this setting.",
"@pictureInPictureSubtitle": {
"description": "Subtitle of the auto-enter Picture-in-Picture setting toggle"
},
"pictureInPictureNotSupported": "Picture-in-Picture is not available on this device",
"@pictureInPictureNotSupported": {
"description": "Snackbar shown when the user taps the PiP button and the OS reports it unsupported"
},
"videoScalingFitHeight": "Fit Height",
"@videoScalingFitHeight": {},
"videoScalingFitWidth": "Fit Width",
Expand Down
3 changes: 2 additions & 1 deletion lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import 'package:fladder/util/deep_link_helper.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/themes_data.dart';
import 'package:fladder/widgets/media_query_scaler.dart';
import 'package:fladder/widgets/pip_lifecycle_controller.dart';

void main(List<String> args) async {
WidgetsFlutterBinding.ensureInitialized();
Expand Down Expand Up @@ -126,7 +127,7 @@ class _FladderApp extends ConsumerWidget {
},
builder: (context, child) => MediaQueryScaler(
child: LocalizationContextWrapper(
child: child ?? Container(),
child: PipLifecycleController(child: child ?? Container()),
currentLocale: language,
),
enable: ref.read(argumentsStateProvider).leanBackMode,
Expand Down
1 change: 1 addition & 0 deletions lib/models/settings/video_player_settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ abstract class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel {
@Default(false) bool enableAdvancedVideoOptions,
@Default(true) bool enableEdgeGestures,
@Default(false) bool reverseEdgeGestures,
@Default(true) bool enablePictureInPicture,
@Default(true) bool enableReplayGain,
@Default(ReplayGainVolumeLevel.quiet) ReplayGainVolumeLevel replayGainVolumeLevel,
@Default(true) bool enablePlayPauseFade,
Expand Down
75 changes: 57 additions & 18 deletions lib/models/settings/video_player_settings.freezed.dart

Large diffs are not rendered by default.

69 changes: 47 additions & 22 deletions lib/models/settings/video_player_settings.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions lib/providers/pip_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'package:fladder/wrappers/pip_manager.dart';

final pipManagerProvider = Provider<PipManager>((ref) {
final manager = PipManager();
ref.onDispose(manager.dispose);
return manager;
});

final pipStateProvider = StreamProvider<bool>((ref) {
final manager = ref.watch(pipManagerProvider);
return manager.isInPip;
});
2 changes: 2 additions & 0 deletions lib/providers/settings/video_player_settings_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,8 @@ class VideoPlayerSettingsProviderNotifier extends StateNotifier<VideoPlayerSetti

void setReverseEdgeGestures(bool value) => state = state.copyWith(reverseEdgeGestures: value);

void setEnablePictureInPicture(bool value) => state = state.copyWith(enablePictureInPicture: value);

void setEnableReplayGain(bool value) => state = state.copyWith(enableReplayGain: value);

void setEnablePlayPauseFade(bool value) => state = state.copyWith(enablePlayPauseFade: value);
Expand Down
11 changes: 11 additions & 0 deletions lib/screens/settings/player_settings_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import 'package:fladder/util/box_fit_extension.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/widgets/shared/fladder_slider.dart';
import 'package:fladder/widgets/shared/item_actions.dart';
import 'package:fladder/wrappers/pip_manager.dart';

@RoutePage()
class PlayerSettingsPage extends ConsumerStatefulWidget {
Expand Down Expand Up @@ -79,6 +80,16 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
),
],
),
if (pipPlatformSupported)
SettingsListTile(
label: Text(context.localized.pictureInPictureAutoTitle),
subLabel: Text(context.localized.pictureInPictureSubtitle),
onTap: () => provider.setEnablePictureInPicture(!videoSettings.enablePictureInPicture),
trailing: Switch(
value: videoSettings.enablePictureInPicture,
onChanged: (value) => provider.setEnablePictureInPicture(value),
),
),
SettingsListTileEnum(
label: Text(context.localized.videoScaling),
current: videoSettings.videoFit.label(context),
Expand Down
5 changes: 4 additions & 1 deletion lib/screens/video_player/video_player.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/media_playback_model.dart';
import 'package:fladder/models/playback/playback_model.dart';
import 'package:fladder/models/playback/tv_playback_model.dart';
import 'package:fladder/providers/pip_provider.dart';
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
import 'package:fladder/providers/video_player_provider.dart';
import 'package:fladder/screens/video_player/components/video_player_guide_wrapper.dart';
Expand Down Expand Up @@ -37,14 +38,16 @@ class _VideoPlayerState extends ConsumerState<VideoPlayer> with WidgetsBindingOb
void didChangeAppLifecycleState(AppLifecycleState state) {
//Don't pause on desktop focus loss
if (!(AdaptiveLayout.of(context).isDesktop || kIsWeb)) {
// Don't pause when entering PiP — playback must continue.
final inPip = ref.read(pipStateProvider).asData?.value ?? false;
switch (state) {
case AppLifecycleState.resumed:
if (playing) ref.read(videoPlayerProvider).play();
break;
case AppLifecycleState.hidden:
case AppLifecycleState.paused:
case AppLifecycleState.detached:
if (playing) ref.read(videoPlayerProvider).pause();
if (playing && !inPip) ref.read(videoPlayerProvider).pause();
break;
default:
break;
Expand Down
27 changes: 26 additions & 1 deletion lib/screens/video_player/video_player_controls.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import 'package:fladder/models/items/media_streams_model.dart';
import 'package:fladder/models/media_playback_model.dart';
import 'package:fladder/models/playback/playback_model.dart';
import 'package:fladder/models/settings/video_player_settings.dart';
import 'package:fladder/providers/pip_provider.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
import 'package:fladder/providers/user_provider.dart';
Expand All @@ -40,6 +41,7 @@ import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/string_extensions.dart';
import 'package:fladder/widgets/full_screen_helpers/full_screen_wrapper.dart';
import 'package:fladder/wrappers/pip_manager.dart';

class DesktopControls extends ConsumerStatefulWidget {
const DesktopControls({super.key});
Expand Down Expand Up @@ -96,8 +98,18 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {

@override
Widget build(BuildContext context) {
final mediaSegments = ref.watch(playBackModel.select((value) => value?.mediaSegments));
final isInPip = ref.watch(pipStateProvider).asData?.value ?? false;
final player = ref.watch(videoPlayerProvider);
if (isInPip) {
// Keep only the subtitle widget so it's captured in the PiP frame.
final pipSubtitleWidget = player.subtitleWidget(false, controlsKey: _bottomControlsKey);
return Stack(
children: [
if (pipSubtitleWidget != null) Positioned.fill(child: pipSubtitleWidget),
],
);
}
final mediaSegments = ref.watch(playBackModel.select((value) => value?.mediaSegments));
final subtitleWidget = player.subtitleWidget(showOverlay, controlsKey: _bottomControlsKey);
final isDesktop = AdaptiveLayout.of(context).isDesktop || kIsWeb;
final speedBoostEnabled = ref.watch(videoPlayerSettingsProvider.select((value) => value.enableSpeedBoost));
Expand Down Expand Up @@ -357,6 +369,19 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
IconButton(
onPressed: () => showVideoPlayerOptions(context, () => minimizePlayer(context)),
icon: const Icon(IconsaxPlusLinear.more)),
if (pipPlatformSupported && MediaQuery.orientationOf(context) == Orientation.landscape)
IconButton(
tooltip: context.localized.pictureInPictureTitle,
onPressed: () async {
final ok = await ref.read(pipManagerProvider).enter();
if (!ok && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.localized.pictureInPictureNotSupported)),
);
}
},
icon: const Icon(IconsaxPlusLinear.screenmirroring),
),
if (AdaptiveLayout.layoutOf(context) == ViewSize.tablet) ...[
IconButton(
onPressed: () => showSubSelection(context),
Expand Down
Loading
Loading