feat: video fill modes
This commit is contained in:
@@ -53,6 +53,10 @@ class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
|
|||||||
StreamSubscription<PlayerLog>? _logSubscription;
|
StreamSubscription<PlayerLog>? _logSubscription;
|
||||||
StreamSubscription<String>? _errorSubscription;
|
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
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
// Stop progress tracking
|
// Stop progress tracking
|
||||||
@@ -1022,22 +1058,47 @@ class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
|
|||||||
},
|
},
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
body: Stack(
|
body: GestureDetector(
|
||||||
children: [
|
behavior: HitTestBehavior.translucent, // Allow taps to pass through to controls
|
||||||
// Video player
|
onScaleStart: (details) {
|
||||||
Center(
|
// Initialize pinch gesture tracking (mobile only)
|
||||||
child: Video(
|
if (!PlatformDetector.isMobile(context)) return;
|
||||||
controller: controller!,
|
_isPinching = false;
|
||||||
controls: (state) => plexVideoControlsBuilder(
|
},
|
||||||
player!,
|
onScaleUpdate: (details) {
|
||||||
widget.metadata,
|
// Track if this is a pinch gesture (2+ fingers) on mobile
|
||||||
onNext: _nextEpisode != null ? _playNext : null,
|
if (!PlatformDetector.isMobile(context)) return;
|
||||||
onPrevious: _previousEpisode != null ? _playPrevious : null,
|
if (details.pointerCount >= 2) {
|
||||||
availableVersions: _availableVersions,
|
_isPinching = true;
|
||||||
selectedMediaIndex: widget.selectedMediaIndex,
|
}
|
||||||
|
},
|
||||||
|
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
|
// Play Next Dialog
|
||||||
if (_showPlayNextDialog && _nextEpisode != null)
|
if (_showPlayNextDialog && _nextEpisode != null)
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
@@ -1149,7 +1210,8 @@ class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ Widget plexVideoControlsBuilder(
|
|||||||
VoidCallback? onPrevious,
|
VoidCallback? onPrevious,
|
||||||
List<PlexMediaVersion>? availableVersions,
|
List<PlexMediaVersion>? availableVersions,
|
||||||
int? selectedMediaIndex,
|
int? selectedMediaIndex,
|
||||||
|
int boxFitMode = 0,
|
||||||
|
VoidCallback? onCycleBoxFitMode,
|
||||||
}) {
|
}) {
|
||||||
return PlexVideoControls(
|
return PlexVideoControls(
|
||||||
player: player,
|
player: player,
|
||||||
@@ -34,6 +36,8 @@ Widget plexVideoControlsBuilder(
|
|||||||
onPrevious: onPrevious,
|
onPrevious: onPrevious,
|
||||||
availableVersions: availableVersions ?? [],
|
availableVersions: availableVersions ?? [],
|
||||||
selectedMediaIndex: selectedMediaIndex ?? 0,
|
selectedMediaIndex: selectedMediaIndex ?? 0,
|
||||||
|
boxFitMode: boxFitMode,
|
||||||
|
onCycleBoxFitMode: onCycleBoxFitMode,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,6 +48,8 @@ class PlexVideoControls extends StatefulWidget {
|
|||||||
final VoidCallback? onPrevious;
|
final VoidCallback? onPrevious;
|
||||||
final List<PlexMediaVersion> availableVersions;
|
final List<PlexMediaVersion> availableVersions;
|
||||||
final int selectedMediaIndex;
|
final int selectedMediaIndex;
|
||||||
|
final int boxFitMode;
|
||||||
|
final VoidCallback? onCycleBoxFitMode;
|
||||||
|
|
||||||
const PlexVideoControls({
|
const PlexVideoControls({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -53,6 +59,8 @@ class PlexVideoControls extends StatefulWidget {
|
|||||||
this.onPrevious,
|
this.onPrevious,
|
||||||
this.availableVersions = const [],
|
this.availableVersions = const [],
|
||||||
this.selectedMediaIndex = 0,
|
this.selectedMediaIndex = 0,
|
||||||
|
this.boxFitMode = 0,
|
||||||
|
this.onCycleBoxFitMode,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -271,6 +279,32 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
|
|||||||
return subtitles.isNotEmpty;
|
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() {
|
Widget _buildTrackAndChapterControls() {
|
||||||
return StreamBuilder<Tracks>(
|
return StreamBuilder<Tracks>(
|
||||||
stream: widget.player.stream.tracks,
|
stream: widget.player.stream.tracks,
|
||||||
@@ -304,6 +338,16 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
|
|||||||
icon: const Icon(Icons.video_file, color: Colors.white),
|
icon: const Icon(Icons.video_file, color: Colors.white),
|
||||||
onPressed: _showVersionBottomSheet,
|
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,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user