From 653fada5dcf669b8f96f55d6a09ab04d6c4a3cf9 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Fri, 8 May 2026 06:24:59 +0200 Subject: [PATCH] refactor(player): move quality picker into playback settings --- .../sheets/version_quality_sheet.dart | 32 ++++---- .../sheets/video_settings_sheet.dart | 76 ++++++++++++++++++- .../widgets/track_chapter_controls.dart | 57 ++------------ 3 files changed, 95 insertions(+), 70 deletions(-) diff --git a/lib/widgets/video_controls/sheets/version_quality_sheet.dart b/lib/widgets/video_controls/sheets/version_quality_sheet.dart index 4d6bf77f..8f48a615 100644 --- a/lib/widgets/video_controls/sheets/version_quality_sheet.dart +++ b/lib/widgets/video_controls/sheets/version_quality_sheet.dart @@ -9,15 +9,19 @@ import '../../../utils/quality_preset_labels.dart'; import '../../../utils/scroll_utils.dart'; import '../../../widgets/focusable_list_tile.dart'; import '../../../widgets/overlay_sheet.dart'; -import 'base_video_control_sheet.dart'; import 'sheet_column_header.dart'; -/// Combined sheet for selecting the media [version] (left) and transcode +String versionQualityPickerTitle({required bool showVersions, required bool showQuality}) { + return showQuality + ? (showVersions ? t.videoControls.versionQualityButton : t.videoControls.qualityColumnHeader) + : t.videoControls.versionColumnHeader; +} + +/// Combined picker for selecting the media [version] (left) and transcode /// [quality] preset (right). The version column is hidden when there is only /// one version so the quality list gets the full width. If the server doesn't -/// support video transcoding, only [TranscodeQualityPreset.original] is -/// enabled in the quality column. -class VersionQualitySheet extends StatelessWidget { +/// support video transcoding, the quality column is hidden entirely. +class VersionQualityPicker extends StatelessWidget { final List availableVersions; final int selectedMediaIndex; final TranscodeQualityPreset selectedQualityPreset; @@ -26,7 +30,7 @@ class VersionQualitySheet extends StatelessWidget { final ValueChanged onVersionSelected; final ValueChanged onQualitySelected; - const VersionQualitySheet({ + const VersionQualityPicker({ super.key, required this.availableVersions, required this.selectedMediaIndex, @@ -40,14 +44,7 @@ class VersionQualitySheet extends StatelessWidget { @override Widget build(BuildContext context) { final showVersions = availableVersions.length > 1; - // Quality presets only do something useful when the server can transcode - // — otherwise non-Original options are disabled and the column degenerates - // into a single tappable row. Hide it entirely in that case so the - // versions list (when present) gets the full sheet. final showQuality = serverSupportsTranscoding; - final title = showQuality - ? (showVersions ? t.videoControls.versionQualityButton : t.videoControls.qualityColumnHeader) - : t.videoControls.versionColumnHeader; final qualityColumn = FocusTraversalGroup( child: _QualityColumn( @@ -76,9 +73,8 @@ class VersionQualitySheet extends StatelessWidget { ), ); - final Widget body; if (showVersions && showQuality) { - body = Row( + return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: versionColumn), @@ -87,12 +83,10 @@ class VersionQualitySheet extends StatelessWidget { ], ); } else if (showVersions) { - body = versionColumn; + return versionColumn; } else { - body = qualityColumn; + return qualityColumn; } - - return BaseVideoControlSheet(title: title, icon: Symbols.video_settings_rounded, child: body); } int? _sourceBitrateKbps() { diff --git a/lib/widgets/video_controls/sheets/video_settings_sheet.dart b/lib/widgets/video_controls/sheets/video_settings_sheet.dart index 1c93d6b7..7b3fae3e 100644 --- a/lib/widgets/video_controls/sheets/video_settings_sheet.dart +++ b/lib/widgets/video_controls/sheets/video_settings_sheet.dart @@ -12,6 +12,8 @@ import 'package:path/path.dart' as path; import 'package:provider/provider.dart'; import '../../../models/shader_preset.dart'; +import '../../../models/transcode_quality_preset.dart'; +import '../../../media/media_version.dart'; import '../../../mpv/mpv.dart'; import '../../../providers/shader_provider.dart'; import '../../../services/file_picker_service.dart'; @@ -22,6 +24,7 @@ import '../../../focus/focusable_wrapper.dart'; import '../../../utils/dialogs.dart'; import '../../../utils/formatters.dart'; import '../../../utils/platform_detector.dart'; +import '../../../utils/quality_preset_labels.dart'; import '../../../utils/snackbar_helper.dart'; import '../../../theme/mono_tokens.dart'; import '../../../widgets/focusable_list_tile.dart'; @@ -30,8 +33,9 @@ import '../widgets/sync_offset_control.dart'; import '../widgets/sleep_timer_content.dart'; import '../../../i18n/strings.g.dart'; import 'base_video_control_sheet.dart'; +import 'version_quality_sheet.dart'; -enum _SettingsView { menu, speed, sleep, audioSync, subtitleSync, audioDevice, shader, dvConversion } +enum _SettingsView { menu, speed, versionQuality, sleep, audioSync, subtitleSync, audioDevice, shader, dvConversion } class _SettingsMenuItem extends StatelessWidget { final IconData icon; @@ -118,6 +122,15 @@ class VideoSettingsSheet extends StatefulWidget { /// Whether this is a live TV stream (hides speed settings). final bool isLive; + /// Available media versions and quality controls shown inside playback settings. + final List availableVersions; + final int selectedMediaIndex; + final TranscodeQualityPreset selectedQualityPreset; + final bool serverSupportsTranscoding; + final int? sourceDurationMs; + final ValueChanged? onVersionSelected; + final ValueChanged? onQualitySelected; + /// Optional shader service for MPV shader control final ShaderService? shaderService; @@ -146,6 +159,13 @@ class VideoSettingsSheet extends StatefulWidget { required this.subtitleSyncOffset, this.canControl = true, this.isLive = false, + this.availableVersions = const [], + this.selectedMediaIndex = 0, + this.selectedQualityPreset = TranscodeQualityPreset.original, + this.serverSupportsTranscoding = false, + this.sourceDurationMs, + this.onVersionSelected, + this.onQualitySelected, this.shaderService, this.onShaderChanged, this.isAmbientLightingEnabled = false, @@ -266,6 +286,8 @@ class _VideoSettingsSheetState extends State { return t.videoSettings.playbackSettings; case _SettingsView.speed: return t.videoSettings.playbackSpeed; + case _SettingsView.versionQuality: + return _versionQualityTitle(); case _SettingsView.sleep: return t.videoSettings.sleepTimer; case _SettingsView.audioSync: @@ -287,6 +309,8 @@ class _VideoSettingsSheetState extends State { return Symbols.tune_rounded; case _SettingsView.speed: return Symbols.speed_rounded; + case _SettingsView.versionQuality: + return Symbols.art_track; case _SettingsView.sleep: return Symbols.bedtime_rounded; case _SettingsView.audioSync: @@ -327,6 +351,33 @@ class _VideoSettingsSheetState extends State { return 'Active (${formatDurationWithSeconds(remaining)})'; } + bool get _hasVersionQuality { + return (widget.availableVersions.length > 1 || widget.serverSupportsTranscoding) && + (widget.onVersionSelected != null || widget.onQualitySelected != null); + } + + String _versionQualityTitle() { + return versionQualityPickerTitle( + showVersions: widget.availableVersions.length > 1, + showQuality: widget.serverSupportsTranscoding, + ); + } + + String _versionQualityValueText() { + final values = []; + if (widget.availableVersions.length > 1) values.add(_selectedVersionLabel()); + if (widget.serverSupportsTranscoding) values.add(qualityPresetLabel(widget.selectedQualityPreset)); + return values.join(' / '); + } + + String _selectedVersionLabel() { + final index = widget.selectedMediaIndex; + if (index >= 0 && index < widget.availableVersions.length) { + return widget.availableVersions[index].displayLabel; + } + return t.videoControls.versionColumnHeader; + } + Widget _buildMenuView() { final sleepTimer = SleepTimerService(); final isDesktop = PlatformDetector.isDesktop(context); @@ -349,6 +400,15 @@ class _VideoSettingsSheetState extends State { }, ), + if (_hasVersionQuality) + _SettingsMenuItem( + icon: Symbols.art_track, + title: _versionQualityTitle(), + valueText: _versionQualityValueText(), + allowValueOverflow: true, + onTap: () => _navigateTo(_SettingsView.versionQuality), + ), + // Sleep Timer ListenableBuilder( listenable: sleepTimer, @@ -577,6 +637,18 @@ class _VideoSettingsSheetState extends State { ); } + Widget _buildVersionQualityView() { + return VersionQualityPicker( + availableVersions: widget.availableVersions, + selectedMediaIndex: widget.selectedMediaIndex, + selectedQualityPreset: widget.selectedQualityPreset, + serverSupportsTranscoding: widget.serverSupportsTranscoding, + sourceDurationMs: widget.sourceDurationMs, + onVersionSelected: (index) => widget.onVersionSelected?.call(index), + onQualitySelected: (preset) => widget.onQualitySelected?.call(preset), + ); + } + /// Extract the audio backend name from a device name (e.g. "coreaudio" from "coreaudio/BuiltIn"). static String _audioBackend(String name) { final slash = name.indexOf('/'); @@ -833,6 +905,8 @@ class _VideoSettingsSheetState extends State { return _buildMenuView(); case _SettingsView.speed: return _buildSpeedView(); + case _SettingsView.versionQuality: + return _buildVersionQualityView(); case _SettingsView.sleep: return _buildSleepView(); case _SettingsView.audioSync: diff --git a/lib/widgets/video_controls/widgets/track_chapter_controls.dart b/lib/widgets/video_controls/widgets/track_chapter_controls.dart index b07a5c73..c65fa678 100644 --- a/lib/widgets/video_controls/widgets/track_chapter_controls.dart +++ b/lib/widgets/video_controls/widgets/track_chapter_controls.dart @@ -16,7 +16,6 @@ import '../../../models/transcode_quality_preset.dart'; import '../sheets/chapter_sheet.dart'; import '../sheets/queue_sheet.dart'; import '../sheets/track_sheet.dart'; -import '../sheets/version_quality_sheet.dart'; import '../sheets/video_settings_sheet.dart'; import '../../../services/shader_service.dart'; import '../helpers/track_filter_helper.dart'; @@ -217,6 +216,13 @@ class TrackChapterControls extends StatelessWidget { subtitleSyncOffset: subtitleSyncOffset, canControl: canControl, isLive: isLive, + availableVersions: availableVersions, + selectedMediaIndex: selectedMediaIndex, + selectedQualityPreset: selectedQualityPreset, + serverSupportsTranscoding: serverSupportsTranscoding, + sourceDurationMs: trackControlsState.sourceDurationMs, + onVersionSelected: onSwitchVersion == null ? null : (i) => onSwitchVersion!(i), + onQualitySelected: onSwitchQualityPreset, shaderService: shaderService, onShaderChanged: onShaderChanged, isAmbientLightingEnabled: isAmbientLightingEnabled, @@ -337,51 +343,6 @@ class TrackChapterControls extends StatelessWidget { buttonIndex++; } - // Version & Quality button - final showVersionQuality = - (availableVersions.length > 1 || serverSupportsTranscoding) && - (onSwitchVersion != null || onSwitchQualityPreset != null); - if (showVersionQuality) { - final currentIndex = buttonIndex; - // Tooltip narrows to whichever column the sheet will actually - // render — Jellyfin items only show the version list, so calling - // the button "Version & Quality" implies a quality picker that - // isn't there. - final buttonLabel = serverSupportsTranscoding - ? (availableVersions.length > 1 - ? t.videoControls.versionQualityButton - : t.videoControls.qualityColumnHeader) - : t.videoControls.versionColumnHeader; - buttons.add( - _buildTrackButton( - buttonIndex: currentIndex, - icon: Symbols.video_settings_rounded, - tooltip: buttonLabel, - semanticLabel: buttonLabel, - tracks: tracks, - isMobile: isMobile, - isDesktop: isDesktop, - onPressed: () { - onCancelAutoHide?.call(); - OverlaySheetController.of(context) - .show( - builder: (_) => VersionQualitySheet( - availableVersions: availableVersions, - selectedMediaIndex: selectedMediaIndex, - selectedQualityPreset: selectedQualityPreset, - serverSupportsTranscoding: serverSupportsTranscoding, - sourceDurationMs: trackControlsState.sourceDurationMs, - onVersionSelected: (i) => onSwitchVersion?.call(i), - onQualitySelected: (p) => onSwitchQualityPreset?.call(p), - ), - ) - .whenComplete(() => onStartAutoHide?.call()); - }, - ), - ); - buttonIndex++; - } - // Picture-in-Picture mode if (onTogglePIPMode != null) { final currentIndex = buttonIndex; @@ -503,10 +464,6 @@ class TrackChapterControls extends StatelessWidget { count++; // Audio & subtitles button always shown if (chapters.isNotEmpty && !hideChaptersAndQueue) count++; if (showQueueButton && onQueueItemSelected != null && !hideChaptersAndQueue) count++; - if ((availableVersions.length > 1 || serverSupportsTranscoding) && - (onSwitchVersion != null || onSwitchQualityPreset != null)) { - count++; - } if (onTogglePIPMode != null) count++; if (onCycleBoxFitMode != null) count++; if (isMobile && !PlatformDetector.isTV()) count++; // Rotation lock (not on TV)