Files
plezy/lib/widgets/video_controls/parts/navigation.dart
T

160 lines
6.8 KiB
Dart

part of '../video_controls.dart';
extension _PlexVideoControlsNavigationMethods on _PlexVideoControlsState {
Widget _buildDesktopControlsListener() {
final playbackState = context.watch<PlaybackStateProvider>();
final trackControlsState = _buildTrackControlsState(
playbackState: playbackState,
onToggleAlwaysOnTop: Platform.isMacOS ? null : _toggleAlwaysOnTop,
);
final useDpad = _videoPlayerNavigationEnabled || PlatformDetector.isTV();
return Listener(
behavior: HitTestBehavior.translucent,
onPointerDown: (_) => _restartHideTimerForCurrentPlaybackState(),
child: DesktopVideoControls(
key: _desktopControlsKey,
player: widget.player,
metadata: widget.metadata,
onNext: widget.onNext,
onPrevious: widget.onPrevious,
chapters: _chapters,
chaptersLoaded: _chaptersLoaded,
showChapterMarkersOnTimeline: _showChapterMarkersOnTimeline,
seekTimeSmall: _seekTimeSmall,
onSeekToPreviousChapter: _seekToPreviousChapter,
onSeekToNextChapter: _seekToNextChapter,
onSeekBackward: () => unawaited(_seekByTime(forward: false)),
onSeekForward: () => unawaited(_seekByTime(forward: true)),
onSeek: _throttledSeek,
onSeekEnd: _finalizeSeek,
onScrubStart: _holdTimelineScrub,
onScrubEnd: _releaseTimelineScrub,
onSeekRequested: widget.onSeekRequested,
getReplayIcon: getReplayIcon,
getForwardIcon: getForwardIcon,
onFocusActivity: _restartHideTimerForCurrentPlaybackState,
onHideControls: _hideControlsFromKeyboard,
trackControlsState: trackControlsState,
onBack: widget.onBack,
hasFirstFrame: widget.hasFirstFrame,
thumbnailDataBuilder: widget.thumbnailDataBuilder,
liveChannelName: widget.liveChannelName,
captureBuffer: widget.captureBuffer,
isAtLiveEdge: widget.isAtLiveEdge,
streamStartEpoch: widget.streamStartEpoch,
currentPositionEpoch: widget.currentPositionEpoch,
onLiveSeek: widget.onLiveSeek,
onLiveSeekBy: widget.onLiveSeekBy,
onJumpToLive: widget.onJumpToLive,
useDpadNavigation: useDpad,
serverId: widget.metadata.serverId,
showQueueTab: playbackState.isQueueActive,
onQueueItemSelected: playbackState.isQueueActive ? _onQueueItemSelected : null,
onCancelAutoHide: widget.chromeController.cancelAutoHide,
onStartAutoHide: _startHideTimer,
onSeekCompleted: widget.onSeekCompleted,
onContentStripVisibilityChanged: (visible) {
widget.chromeController.setContentStripVisible(visible);
},
chromeController: widget.chromeController,
),
);
}
void _onQueueItemSelected(MediaItem item) {
final videoPlayerState = context.findAncestorStateOfType<VideoPlayerScreenState>();
videoPlayerState?.navigateToQueueItem(item);
}
Future<SubtitleDownloadApplyOutcome> _onSubtitleDownloaded({
required String serverId,
required String ratingKey,
}) async {
if (!mounted) return SubtitleDownloadApplyOutcome.unavailable;
// Plex-only: the OpenSubtitles polling flow uses [getVideoPlaybackData]
// and the Plex token. Jellyfin has no analogue and the entry point
// (`subtitleSearchSupported`) is already gated on backend, but guard
// here too in case a future caller wires the same handler elsewhere.
if (widget.metadata.backend != MediaBackend.plex) return SubtitleDownloadApplyOutcome.unavailable;
if (widget.metadata.serverId != serverId || widget.metadata.id != ratingKey) {
return SubtitleDownloadApplyOutcome.superseded;
}
final switchSource = widget.onPlaybackSourceChanged;
if (switchSource == null) return SubtitleDownloadApplyOutcome.unavailable;
final itemKey = widget.metadata.globalKey;
bool targetIsCurrent() =>
mounted &&
widget.metadata.globalKey == itemKey &&
widget.metadata.serverId == serverId &&
widget.metadata.id == ratingKey;
try {
final client = context.getPlexClientForServer(ServerId(serverId));
// Plex's OpenSubtitles download is asynchronous: the PUT returns immediately
// but the new stream entry shows up in metadata seconds later. Poll until it
// appears. Up to 15s matches what Plex-web tolerates before giving up.
// Snapshot the authoritative source IDs so we can identify the new
// download without asking mpv to synchronously open its remote URL.
final existingSourceIds = widget.sourceSubtitleTracks.map((track) => track.id).toSet();
final deadline = DateTime.now().add(const Duration(seconds: 15));
MediaSubtitleTrack? newTrack;
while (mounted && DateTime.now().isBefore(deadline)) {
await Future.delayed(const Duration(seconds: 2));
if (!mounted) return SubtitleDownloadApplyOutcome.superseded;
try {
if (!targetIsCurrent()) return SubtitleDownloadApplyOutcome.superseded;
final data = await client.getVideoPlaybackData(ratingKey);
if (!targetIsCurrent()) return SubtitleDownloadApplyOutcome.superseded;
if (data.mediaInfo == null) continue;
newTrack = findNewExternalSubtitleTrack(data.mediaInfo!.subtitleTracks, existingSourceIds);
if (newTrack != null) break;
} catch (e) {
appLogger.w('Subtitle download poll iteration failed', error: e);
if (!targetIsCurrent()) return SubtitleDownloadApplyOutcome.superseded;
}
}
if (!targetIsCurrent()) return SubtitleDownloadApplyOutcome.superseded;
if (newTrack == null) return SubtitleDownloadApplyOutcome.timedOut;
final outcome = await switchSource(newSubtitleChoice: PlaybackSourceSubtitleChoice.source(newTrack.id));
return subtitleDownloadApplyOutcomeFor(outcome);
} catch (e) {
appLogger.w('Failed to refresh subtitles after download', error: e);
return SubtitleDownloadApplyOutcome.failed;
}
}
/// Request a version, quality preset, audio stream, or source subtitle reload.
/// The owning player screen decides how to apply it so controls do not own
/// player lifecycle/navigation policy.
Future<void> _switchVersionAndQuality({
int? newMediaIndex,
TranscodeQualityPreset? newPreset,
int? newAudioStreamId,
PlaybackSourceSubtitleChoice? newSubtitleChoice,
}) async {
final onPlaybackSourceChanged = widget.onPlaybackSourceChanged;
if (onPlaybackSourceChanged == null) return;
try {
await onPlaybackSourceChanged(
newMediaIndex: newMediaIndex,
newPreset: newPreset,
newAudioStreamId: newAudioStreamId,
newSubtitleChoice: newSubtitleChoice,
);
} catch (e) {
if (mounted) {
showErrorSnackBar(context, t.messages.errorLoading(error: e.toString()));
}
}
}
}