refactor(player): move quality picker into playback settings
This commit is contained in:
@@ -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<MediaVersion> availableVersions;
|
||||
final int selectedMediaIndex;
|
||||
final TranscodeQualityPreset selectedQualityPreset;
|
||||
@@ -26,7 +30,7 @@ class VersionQualitySheet extends StatelessWidget {
|
||||
final ValueChanged<int> onVersionSelected;
|
||||
final ValueChanged<TranscodeQualityPreset> 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() {
|
||||
|
||||
@@ -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<MediaVersion> availableVersions;
|
||||
final int selectedMediaIndex;
|
||||
final TranscodeQualityPreset selectedQualityPreset;
|
||||
final bool serverSupportsTranscoding;
|
||||
final int? sourceDurationMs;
|
||||
final ValueChanged<int>? onVersionSelected;
|
||||
final ValueChanged<TranscodeQualityPreset>? 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<VideoSettingsSheet> {
|
||||
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<VideoSettingsSheet> {
|
||||
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<VideoSettingsSheet> {
|
||||
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 = <String>[];
|
||||
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<VideoSettingsSheet> {
|
||||
},
|
||||
),
|
||||
|
||||
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<VideoSettingsSheet> {
|
||||
);
|
||||
}
|
||||
|
||||
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<VideoSettingsSheet> {
|
||||
return _buildMenuView();
|
||||
case _SettingsView.speed:
|
||||
return _buildSpeedView();
|
||||
case _SettingsView.versionQuality:
|
||||
return _buildVersionQualityView();
|
||||
case _SettingsView.sleep:
|
||||
return _buildSleepView();
|
||||
case _SettingsView.audioSync:
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user