diff --git a/lib/mpv/player/platform/player_android.dart b/lib/mpv/player/platform/player_android.dart index 3d0746b0..c24e4813 100644 --- a/lib/mpv/player/platform/player_android.dart +++ b/lib/mpv/player/platform/player_android.dart @@ -12,6 +12,9 @@ class PlayerAndroid extends PlayerBase { int? _bufferSizeBytes; bool _tunnelingEnabled = true; + /// Stored subtitle track ID when subtitles are hidden via sub-visibility. + String? _hiddenSubtitleTrackId; + @override MethodChannel get methodChannel => _methodChannel; @@ -193,6 +196,29 @@ class PlayerAndroid extends PlayerBase { case 'tunneled-playback': _tunnelingEnabled = value != 'no'; break; + case 'sub-visibility': + if (value == 'no') { + // Store current subtitle track and disable + final current = state.track.subtitle; + if (current != null && current.id != 'no') { + _hiddenSubtitleTrackId = current.id; + await selectSubtitleTrack(SubtitleTrack.off); + } + } else { + // Restore previously hidden subtitle track + final storedId = _hiddenSubtitleTrackId; + if (storedId != null) { + _hiddenSubtitleTrackId = null; + final track = state.tracks.subtitle.cast().firstWhere( + (t) => t?.id == storedId, + orElse: () => null, + ); + if (track != null) { + await selectSubtitleTrack(track); + } + } + } + break; // Other properties are no-ops for ExoPlayer } } diff --git a/lib/widgets/video_controls/desktop_video_controls.dart b/lib/widgets/video_controls/desktop_video_controls.dart index a76967a9..cded5763 100644 --- a/lib/widgets/video_controls/desktop_video_controls.dart +++ b/lib/widgets/video_controls/desktop_video_controls.dart @@ -96,6 +96,9 @@ class DesktopVideoControls extends StatefulWidget { /// Called to toggle ambient lighting (passed to settings sheet) final VoidCallback? onToggleAmbientLighting; + /// Whether subtitles are currently visible (false = hidden via sub-visibility toggle) + final bool subtitlesVisible; + /// Whether to show the queue button final bool showQueueButton; @@ -151,6 +154,7 @@ class DesktopVideoControls extends StatefulWidget { this.liveChannelName, this.isAmbientLightingEnabled = false, this.onToggleAmbientLighting, + this.subtitlesVisible = true, this.showQueueButton = false, this.onQueueItemSelected, }); @@ -697,6 +701,7 @@ class DesktopVideoControlsState extends State { onNavigateLeft: navigateFromTrackToVolume, canControl: widget.canControl, isLive: widget.isLive, + subtitlesVisible: widget.subtitlesVisible, showQueueButton: widget.showQueueButton, onQueueItemSelected: widget.onQueueItemSelected, shaderService: widget.shaderService, diff --git a/lib/widgets/video_controls/video_controls.dart b/lib/widgets/video_controls/video_controls.dart index 4fbff50c..a538dd02 100644 --- a/lib/widgets/video_controls/video_controls.dart +++ b/lib/widgets/video_controls/video_controls.dart @@ -256,6 +256,8 @@ class _PlexVideoControlsState extends State with WindowListen bool _showPerformanceOverlay = false; // Long-press 2x speed state bool _isLongPressing = false; + // Subtitle visibility toggle state + bool _subtitlesVisible = true; // Skip marker button focus node (for TV D-pad navigation) late final FocusNode _skipMarkerFocusNode; double? _rateBeforeLongPress; @@ -551,8 +553,28 @@ class _PlexVideoControlsState extends State with WindowListen } } - // ignore: no-empty-block - stub for future subtitle toggle implementation - void _toggleSubtitles() {} + void _toggleSubtitles() { + final currentTrack = widget.player.state.track.subtitle; + // No-op if no subtitle track is selected + if (currentTrack == null || currentTrack.id == 'no') return; + + final newVisible = !_subtitlesVisible; + widget.player.setProperty('sub-visibility', newVisible ? 'yes' : 'no'); + setState(() { + _subtitlesVisible = newVisible; + }); + } + + void _onSubtitleTrackChanged(SubtitleTrack track) { + // Reset visibility when user explicitly picks a new subtitle track + if (track.id != 'no' && !_subtitlesVisible) { + widget.player.setProperty('sub-visibility', 'yes'); + setState(() { + _subtitlesVisible = true; + }); + } + widget.onSubtitleTrackChanged?.call(track); + } void _toggleShader() { final shaderService = widget.shaderService; @@ -982,7 +1004,8 @@ class _PlexVideoControlsState extends State with WindowListen onToggleFullscreen: _toggleFullscreen, onSwitchVersion: _switchMediaVersion, onAudioTrackChanged: widget.onAudioTrackChanged, - onSubtitleTrackChanged: widget.onSubtitleTrackChanged, + onSubtitleTrackChanged: _onSubtitleTrackChanged, + subtitlesVisible: _subtitlesVisible, onLoadSeekTimes: () async { if (mounted) { await _loadSeekTimes(); @@ -2009,7 +2032,8 @@ class _PlexVideoControlsState extends State with WindowListen onToggleAlwaysOnTop: Platform.isMacOS ? null : _toggleAlwaysOnTop, onSwitchVersion: _switchMediaVersion, onAudioTrackChanged: widget.onAudioTrackChanged, - onSubtitleTrackChanged: widget.onSubtitleTrackChanged, + onSubtitleTrackChanged: _onSubtitleTrackChanged, + subtitlesVisible: _subtitlesVisible, onLoadSeekTimes: () async { if (mounted) { await _loadSeekTimes(); diff --git a/lib/widgets/video_controls/widgets/track_chapter_controls.dart b/lib/widgets/video_controls/widgets/track_chapter_controls.dart index 9cab3bee..d42bf3e1 100644 --- a/lib/widgets/video_controls/widgets/track_chapter_controls.dart +++ b/lib/widgets/video_controls/widgets/track_chapter_controls.dart @@ -73,6 +73,9 @@ class TrackChapterControls extends StatelessWidget { /// Whether this is a live TV stream (hides speed settings). final bool isLive; + /// Whether subtitles are currently visible (false = hidden via sub-visibility toggle) + final bool subtitlesVisible; + /// Whether to show the queue button final bool showQueueButton; @@ -110,6 +113,7 @@ class TrackChapterControls extends StatelessWidget { this.onNavigateLeft, this.canControl = true, this.isLive = false, + this.subtitlesVisible = true, this.showQueueButton = false, this.onQueueItemSelected, this.shaderService, @@ -267,10 +271,12 @@ class TrackChapterControls extends StatelessWidget { // Subtitles button if (_hasSubtitles(tracks)) { final currentIndex = buttonIndex; + final hasActiveSubtitle = tracks?.track.subtitle != null && tracks!.track.subtitle!.id != 'no'; + final isHidden = hasActiveSubtitle && !subtitlesVisible; buttons.add( _buildTrackButton( buttonIndex: currentIndex, - icon: Symbols.subtitles_rounded, + icon: isHidden ? Symbols.subtitles_off_rounded : Symbols.subtitles_rounded, tooltip: t.videoControls.subtitlesButton, semanticLabel: t.videoControls.subtitlesButton, tracks: tracks,