feat: video fill modes

This commit is contained in:
edde746
2025-11-05 11:00:22 +01:00
parent 36c6adb50c
commit 60ba1b1570
2 changed files with 121 additions and 15 deletions
+77 -15
View File
@@ -53,6 +53,10 @@ class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
StreamSubscription<PlayerLog>? _logSubscription;
StreamSubscription<String>? _errorSubscription;
// BoxFit mode state: 0=contain (letterbox), 1=cover (fill screen), 2=fill (stretch)
int _boxFitMode = 0;
bool _isPinching = false; // Track if a pinch gesture is occurring
@override
void initState() {
super.initState();
@@ -287,6 +291,38 @@ class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
}
}
/// Cycle through BoxFit modes: contain → cover → fill → contain (for button)
void _cycleBoxFitMode() {
setState(() {
_boxFitMode = (_boxFitMode + 1) % 3;
});
final modes = ['contain (letterbox)', 'cover (fill screen)', 'fill (stretch)'];
appLogger.d('BoxFit mode: ${modes[_boxFitMode]}');
}
/// Toggle between contain and cover modes only (for pinch gesture)
void _toggleContainCover() {
setState(() {
_boxFitMode = _boxFitMode == 0 ? 1 : 0; // Toggle between 0 and 1
});
final modes = ['contain (letterbox)', 'cover (fill screen)'];
appLogger.d('BoxFit mode toggled: ${modes[_boxFitMode]}');
}
/// Get current BoxFit based on mode
BoxFit get _getCurrentBoxFit {
switch (_boxFitMode) {
case 0:
return BoxFit.contain;
case 1:
return BoxFit.cover;
case 2:
return BoxFit.fill;
default:
return BoxFit.contain;
}
}
@override
void dispose() {
// Stop progress tracking
@@ -1022,22 +1058,47 @@ class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
},
child: Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
// Video player
Center(
child: Video(
controller: controller!,
controls: (state) => plexVideoControlsBuilder(
player!,
widget.metadata,
onNext: _nextEpisode != null ? _playNext : null,
onPrevious: _previousEpisode != null ? _playPrevious : null,
availableVersions: _availableVersions,
selectedMediaIndex: widget.selectedMediaIndex,
body: GestureDetector(
behavior: HitTestBehavior.translucent, // Allow taps to pass through to controls
onScaleStart: (details) {
// Initialize pinch gesture tracking (mobile only)
if (!PlatformDetector.isMobile(context)) return;
_isPinching = false;
},
onScaleUpdate: (details) {
// Track if this is a pinch gesture (2+ fingers) on mobile
if (!PlatformDetector.isMobile(context)) return;
if (details.pointerCount >= 2) {
_isPinching = true;
}
},
onScaleEnd: (details) {
// Only toggle if we detected a pinch gesture on mobile
if (!PlatformDetector.isMobile(context)) return;
if (_isPinching) {
_toggleContainCover();
_isPinching = false;
}
},
child: Stack(
children: [
// Video player
Center(
child: Video(
controller: controller!,
fit: _getCurrentBoxFit,
controls: (state) => plexVideoControlsBuilder(
player!,
widget.metadata,
onNext: _nextEpisode != null ? _playNext : null,
onPrevious: _previousEpisode != null ? _playPrevious : null,
availableVersions: _availableVersions,
selectedMediaIndex: widget.selectedMediaIndex,
boxFitMode: _boxFitMode,
onCycleBoxFitMode: _cycleBoxFitMode,
),
),
),
),
// Play Next Dialog
if (_showPlayNextDialog && _nextEpisode != null)
Positioned.fill(
@@ -1149,7 +1210,8 @@ class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
),
),
),
],
],
),
),
),
);
+44
View File
@@ -26,6 +26,8 @@ Widget plexVideoControlsBuilder(
VoidCallback? onPrevious,
List<PlexMediaVersion>? availableVersions,
int? selectedMediaIndex,
int boxFitMode = 0,
VoidCallback? onCycleBoxFitMode,
}) {
return PlexVideoControls(
player: player,
@@ -34,6 +36,8 @@ Widget plexVideoControlsBuilder(
onPrevious: onPrevious,
availableVersions: availableVersions ?? [],
selectedMediaIndex: selectedMediaIndex ?? 0,
boxFitMode: boxFitMode,
onCycleBoxFitMode: onCycleBoxFitMode,
);
}
@@ -44,6 +48,8 @@ class PlexVideoControls extends StatefulWidget {
final VoidCallback? onPrevious;
final List<PlexMediaVersion> availableVersions;
final int selectedMediaIndex;
final int boxFitMode;
final VoidCallback? onCycleBoxFitMode;
const PlexVideoControls({
super.key,
@@ -53,6 +59,8 @@ class PlexVideoControls extends StatefulWidget {
this.onPrevious,
this.availableVersions = const [],
this.selectedMediaIndex = 0,
this.boxFitMode = 0,
this.onCycleBoxFitMode,
});
@override
@@ -271,6 +279,32 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
return subtitles.isNotEmpty;
}
IconData _getBoxFitIcon(int mode) {
switch (mode) {
case 0:
return Icons.crop_free; // contain (letterbox)
case 1:
return Icons.crop; // cover (fill screen)
case 2:
return Icons.fit_screen; // fill (stretch)
default:
return Icons.crop_free;
}
}
String _getBoxFitTooltip(int mode) {
switch (mode) {
case 0:
return 'Letterbox';
case 1:
return 'Fill screen';
case 2:
return 'Stretch';
default:
return 'Letterbox';
}
}
Widget _buildTrackAndChapterControls() {
return StreamBuilder<Tracks>(
stream: widget.player.stream.tracks,
@@ -304,6 +338,16 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
icon: const Icon(Icons.video_file, color: Colors.white),
onPressed: _showVersionBottomSheet,
),
// BoxFit mode cycle button
if (widget.onCycleBoxFitMode != null)
IconButton(
icon: Icon(
_getBoxFitIcon(widget.boxFitMode),
color: Colors.white,
),
tooltip: _getBoxFitTooltip(widget.boxFitMode),
onPressed: widget.onCycleBoxFitMode,
),
],
);
},