290 lines
8.8 KiB
Dart
290 lines
8.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:material_symbols_icons/symbols.dart';
|
|
import 'package:plezy/widgets/app_icon.dart';
|
|
|
|
import '../../../i18n/strings.g.dart';
|
|
import '../../../media/media_version.dart';
|
|
import '../../../models/transcode_quality_preset.dart';
|
|
import '../../../utils/quality_preset_labels.dart';
|
|
import '../../../utils/scroll_utils.dart';
|
|
import '../../../widgets/focusable_list_tile.dart';
|
|
import '../../../widgets/overlay_sheet.dart';
|
|
import 'sheet_column_header.dart';
|
|
|
|
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, the quality column is hidden entirely.
|
|
class VersionQualityPicker extends StatelessWidget {
|
|
final List<MediaVersion> availableVersions;
|
|
final int selectedMediaIndex;
|
|
final TranscodeQualityPreset selectedQualityPreset;
|
|
final bool serverSupportsTranscoding;
|
|
final int? sourceDurationMs;
|
|
final ValueChanged<int> onVersionSelected;
|
|
final ValueChanged<TranscodeQualityPreset> onQualitySelected;
|
|
|
|
const VersionQualityPicker({
|
|
super.key,
|
|
required this.availableVersions,
|
|
required this.selectedMediaIndex,
|
|
required this.selectedQualityPreset,
|
|
required this.serverSupportsTranscoding,
|
|
required this.onVersionSelected,
|
|
required this.onQualitySelected,
|
|
this.sourceDurationMs,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final showVersions = availableVersions.length > 1;
|
|
final showQuality = serverSupportsTranscoding;
|
|
|
|
final qualityColumn = FocusTraversalGroup(
|
|
child: _QualityColumn(
|
|
selected: selectedQualityPreset,
|
|
enabledForTranscoding: serverSupportsTranscoding,
|
|
sourceBitrateKbps: _sourceBitrateKbps(),
|
|
sourceDurationMs: sourceDurationMs,
|
|
sourceSizeBytes: _sourceSizeBytes(),
|
|
onSelected: (preset) {
|
|
OverlaySheetController.of(context).close();
|
|
onQualitySelected(preset);
|
|
},
|
|
showHeader: showVersions,
|
|
),
|
|
);
|
|
|
|
final versionColumn = FocusTraversalGroup(
|
|
child: _VersionColumn(
|
|
versions: availableVersions,
|
|
selectedIndex: selectedMediaIndex,
|
|
onSelected: (index) {
|
|
OverlaySheetController.of(context).close();
|
|
onVersionSelected(index);
|
|
},
|
|
showHeader: showQuality,
|
|
),
|
|
);
|
|
|
|
if (showVersions && showQuality) {
|
|
return Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Expanded(child: versionColumn),
|
|
VerticalDivider(width: 1, color: Theme.of(context).dividerColor),
|
|
Expanded(child: qualityColumn),
|
|
],
|
|
);
|
|
} else if (showVersions) {
|
|
return versionColumn;
|
|
} else {
|
|
return qualityColumn;
|
|
}
|
|
}
|
|
|
|
int? _sourceBitrateKbps() {
|
|
if (selectedMediaIndex < 0 || selectedMediaIndex >= availableVersions.length) {
|
|
return null;
|
|
}
|
|
final b = availableVersions[selectedMediaIndex].bitrate;
|
|
if (b == null || b <= 0) return null;
|
|
return b;
|
|
}
|
|
|
|
int? _sourceSizeBytes() {
|
|
if (selectedMediaIndex < 0 || selectedMediaIndex >= availableVersions.length) {
|
|
return null;
|
|
}
|
|
final parts = availableVersions[selectedMediaIndex].parts;
|
|
if (parts.isEmpty) return null;
|
|
var total = 0;
|
|
for (final p in parts) {
|
|
final s = p.sizeBytes;
|
|
if (s == null || s <= 0) return null;
|
|
total += s;
|
|
}
|
|
return total > 0 ? total : null;
|
|
}
|
|
}
|
|
|
|
class _VersionColumn extends StatefulWidget {
|
|
final List<MediaVersion> versions;
|
|
final int selectedIndex;
|
|
final ValueChanged<int> onSelected;
|
|
final bool showHeader;
|
|
|
|
const _VersionColumn({
|
|
required this.versions,
|
|
required this.selectedIndex,
|
|
required this.onSelected,
|
|
required this.showHeader,
|
|
});
|
|
|
|
@override
|
|
State<_VersionColumn> createState() => _VersionColumnState();
|
|
}
|
|
|
|
class _VersionColumnState extends State<_VersionColumn> {
|
|
final _initialScroll = InitialItemScrollController();
|
|
|
|
@override
|
|
void dispose() {
|
|
_initialScroll.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
_initialScroll.maybeScrollTo(widget.selectedIndex);
|
|
|
|
return Column(
|
|
children: [
|
|
if (widget.showHeader) SheetColumnHeader(label: t.videoControls.versionColumnHeader),
|
|
Expanded(
|
|
child: ListView.builder(
|
|
controller: _initialScroll.controller,
|
|
itemCount: widget.versions.length,
|
|
itemBuilder: (context, index) {
|
|
final version = widget.versions[index];
|
|
final isSelected = index == widget.selectedIndex;
|
|
return _SelectionTile(
|
|
key: index == 0 ? _initialScroll.firstItemKey : null,
|
|
label: version.displayLabel,
|
|
isSelected: isSelected,
|
|
onTap: () => widget.onSelected(index),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _QualityColumn extends StatefulWidget {
|
|
final TranscodeQualityPreset selected;
|
|
final bool enabledForTranscoding;
|
|
final int? sourceBitrateKbps;
|
|
final int? sourceDurationMs;
|
|
final int? sourceSizeBytes;
|
|
final ValueChanged<TranscodeQualityPreset> onSelected;
|
|
final bool showHeader;
|
|
|
|
const _QualityColumn({
|
|
required this.selected,
|
|
required this.enabledForTranscoding,
|
|
required this.sourceBitrateKbps,
|
|
required this.sourceDurationMs,
|
|
required this.sourceSizeBytes,
|
|
required this.onSelected,
|
|
required this.showHeader,
|
|
});
|
|
|
|
@override
|
|
State<_QualityColumn> createState() => _QualityColumnState();
|
|
}
|
|
|
|
class _QualityColumnState extends State<_QualityColumn> {
|
|
final _initialScroll = InitialItemScrollController();
|
|
|
|
@override
|
|
void dispose() {
|
|
_initialScroll.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final presets = TranscodeQualityPreset.displayOrder;
|
|
final selectedIndex = presets.indexOf(widget.selected);
|
|
|
|
_initialScroll.maybeScrollTo(selectedIndex);
|
|
|
|
return Column(
|
|
children: [
|
|
if (widget.showHeader) SheetColumnHeader(label: t.videoControls.qualityColumnHeader),
|
|
Expanded(
|
|
child: ListView.builder(
|
|
controller: _initialScroll.controller,
|
|
itemCount: presets.length,
|
|
itemBuilder: (context, index) {
|
|
final preset = presets[index];
|
|
final isSelected = preset == widget.selected;
|
|
final isOriginal = preset.isOriginal;
|
|
final enabled = isOriginal || widget.enabledForTranscoding;
|
|
|
|
final trailing = qualityPresetSizeEstimate(
|
|
preset: preset,
|
|
sourceBitrateKbps: widget.sourceBitrateKbps,
|
|
sourceDurationMs: widget.sourceDurationMs,
|
|
sourceSizeBytes: widget.sourceSizeBytes,
|
|
);
|
|
|
|
return _SelectionTile(
|
|
key: index == 0 ? _initialScroll.firstItemKey : null,
|
|
label: qualityPresetLabel(preset),
|
|
trailingText: trailing,
|
|
isSelected: isSelected,
|
|
enabled: enabled,
|
|
onTap: enabled ? () => widget.onSelected(preset) : null,
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _SelectionTile extends StatelessWidget {
|
|
final String label;
|
|
final String? trailingText;
|
|
final bool isSelected;
|
|
final bool enabled;
|
|
final VoidCallback? onTap;
|
|
|
|
const _SelectionTile({
|
|
super.key,
|
|
required this.label,
|
|
this.trailingText,
|
|
required this.isSelected,
|
|
this.enabled = true,
|
|
this.onTap,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final scheme = Theme.of(context).colorScheme;
|
|
final primary = scheme.primary;
|
|
final disabledColor = scheme.onSurface.withValues(alpha: 0.38);
|
|
final titleColor = !enabled ? disabledColor : (isSelected ? primary : null);
|
|
final trailingColor = !enabled ? disabledColor : scheme.onSurfaceVariant;
|
|
|
|
final hasText = trailingText != null && trailingText!.isNotEmpty;
|
|
final trailing = (hasText || isSelected)
|
|
? Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
if (hasText) Text(trailingText!, style: TextStyle(color: trailingColor)),
|
|
if (hasText && isSelected) const SizedBox(width: 8),
|
|
if (isSelected) AppIcon(Symbols.check_rounded, fill: 1, color: primary),
|
|
],
|
|
)
|
|
: null;
|
|
|
|
return FocusableListTile(
|
|
title: Text(label, style: TextStyle(color: titleColor)),
|
|
trailing: trailing,
|
|
enabled: enabled,
|
|
onTap: onTap,
|
|
);
|
|
}
|
|
}
|