Add Picture-in-Picture support for Android video playback
Implemented native Picture-in-Picture (PiP) mode for Android by updating the AndroidManifest, MainActivity, and introducing PipService and VideoPIPManager. Updated video player screen and controls to support toggling PiP mode, and added localized strings for the PiP button in multiple languages. Relating to Issue #44: At this time PiP in Plezy only supports Android devices... iOS+MacOS will require significant changes to the video player for it to work properly...
This commit is contained in:
@@ -3,6 +3,10 @@
|
||||
<!-- Internet access permissions -->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
||||
<!-- PIP Permissions -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
|
||||
|
||||
<!-- Media session permissions for OS media controls -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
|
||||
@@ -28,6 +32,7 @@
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
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">
|
||||
|
||||
@@ -2,11 +2,18 @@ package com.edde746.plezy
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.app.PictureInPictureParams
|
||||
import android.content.res.Configuration
|
||||
import android.util.Rational
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import com.edde746.plezy.mpv.MpvPlayerPlugin
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
|
||||
private val PIP_CHANNEL = "app.plezy/pip"
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -20,5 +27,27 @@ class MainActivity : FlutterActivity() {
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
flutterEngine.plugins.add(MpvPlayerPlugin())
|
||||
|
||||
MethodChannel( flutterEngine.dartExecutor.binaryMessenger, PIP_CHANNEL ).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"isSupported" -> {
|
||||
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
}
|
||||
"enter" -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val width = call.argument<Int>("width") ?: 16
|
||||
val height = call.argument<Int>("height") ?: 9
|
||||
val params = PictureInPictureParams.Builder().setAspectRatio(Rational(width, height)).build()
|
||||
enterPictureInPictureMode(params)
|
||||
}
|
||||
result.success(null)
|
||||
} else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean,newConfig: Configuration) {
|
||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
||||
MethodChannel( flutterEngine!!.dartExecutor.binaryMessenger, PIP_CHANNEL ).invokeMethod( "onPipChanged" , isInPictureInPictureMode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -532,6 +532,7 @@ class _StringsVideoControlsEn {
|
||||
String get subtitlesButton => 'Subtitles';
|
||||
String get chaptersButton => 'Chapters';
|
||||
String get versionsButton => 'Video versions';
|
||||
String get pipButton => 'Picture-in-Picture mode';
|
||||
String get aspectRatioButton => 'Aspect ratio';
|
||||
String get fullscreenButton => 'Enter fullscreen';
|
||||
String get exitFullscreenButton => 'Exit fullscreen';
|
||||
@@ -1404,6 +1405,7 @@ class _StringsVideoControlsDe implements _StringsVideoControlsEn {
|
||||
@override String get subtitlesButton => 'Untertitel';
|
||||
@override String get chaptersButton => 'Kapitel';
|
||||
@override String get versionsButton => 'Videoversionen';
|
||||
@override String get pipButton => 'Bild-in-Bild Modus';
|
||||
@override String get aspectRatioButton => 'Seitenverhältnis';
|
||||
@override String get fullscreenButton => 'Vollbild aktivieren';
|
||||
@override String get exitFullscreenButton => 'Vollbild verlassen';
|
||||
@@ -2277,6 +2279,7 @@ class _StringsVideoControlsIt implements _StringsVideoControlsEn {
|
||||
@override String get subtitlesButton => 'Sottotitoli';
|
||||
@override String get chaptersButton => 'Capitoli';
|
||||
@override String get versionsButton => 'Versioni video';
|
||||
@override String get pipButton => 'Modalità Picture-in-Picture';
|
||||
@override String get aspectRatioButton => 'Proporzioni';
|
||||
@override String get fullscreenButton => 'Attiva schermo intero';
|
||||
@override String get exitFullscreenButton => 'Esci da schermo intero';
|
||||
@@ -3150,6 +3153,7 @@ class _StringsVideoControlsKo implements _StringsVideoControlsEn {
|
||||
@override String get subtitlesButton => '자막';
|
||||
@override String get chaptersButton => '챕터';
|
||||
@override String get versionsButton => '동영상 버전';
|
||||
@override String get pipButton => '픽처 인 픽처 모드';
|
||||
@override String get aspectRatioButton => '화면비율';
|
||||
@override String get fullscreenButton => '전체화면';
|
||||
@override String get exitFullscreenButton => '전체화면 종료';
|
||||
@@ -4023,6 +4027,7 @@ class _StringsVideoControlsNl implements _StringsVideoControlsEn {
|
||||
@override String get subtitlesButton => 'Ondertitels';
|
||||
@override String get chaptersButton => 'Hoofdstukken';
|
||||
@override String get versionsButton => 'Videoversies';
|
||||
@override String get pipButton => 'Beeld-in-beeld modus';
|
||||
@override String get aspectRatioButton => 'Beeldverhouding';
|
||||
@override String get fullscreenButton => 'Volledig scherm activeren';
|
||||
@override String get exitFullscreenButton => 'Volledig scherm verlaten';
|
||||
@@ -4896,6 +4901,7 @@ class _StringsVideoControlsSv implements _StringsVideoControlsEn {
|
||||
@override String get subtitlesButton => 'Undertexter';
|
||||
@override String get chaptersButton => 'Kapitel';
|
||||
@override String get versionsButton => 'Videoversioner';
|
||||
@override String get pipButton => 'Bild-i-bild läge';
|
||||
@override String get aspectRatioButton => 'Bildförhållande';
|
||||
@override String get fullscreenButton => 'Aktivera helskärm';
|
||||
@override String get exitFullscreenButton => 'Avsluta helskärm';
|
||||
@@ -5769,6 +5775,7 @@ class _StringsVideoControlsZh implements _StringsVideoControlsEn {
|
||||
@override String get subtitlesButton => '字幕';
|
||||
@override String get chaptersButton => '章节';
|
||||
@override String get versionsButton => '视频版本';
|
||||
@override String get pipButton => '画中画模式';
|
||||
@override String get aspectRatioButton => '宽高比';
|
||||
@override String get fullscreenButton => '进入全屏';
|
||||
@override String get exitFullscreenButton => '退出全屏';
|
||||
@@ -6465,6 +6472,7 @@ extension on Translations {
|
||||
case 'videoControls.subtitlesButton': return 'Subtitles';
|
||||
case 'videoControls.chaptersButton': return 'Chapters';
|
||||
case 'videoControls.versionsButton': return 'Video versions';
|
||||
case 'videoControls.pipButton': return 'Picture-in-Picture mode';
|
||||
case 'videoControls.aspectRatioButton': return 'Aspect ratio';
|
||||
case 'videoControls.fullscreenButton': return 'Enter fullscreen';
|
||||
case 'videoControls.exitFullscreenButton': return 'Exit fullscreen';
|
||||
@@ -6979,6 +6987,7 @@ extension on _StringsDe {
|
||||
case 'videoControls.subtitlesButton': return 'Untertitel';
|
||||
case 'videoControls.chaptersButton': return 'Kapitel';
|
||||
case 'videoControls.versionsButton': return 'Videoversionen';
|
||||
case 'videoControls.pipButton': return 'Bild-in-Bild Modus';
|
||||
case 'videoControls.aspectRatioButton': return 'Seitenverhältnis';
|
||||
case 'videoControls.fullscreenButton': return 'Vollbild aktivieren';
|
||||
case 'videoControls.exitFullscreenButton': return 'Vollbild verlassen';
|
||||
@@ -7494,6 +7503,7 @@ extension on _StringsIt {
|
||||
case 'videoControls.subtitlesButton': return 'Sottotitoli';
|
||||
case 'videoControls.chaptersButton': return 'Capitoli';
|
||||
case 'videoControls.versionsButton': return 'Versioni video';
|
||||
case 'videoControls.pipButton': return 'Modalità Picture-in-Picture';
|
||||
case 'videoControls.aspectRatioButton': return 'Proporzioni';
|
||||
case 'videoControls.fullscreenButton': return 'Attiva schermo intero';
|
||||
case 'videoControls.exitFullscreenButton': return 'Esci da schermo intero';
|
||||
@@ -8009,6 +8019,7 @@ extension on _StringsKo {
|
||||
case 'videoControls.subtitlesButton': return '자막';
|
||||
case 'videoControls.chaptersButton': return '챕터';
|
||||
case 'videoControls.versionsButton': return '동영상 버전';
|
||||
case 'videoControls.pipButton': return '픽처 인 픽처 모드';
|
||||
case 'videoControls.aspectRatioButton': return '화면비율';
|
||||
case 'videoControls.fullscreenButton': return '전체화면';
|
||||
case 'videoControls.exitFullscreenButton': return '전체화면 종료';
|
||||
@@ -8524,6 +8535,7 @@ extension on _StringsNl {
|
||||
case 'videoControls.subtitlesButton': return 'Ondertitels';
|
||||
case 'videoControls.chaptersButton': return 'Hoofdstukken';
|
||||
case 'videoControls.versionsButton': return 'Videoversies';
|
||||
case 'videoControls.pipButton': return 'Beeld-in-beeld modus';
|
||||
case 'videoControls.aspectRatioButton': return 'Beeldverhouding';
|
||||
case 'videoControls.fullscreenButton': return 'Volledig scherm activeren';
|
||||
case 'videoControls.exitFullscreenButton': return 'Volledig scherm verlaten';
|
||||
@@ -9039,6 +9051,7 @@ extension on _StringsSv {
|
||||
case 'videoControls.subtitlesButton': return 'Undertexter';
|
||||
case 'videoControls.chaptersButton': return 'Kapitel';
|
||||
case 'videoControls.versionsButton': return 'Videoversioner';
|
||||
case 'videoControls.pipButton': return 'Bild-i-bild läge';
|
||||
case 'videoControls.aspectRatioButton': return 'Bildförhållande';
|
||||
case 'videoControls.fullscreenButton': return 'Aktivera helskärm';
|
||||
case 'videoControls.exitFullscreenButton': return 'Avsluta helskärm';
|
||||
@@ -9554,6 +9567,7 @@ extension on _StringsZh {
|
||||
case 'videoControls.subtitlesButton': return '字幕';
|
||||
case 'videoControls.chaptersButton': return '章节';
|
||||
case 'videoControls.versionsButton': return '视频版本';
|
||||
case 'videoControls.pipButton': return '画中画模式';
|
||||
case 'videoControls.aspectRatioButton': return '宽高比';
|
||||
case 'videoControls.fullscreenButton': return '进入全屏';
|
||||
case 'videoControls.exitFullscreenButton': return '退出全屏';
|
||||
|
||||
@@ -29,6 +29,7 @@ import '../services/offline_watch_sync_service.dart';
|
||||
import '../services/settings_service.dart';
|
||||
import '../services/track_selection_service.dart';
|
||||
import '../services/video_filter_manager.dart';
|
||||
import '../services/video_pip_manager.dart';
|
||||
import '../providers/user_profile_provider.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import '../utils/orientation_helper.dart';
|
||||
@@ -104,6 +105,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
MediaControlsManager? _mediaControlsManager;
|
||||
PlaybackProgressTracker? _progressTracker;
|
||||
VideoFilterManager? _videoFilterManager;
|
||||
VideoPIPManager? _videoPIPManager;
|
||||
final EpisodeNavigationService _episodeNavigation = EpisodeNavigationService();
|
||||
|
||||
// Watch Together provider reference (stored early to use in dispose)
|
||||
@@ -714,7 +716,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
_currentMediaInfo = result.mediaInfo;
|
||||
});
|
||||
|
||||
// Initialize video filter manager with player and available versions
|
||||
// Initialize video PIP and filter manager with player and available versions
|
||||
if (player != null) {
|
||||
_videoFilterManager = VideoFilterManager(
|
||||
player: player!,
|
||||
@@ -723,6 +725,11 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
);
|
||||
// Update video filter once dimensions are available
|
||||
_videoFilterManager!.updateVideoFilter();
|
||||
|
||||
// PIP Manager
|
||||
_videoPIPManager = VideoPIPManager(
|
||||
player: player!
|
||||
);
|
||||
}
|
||||
|
||||
// Add external subtitles to the player
|
||||
@@ -782,6 +789,12 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
);
|
||||
}
|
||||
|
||||
void _togglePIPMode() {
|
||||
setState(() {
|
||||
_videoPIPManager?.togglePIP();
|
||||
});
|
||||
}
|
||||
|
||||
/// Cycle through BoxFit modes: contain → cover → fill → contain (for button)
|
||||
void _cycleBoxFitMode() {
|
||||
setState(() {
|
||||
@@ -1519,6 +1532,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
||||
onPrevious: (_previousEpisode != null && _canNavigateEpisodes()) ? _playPrevious : null,
|
||||
availableVersions: _availableVersions,
|
||||
selectedMediaIndex: widget.selectedMediaIndex,
|
||||
onTogglePIPMode: _togglePIPMode,
|
||||
boxFitMode: _videoFilterManager?.boxFitMode ?? 0,
|
||||
onCycleBoxFitMode: _cycleBoxFitMode,
|
||||
onAudioTrackChanged: _onAudioTrackChanged,
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class PipService {
|
||||
static const MethodChannel _channel =
|
||||
MethodChannel('app.plezy/pip');
|
||||
|
||||
static Future<bool> isSupported() async {
|
||||
return await _channel.invokeMethod<bool>('isSupported') ?? false;
|
||||
}
|
||||
|
||||
static Future<void> enter({
|
||||
int? width,
|
||||
int? height,
|
||||
}) async {
|
||||
await _channel.invokeMethod('enter', {
|
||||
'width': width,
|
||||
'height': height,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../mpv/mpv.dart';
|
||||
import '../services/pip_service.dart';
|
||||
|
||||
/// Manages video Picture-in-Picture mode
|
||||
|
||||
class VideoPIPManager {
|
||||
final Player player;
|
||||
|
||||
bool isPIPactive = false;
|
||||
|
||||
VideoPIPManager({required this.player});
|
||||
|
||||
Size? _playerSize;
|
||||
Size? get playerSize => _playerSize;
|
||||
|
||||
/// Toggle native PiP
|
||||
Future<void> togglePIP() async {
|
||||
final supported = await PipService.isSupported();
|
||||
if (!supported) return;
|
||||
|
||||
await PipService.enter(
|
||||
width: _playerSize?.width.toInt(),
|
||||
height: _playerSize?.height.toInt(),
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,7 @@ class DesktopVideoControls extends StatefulWidget {
|
||||
final int subtitleSyncOffset;
|
||||
final bool isFullscreen;
|
||||
final bool isAlwaysOnTop;
|
||||
final VoidCallback? onTogglePIPMode;
|
||||
final VoidCallback? onCycleBoxFitMode;
|
||||
final VoidCallback? onToggleFullscreen;
|
||||
final VoidCallback? onToggleAlwaysOnTop;
|
||||
@@ -95,6 +96,7 @@ class DesktopVideoControls extends StatefulWidget {
|
||||
this.subtitleSyncOffset = 0,
|
||||
this.isFullscreen = false,
|
||||
this.isAlwaysOnTop = false,
|
||||
this.onTogglePIPMode,
|
||||
this.onCycleBoxFitMode,
|
||||
this.onToggleFullscreen,
|
||||
this.onToggleAlwaysOnTop,
|
||||
@@ -524,6 +526,7 @@ class DesktopVideoControlsState extends State<DesktopVideoControls> {
|
||||
isFullscreen: widget.isFullscreen,
|
||||
isAlwaysOnTop: widget.isAlwaysOnTop,
|
||||
serverId: widget.serverId,
|
||||
onTogglePIPMode: null, // PIP not supported on desktop
|
||||
onCycleBoxFitMode: widget.onCycleBoxFitMode,
|
||||
onToggleFullscreen: widget.onToggleFullscreen,
|
||||
onToggleAlwaysOnTop: widget.onToggleAlwaysOnTop,
|
||||
|
||||
@@ -15,6 +15,7 @@ import 'package:flutter/services.dart'
|
||||
KeyDownEvent,
|
||||
HardwareKeyboard;
|
||||
import '../../services/macos_window_service.dart';
|
||||
import '../../services/pip_service.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import '../../mpv/mpv.dart';
|
||||
@@ -51,6 +52,7 @@ Widget plexVideoControlsBuilder(
|
||||
VoidCallback? onPrevious,
|
||||
List<PlexMediaVersion>? availableVersions,
|
||||
int? selectedMediaIndex,
|
||||
VoidCallback? onTogglePIPMode,
|
||||
int boxFitMode = 0,
|
||||
VoidCallback? onCycleBoxFitMode,
|
||||
Function(AudioTrack)? onAudioTrackChanged,
|
||||
@@ -68,6 +70,7 @@ Widget plexVideoControlsBuilder(
|
||||
availableVersions: availableVersions ?? [],
|
||||
selectedMediaIndex: selectedMediaIndex ?? 0,
|
||||
boxFitMode: boxFitMode,
|
||||
onTogglePIPMode: onTogglePIPMode,
|
||||
onCycleBoxFitMode: onCycleBoxFitMode,
|
||||
onAudioTrackChanged: onAudioTrackChanged,
|
||||
onSubtitleTrackChanged: onSubtitleTrackChanged,
|
||||
@@ -86,6 +89,7 @@ class PlexVideoControls extends StatefulWidget {
|
||||
final List<PlexMediaVersion> availableVersions;
|
||||
final int selectedMediaIndex;
|
||||
final int boxFitMode;
|
||||
final VoidCallback? onTogglePIPMode;
|
||||
final VoidCallback? onCycleBoxFitMode;
|
||||
final Function(AudioTrack)? onAudioTrackChanged;
|
||||
final Function(SubtitleTrack)? onSubtitleTrackChanged;
|
||||
@@ -111,6 +115,7 @@ class PlexVideoControls extends StatefulWidget {
|
||||
this.availableVersions = const [],
|
||||
this.selectedMediaIndex = 0,
|
||||
this.boxFitMode = 0,
|
||||
this.onTogglePIPMode,
|
||||
this.onCycleBoxFitMode,
|
||||
this.onAudioTrackChanged,
|
||||
this.onSubtitleTrackChanged,
|
||||
@@ -186,6 +191,9 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
|
||||
double? _rateBeforeLongPress;
|
||||
bool _showSpeedIndicator = false;
|
||||
|
||||
// PiP support
|
||||
bool _isPipSupported = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -203,6 +211,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
|
||||
_listenToPosition();
|
||||
_listenToPlayingState();
|
||||
_listenToCompleted();
|
||||
_checkPipSupport();
|
||||
// Add lifecycle observer to reload settings when app resumes
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
// Add window listener for tracking fullscreen state (for button icon)
|
||||
@@ -210,6 +219,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
|
||||
windowManager.addListener(this);
|
||||
_initAlwaysOnTopState();
|
||||
}
|
||||
|
||||
// Focus play/pause button on first frame if in keyboard mode
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_focusPlayPauseIfKeyboardMode();
|
||||
@@ -636,6 +646,24 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
|
||||
await MacOSWindowService.setTrafficLightsVisible(_showControls);
|
||||
}
|
||||
|
||||
/// Check whether PiP is supported on this device
|
||||
Future<void> _checkPipSupport() async {
|
||||
if (!Platform.isAndroid) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final supported = await PipService.isSupported();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isPipSupported = supported;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadPlaybackExtras() async {
|
||||
try {
|
||||
appLogger.d('_loadPlaybackExtras: starting for ${widget.metadata.ratingKey}');
|
||||
@@ -731,6 +759,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
|
||||
subtitleSyncOffset: _subtitleSyncOffset,
|
||||
isRotationLocked: _isRotationLocked,
|
||||
isFullscreen: _isFullscreen,
|
||||
onTogglePIPMode: (_isPipSupported && Platform.isAndroid) ? widget.onTogglePIPMode : null,
|
||||
onCycleBoxFitMode: widget.onCycleBoxFitMode,
|
||||
onToggleRotationLock: _toggleRotationLock,
|
||||
onToggleFullscreen: _toggleFullscreen,
|
||||
@@ -1498,6 +1527,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
|
||||
subtitleSyncOffset: _subtitleSyncOffset,
|
||||
isFullscreen: _isFullscreen,
|
||||
isAlwaysOnTop: _isAlwaysOnTop,
|
||||
onTogglePIPMode: (_isPipSupported && Platform.isAndroid) ? widget.onTogglePIPMode : null,
|
||||
onCycleBoxFitMode: widget.onCycleBoxFitMode,
|
||||
onToggleFullscreen: _toggleFullscreen,
|
||||
onToggleAlwaysOnTop: _toggleAlwaysOnTop,
|
||||
|
||||
@@ -32,6 +32,7 @@ class TrackChapterControls extends StatelessWidget {
|
||||
final bool isRotationLocked;
|
||||
final bool isFullscreen;
|
||||
final bool isAlwaysOnTop;
|
||||
final VoidCallback? onTogglePIPMode;
|
||||
final VoidCallback? onCycleBoxFitMode;
|
||||
final VoidCallback? onToggleRotationLock;
|
||||
final VoidCallback? onToggleFullscreen;
|
||||
@@ -70,6 +71,7 @@ class TrackChapterControls extends StatelessWidget {
|
||||
required this.isFullscreen,
|
||||
required this.serverId,
|
||||
this.isAlwaysOnTop = false,
|
||||
this.onTogglePIPMode,
|
||||
this.onCycleBoxFitMode,
|
||||
this.onToggleRotationLock,
|
||||
this.onToggleFullscreen,
|
||||
@@ -288,6 +290,24 @@ class TrackChapterControls extends StatelessWidget {
|
||||
buttonIndex++;
|
||||
}
|
||||
|
||||
// Picture-in-Picture mode
|
||||
if (onTogglePIPMode != null) {
|
||||
final currentIndex = buttonIndex;
|
||||
buttons.add(
|
||||
_buildTrackButton(
|
||||
buttonIndex: currentIndex,
|
||||
icon: Symbols.picture_in_picture_alt,
|
||||
tooltip: t.videoControls.pipButton,
|
||||
semanticLabel: t.videoControls.pipButton,
|
||||
tracks: tracks,
|
||||
isMobile: isMobile,
|
||||
isDesktop: isDesktop,
|
||||
onPressed: onTogglePIPMode,
|
||||
),
|
||||
);
|
||||
buttonIndex++;
|
||||
}
|
||||
|
||||
// BoxFit mode button
|
||||
if (onCycleBoxFitMode != null) {
|
||||
final currentIndex = buttonIndex;
|
||||
@@ -372,6 +392,7 @@ class TrackChapterControls extends StatelessWidget {
|
||||
if (_hasSubtitles(tracks)) count++;
|
||||
if (chapters.isNotEmpty) count++;
|
||||
if (availableVersions.length > 1 && onSwitchVersion != null) count++;
|
||||
if (onTogglePIPMode != null) count++;
|
||||
if (onCycleBoxFitMode != null) count++;
|
||||
if (isMobile && !PlatformDetector.isTV()) count++; // Rotation lock (not on TV)
|
||||
if (isDesktop) count++; // Fullscreen
|
||||
|
||||
Reference in New Issue
Block a user