chore: clean up code comments
This commit is contained in:
@@ -68,7 +68,6 @@ class BottomSheetHeader extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final usesBackButton = leading == null && onBack != null;
|
||||
|
||||
// Determine the leading widget based on priority: leading > onBack > icon
|
||||
Widget? resolvedLeading;
|
||||
if (leading != null) {
|
||||
resolvedLeading = leading;
|
||||
|
||||
@@ -92,7 +92,6 @@ class _DownloadTreeViewState extends State<DownloadTreeView> {
|
||||
@override
|
||||
void didUpdateWidget(DownloadTreeView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
// When suppressAutoFocus changes from true to false, focus the first item
|
||||
if (oldWidget.suppressAutoFocus && !widget.suppressAutoFocus) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted && _firstItemFocusNode.canRequestFocus) {
|
||||
@@ -121,13 +120,11 @@ class _DownloadTreeViewState extends State<DownloadTreeView> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Build the download tree from flat download list
|
||||
List<DownloadTreeNode> _buildTree() {
|
||||
final Map<String, List<MapEntry<String, DownloadProgress>>> showGroups = {};
|
||||
final Map<String, List<MapEntry<String, DownloadProgress>>> albumGroups = {};
|
||||
final List<DownloadTreeNode> movies = [];
|
||||
|
||||
// Group downloads
|
||||
for (final entry in widget.downloads.entries) {
|
||||
final globalKey = entry.key;
|
||||
final download = entry.value;
|
||||
@@ -136,17 +133,14 @@ class _DownloadTreeViewState extends State<DownloadTreeView> {
|
||||
if (meta == null) continue;
|
||||
|
||||
if (meta.isEpisode) {
|
||||
// Group episodes by show
|
||||
final showKey = meta.grandparentId ?? 'unknown';
|
||||
showGroups.putIfAbsent(showKey, () => []);
|
||||
showGroups[showKey]!.add(entry);
|
||||
} else if (meta.kind == MediaKind.track) {
|
||||
// Group tracks by album (single level — no per-disc tier)
|
||||
final albumKey = meta.parentId ?? 'unknown';
|
||||
albumGroups.putIfAbsent(albumKey, () => []);
|
||||
albumGroups[albumKey]!.add(entry);
|
||||
} else if (meta.isMovie) {
|
||||
// Movies go at top level
|
||||
movies.add(
|
||||
DownloadTreeNode(
|
||||
key: globalKey,
|
||||
@@ -161,7 +155,6 @@ class _DownloadTreeViewState extends State<DownloadTreeView> {
|
||||
}
|
||||
}
|
||||
|
||||
// Build show nodes
|
||||
final List<DownloadTreeNode> shows = [];
|
||||
for (final showEntry in showGroups.entries) {
|
||||
final showKey = showEntry.key;
|
||||
@@ -169,11 +162,9 @@ class _DownloadTreeViewState extends State<DownloadTreeView> {
|
||||
|
||||
if (episodes.isEmpty) continue;
|
||||
|
||||
// Get show metadata from first episode
|
||||
final firstEpisode = widget.metadata[episodes.first.key];
|
||||
final showTitle = firstEpisode?.grandparentTitle ?? t.downloads.unknownShow;
|
||||
|
||||
// Group episodes by season
|
||||
final Map<String, List<MapEntry<String, DownloadProgress>>> seasonGroups = {};
|
||||
for (final episode in episodes) {
|
||||
final meta = widget.metadata[episode.key];
|
||||
@@ -184,7 +175,6 @@ class _DownloadTreeViewState extends State<DownloadTreeView> {
|
||||
seasonGroups[seasonKey]!.add(episode);
|
||||
}
|
||||
|
||||
// Build season nodes
|
||||
final List<DownloadTreeNode> seasons = [];
|
||||
for (final seasonEntry in seasonGroups.entries) {
|
||||
final seasonKey = seasonEntry.key;
|
||||
@@ -192,7 +182,6 @@ class _DownloadTreeViewState extends State<DownloadTreeView> {
|
||||
|
||||
if (seasonEpisodes.isEmpty) continue;
|
||||
|
||||
// Get season metadata from first episode
|
||||
final firstEpisode = widget.metadata[seasonEpisodes.first.key];
|
||||
final seasonNumber = firstEpisode?.parentIndex;
|
||||
final seasonTitle = firstEpisode?.parentTitle?.isNotEmpty == true
|
||||
@@ -201,7 +190,6 @@ class _DownloadTreeViewState extends State<DownloadTreeView> {
|
||||
? t.common.seasonNumber(number: seasonNumber)
|
||||
: t.downloads.unknownSeason;
|
||||
|
||||
// Build episode nodes
|
||||
final List<DownloadTreeNode> episodeNodes = [];
|
||||
for (final episodeEntry in seasonEpisodes) {
|
||||
final globalKey = episodeEntry.key;
|
||||
@@ -228,14 +216,12 @@ class _DownloadTreeViewState extends State<DownloadTreeView> {
|
||||
);
|
||||
}
|
||||
|
||||
// Sort episodes by episode number only (not by status)
|
||||
episodeNodes.sort((a, b) {
|
||||
final aIndex = a.metadata?.index ?? 0;
|
||||
final bIndex = b.metadata?.index ?? 0;
|
||||
return aIndex.compareTo(bIndex);
|
||||
});
|
||||
|
||||
// Calculate aggregate season progress
|
||||
final seasonProgress = episodeNodes.isEmpty
|
||||
? 0.0
|
||||
: episodeNodes.map((e) => e.progress).reduce((a, b) => a + b) / episodeNodes.length;
|
||||
@@ -255,14 +241,12 @@ class _DownloadTreeViewState extends State<DownloadTreeView> {
|
||||
|
||||
seasons.removeWhere((s) => s.children.isEmpty);
|
||||
|
||||
// Sort seasons by season number
|
||||
seasons.sort((a, b) {
|
||||
final aSeasonNum = widget.metadata[a.children.first.key]?.parentIndex ?? 0;
|
||||
final bSeasonNum = widget.metadata[b.children.first.key]?.parentIndex ?? 0;
|
||||
return aSeasonNum.compareTo(bSeasonNum);
|
||||
});
|
||||
|
||||
// Calculate aggregate show progress
|
||||
final showProgress = seasons.isEmpty
|
||||
? 0.0
|
||||
: seasons.map((s) => s.progress).reduce((a, b) => a + b) / seasons.length;
|
||||
@@ -280,14 +264,12 @@ class _DownloadTreeViewState extends State<DownloadTreeView> {
|
||||
);
|
||||
}
|
||||
|
||||
// Build album nodes (album -> tracks)
|
||||
final List<DownloadTreeNode> albums = [];
|
||||
for (final albumEntry in albumGroups.entries) {
|
||||
final albumKey = albumEntry.key;
|
||||
final tracks = albumEntry.value;
|
||||
if (tracks.isEmpty) continue;
|
||||
|
||||
// Album/artist names from any track's parent fields
|
||||
final firstTrack = widget.metadata[tracks.first.key];
|
||||
final albumTitle = firstTrack?.albumTitle ?? t.downloads.unknownAlbum;
|
||||
final artistTitle = firstTrack?.albumArtistTitle;
|
||||
@@ -314,7 +296,6 @@ class _DownloadTreeViewState extends State<DownloadTreeView> {
|
||||
}
|
||||
if (trackNodes.isEmpty) continue;
|
||||
|
||||
// Sort tracks by disc then track number
|
||||
trackNodes.sort((a, b) {
|
||||
final byDisc = (a.metadata?.discNumber ?? 1).compareTo(b.metadata?.discNumber ?? 1);
|
||||
if (byDisc != 0) return byDisc;
|
||||
@@ -336,17 +317,13 @@ class _DownloadTreeViewState extends State<DownloadTreeView> {
|
||||
);
|
||||
}
|
||||
|
||||
// Sort shows, albums, and movies by status and title
|
||||
_sortNodesByStatusAndTitle(shows);
|
||||
_sortNodesByStatusAndTitle(albums);
|
||||
_sortNodesByStatusAndTitle(movies);
|
||||
|
||||
// Combine movies, shows, and albums
|
||||
return [...movies, ...shows, ...albums];
|
||||
}
|
||||
|
||||
/// Determine aggregate status from child statuses
|
||||
/// Priority: downloading > queued > paused > completed > failed
|
||||
DownloadStatus _determineAggregateStatus(List<DownloadStatus> statuses) {
|
||||
if (statuses.isEmpty) return DownloadStatus.queued;
|
||||
|
||||
@@ -365,7 +342,6 @@ class _DownloadTreeViewState extends State<DownloadTreeView> {
|
||||
return DownloadStatus.completed;
|
||||
}
|
||||
|
||||
/// Compare statuses for sorting (downloading first, then queued, etc.)
|
||||
int _compareByStatus(DownloadStatus a, DownloadStatus b) {
|
||||
const statusOrder = {
|
||||
DownloadStatus.downloading: 0,
|
||||
@@ -378,7 +354,6 @@ class _DownloadTreeViewState extends State<DownloadTreeView> {
|
||||
return (statusOrder[a] ?? 99).compareTo(statusOrder[b] ?? 99);
|
||||
}
|
||||
|
||||
/// Sort nodes by status (downloading first) then by title
|
||||
void _sortNodesByStatusAndTitle(List<DownloadTreeNode> nodes) {
|
||||
nodes.sort((a, b) {
|
||||
final statusCompare = _compareByStatus(a.status, b.status);
|
||||
@@ -414,7 +389,6 @@ class _DownloadTreeViewState extends State<DownloadTreeView> {
|
||||
});
|
||||
}
|
||||
|
||||
/// Build a tree item widget
|
||||
Widget _buildTreeItem(DownloadTreeNode node, int depth, {bool isFirst = false}) {
|
||||
return _DownloadTreeItem(
|
||||
node: node,
|
||||
@@ -590,9 +564,7 @@ class _DownloadTreeItemState extends State<_DownloadTreeItem> {
|
||||
return widget.node.status;
|
||||
}
|
||||
|
||||
// Focus node for row content (only created if not provided externally)
|
||||
FocusNode? _ownedRowFocusNode;
|
||||
// Focus nodes for action buttons (up to 3 buttons max)
|
||||
final List<FocusNode> _buttonFocusNodes = [];
|
||||
|
||||
FocusNode get _rowFocusNode => widget.rowFocusNode ?? _ownedRowFocusNode!;
|
||||
@@ -676,10 +648,8 @@ class _DownloadTreeItemState extends State<_DownloadTreeItem> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
// Row content
|
||||
Expanded(child: _buildRowContent(theme, canExpand)),
|
||||
|
||||
// Action buttons
|
||||
if (actions.isNotEmpty)
|
||||
Row(
|
||||
mainAxisSize: .min,
|
||||
@@ -696,7 +666,6 @@ class _DownloadTreeItemState extends State<_DownloadTreeItem> {
|
||||
Widget _buildRowContent(ThemeData theme, bool canExpand) {
|
||||
return Row(
|
||||
children: [
|
||||
// Expand/collapse icon
|
||||
if (canExpand)
|
||||
AppIcon(widget.isExpanded ? Symbols.expand_more_rounded : Symbols.chevron_right_rounded, fill: 1, size: 20)
|
||||
else
|
||||
@@ -704,12 +673,10 @@ class _DownloadTreeItemState extends State<_DownloadTreeItem> {
|
||||
|
||||
const SizedBox(width: 8),
|
||||
|
||||
// Status icon
|
||||
DownloadStatusIcon(status: _effectiveStatus, size: 20),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Title and info
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: .start,
|
||||
|
||||
@@ -275,7 +275,6 @@ class HubSectionState extends State<HubSection> with MountedSetStateMixin, Skele
|
||||
}
|
||||
}
|
||||
|
||||
// Handle key down and repeat events
|
||||
if (!event.isActionable) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
@@ -289,7 +289,6 @@ class _LibraryManagementSheetState extends State<_LibraryManagementSheet>
|
||||
final ScrollController _dialogScrollController = ScrollController();
|
||||
final ScrollController _sheetScrollController = ScrollController();
|
||||
|
||||
// Keyboard navigation: column 0 = row, 1 = visibility button, 2 = options button.
|
||||
@override
|
||||
List<MediaLibrary> get reorderItems => _tempLibraries;
|
||||
|
||||
@@ -468,16 +467,13 @@ class _LibraryManagementSheetState extends State<_LibraryManagementSheet>
|
||||
final isHidden = hiddenLibraryKeys.contains(library.globalKey);
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
// Determine background color based on state
|
||||
Color? tileColor;
|
||||
if (isMoving) {
|
||||
tileColor = colorScheme.primaryContainer;
|
||||
} else if (isFocused && focusedColumn == 0) {
|
||||
// Only highlight row when row itself is focused (column 0)
|
||||
tileColor = colorScheme.surfaceContainerHighest;
|
||||
}
|
||||
|
||||
// Button focus states
|
||||
final isVisibilityButtonFocused = isFocused && focusedColumn == 1;
|
||||
final isOptionsButtonFocused = isFocused && focusedColumn == 2;
|
||||
|
||||
|
||||
@@ -615,13 +615,11 @@ class MediaCardState extends State<MediaCard> with ContextMenuTapMixin<MediaCard
|
||||
mainAxisSize: .min,
|
||||
crossAxisAlignment: .start,
|
||||
children: [
|
||||
// Poster with overlay
|
||||
if (posterHeight != null)
|
||||
SizedBox(width: double.infinity, height: posterHeight, child: poster)
|
||||
else
|
||||
Expanded(child: poster),
|
||||
const SizedBox(height: 2),
|
||||
// Title (flattened — no inner Column)
|
||||
if (widget.onTap == null && item is MediaItem && _hasClickableTitle(item))
|
||||
_ClickableText(
|
||||
text: item.displayTitle,
|
||||
@@ -637,7 +635,6 @@ class MediaCardState extends State<MediaCard> with ContextMenuTapMixin<MediaCard
|
||||
style: const TextStyle(fontWeight: .w600, fontSize: 13, height: 1.1),
|
||||
),
|
||||
),
|
||||
// Subtitle
|
||||
if (item is MediaPlaylist)
|
||||
_MediaCardHelpers.buildPlaylistMeta(context, item)
|
||||
else if (item is MediaItem)
|
||||
@@ -1131,7 +1128,6 @@ class _MediaCardHelpers {
|
||||
}
|
||||
}
|
||||
|
||||
// For collections, show item count
|
||||
if (mi.kind == MediaKind.collection) {
|
||||
final count = mi.childCount ?? mi.leafCount;
|
||||
if (count != null && count > 0) {
|
||||
@@ -1146,14 +1142,12 @@ class _MediaCardHelpers {
|
||||
}
|
||||
}
|
||||
|
||||
// For albums, show the album artist
|
||||
if (mi.kind == MediaKind.album && mi.albumArtistTitle != null) {
|
||||
return ExcludeSemantics(
|
||||
child: Text(mi.albumArtistTitle!, maxLines: 1, overflow: .ellipsis, style: subtitleStyle),
|
||||
);
|
||||
}
|
||||
|
||||
// For tracks, show "Artist • duration"
|
||||
if (mi.kind == MediaKind.track) {
|
||||
final parts = [?mi.trackArtistTitle, if (mi.durationMs case final durationMs?) formatDurationTextual(durationMs)];
|
||||
if (parts.isNotEmpty) {
|
||||
@@ -1163,7 +1157,6 @@ class _MediaCardHelpers {
|
||||
}
|
||||
}
|
||||
|
||||
// For episodes, show "S# · Episode Title" with clickable season link
|
||||
if (mi.isEpisode && mi.parentIndex != null) {
|
||||
if (enableDetailLinks && mi.parentId != null) {
|
||||
return _buildEpisodeSubtitleRow(
|
||||
@@ -1185,7 +1178,6 @@ class _MediaCardHelpers {
|
||||
);
|
||||
}
|
||||
|
||||
// For other media types, show subtitle/parent/year
|
||||
if (mi.displaySubtitle != null) {
|
||||
return ExcludeSemantics(
|
||||
child: Text(mi.displaySubtitle!, maxLines: 1, overflow: .ellipsis, style: subtitleStyle),
|
||||
|
||||
@@ -130,7 +130,7 @@ class MediaContextMenu extends StatefulWidget {
|
||||
final Object item;
|
||||
final void Function(MediaItem source)? onRefresh;
|
||||
final VoidCallback? onRemoveFromContinueWatching;
|
||||
final VoidCallback? onListRefresh; // For refreshing list after deletion
|
||||
final VoidCallback? onListRefresh;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
/// Plays the item's trailer. When non-null a "Play trailer" item is added to
|
||||
|
||||
@@ -173,7 +173,6 @@ class DesktopVideoControlsState extends State<DesktopVideoControls> {
|
||||
bool get _canControl => _trackControlsState.canControl;
|
||||
bool get _isLive => _trackControlsState.isLive;
|
||||
|
||||
// Focus nodes for playback control buttons
|
||||
late final FocusNode _prevItemFocusNode;
|
||||
late final FocusNode _prevChapterFocusNode;
|
||||
late final FocusNode _skipBackFocusNode;
|
||||
@@ -184,30 +183,23 @@ class DesktopVideoControlsState extends State<DesktopVideoControls> {
|
||||
late final FocusNode _goToLiveFocusNode;
|
||||
late final FocusNode _timelineFocusNode;
|
||||
|
||||
// Focus node for volume control
|
||||
late final FocusNode _volumeFocusNode;
|
||||
|
||||
// Focus nodes for track/chapter controls (max 8 buttons possible)
|
||||
late final List<FocusNode> _trackControlFocusNodes;
|
||||
|
||||
// List of button focus nodes for horizontal navigation
|
||||
late final List<FocusNode> _buttonFocusNodes;
|
||||
|
||||
// Progressive seek acceleration state
|
||||
LogicalKeyboardKey? _seekDirection; // Current direction being held
|
||||
int _seekRepeatCount = 0; // Consecutive key repeats for acceleration
|
||||
|
||||
// Preview thumbnail during sustained dpad/keyboard seeking
|
||||
bool _showKeyRepeatThumbnail = false;
|
||||
Timer? _keyRepeatThumbnailTimer;
|
||||
late final DebouncedSeekAccumulator _timelineSeek;
|
||||
static const _keyRepeatThumbnailTimeout = Duration(milliseconds: 400);
|
||||
|
||||
// Content strip state
|
||||
bool _contentStripVisible = false;
|
||||
final GlobalKey<ContentStripState> _contentStripKey = GlobalKey<ContentStripState>();
|
||||
|
||||
// Track which button was last focused (for returning from content strip)
|
||||
FocusNode? _lastFocusedButtonNode;
|
||||
|
||||
/// Whether the content strip has any content to show
|
||||
@@ -714,7 +706,6 @@ class DesktopVideoControlsState extends State<DesktopVideoControls> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
// Row 1: Timeline (LiveTimelineBar for time-shifted live, VideoTimelineBar for VOD)
|
||||
if (_isLive && widget.captureBuffer != null) ...[
|
||||
LiveTimelineBar(
|
||||
player: widget.player,
|
||||
@@ -748,14 +739,12 @@ class DesktopVideoControlsState extends State<DesktopVideoControls> {
|
||||
previewPosition: _timelineSeek.pendingPosition,
|
||||
),
|
||||
],
|
||||
// Row 2: Playback controls and options
|
||||
Focus(
|
||||
onFocusChange: _onButtonRowFocusChange,
|
||||
skipTraversal: true,
|
||||
child: Row(
|
||||
children: [
|
||||
if (!_isLive) ...[
|
||||
// Previous item
|
||||
Opacity(
|
||||
opacity: _canControl ? 1.0 : 0.5,
|
||||
child: _buildFocusableButton(
|
||||
|
||||
@@ -354,7 +354,6 @@ class _MobileVideoControlsState extends State<MobileVideoControls> with SingleTi
|
||||
mainAxisAlignment: .center,
|
||||
children: [
|
||||
if (!widget.isLive) ...[
|
||||
// Previous episode button (greyed out when unavailable)
|
||||
CircularControlButton(
|
||||
semanticLabel: t.videoControls.previousButton,
|
||||
icon: Symbols.skip_previous_rounded,
|
||||
@@ -378,7 +377,6 @@ class _MobileVideoControlsState extends State<MobileVideoControls> with SingleTi
|
||||
),
|
||||
if (!widget.isLive) ...[
|
||||
const SizedBox(width: 24),
|
||||
// Next episode button (greyed out when unavailable)
|
||||
CircularControlButton(
|
||||
semanticLabel: t.videoControls.nextButton,
|
||||
icon: Symbols.skip_next_rounded,
|
||||
|
||||
@@ -24,7 +24,6 @@ extension _PlexVideoControlsTrackMethods on _PlexVideoControlsState {
|
||||
}
|
||||
|
||||
final currentTrack = widget.player.state.track.subtitle;
|
||||
// Nothing to hide when no subtitle track is selected.
|
||||
if (currentTrack == null || currentTrack.id == SubtitleTrack.off.id) return;
|
||||
|
||||
_setSubtitleVisibility(false);
|
||||
@@ -51,7 +50,6 @@ extension _PlexVideoControlsTrackMethods on _PlexVideoControlsState {
|
||||
}
|
||||
|
||||
void _onSubtitleTrackChanged(SubtitleTrack track) {
|
||||
// Reset visibility when user explicitly picks a new subtitle track
|
||||
if (track.id != 'no' && !_subtitlesVisible) {
|
||||
_setSubtitleVisibility(true);
|
||||
}
|
||||
|
||||
@@ -770,7 +770,6 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
|
||||
},
|
||||
),
|
||||
|
||||
// Audio Sync
|
||||
_SettingsMenuItem(
|
||||
icon: Symbols.sync_rounded,
|
||||
title: t.videoSettings.audioSync,
|
||||
@@ -779,7 +778,6 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
|
||||
onTap: () => _navigateTo(_SettingsView.audioSync),
|
||||
),
|
||||
|
||||
// Subtitle Sync
|
||||
_SettingsMenuItem(
|
||||
icon: Symbols.subtitles_rounded,
|
||||
title: t.videoSettings.subtitleSync,
|
||||
@@ -788,7 +786,6 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
|
||||
onTap: () => _navigateTo(_SettingsView.subtitleSync),
|
||||
),
|
||||
|
||||
// HDR Toggle
|
||||
if (_supportsHdrControl)
|
||||
_SettingsToggleItem(
|
||||
pref: SettingsService.enableHDR,
|
||||
@@ -808,14 +805,12 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
|
||||
onTap: () => _navigateTo(_SettingsView.hdrToneMapping),
|
||||
),
|
||||
|
||||
// Auto-Play Next Episode Toggle
|
||||
_SettingsToggleItem(
|
||||
pref: SettingsService.autoPlayNextEpisode,
|
||||
icon: Symbols.skip_next_rounded,
|
||||
title: t.videoControls.autoPlayNext,
|
||||
),
|
||||
|
||||
// Audio Output Device (Desktop only)
|
||||
if (isDesktop)
|
||||
StreamBuilder<AudioDevice>(
|
||||
stream: widget.player.streams.audioDevice,
|
||||
@@ -845,7 +840,6 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
|
||||
// "not Dolby" when the system reports notApplicable.
|
||||
if (PlatformDetector.isAppleTV()) _AudioRenderingModeItem(player: widget.player),
|
||||
|
||||
// Audio Normalization
|
||||
_SettingsToggleItem(
|
||||
pref: SettingsService.audioNormalization,
|
||||
icon: Symbols.graphic_eq_rounded,
|
||||
@@ -853,7 +847,6 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
|
||||
onAfterWrite: widget.player.setAudioNormalization,
|
||||
),
|
||||
|
||||
// Stereo Downmix
|
||||
_SettingsToggleItem(
|
||||
pref: SettingsService.audioDownmix,
|
||||
icon: Symbols.headphones_rounded,
|
||||
|
||||
@@ -62,7 +62,6 @@ class VideoControlButton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Determine the effective color: explicit color > active amber > default white
|
||||
final effectiveColor = color ?? (isActive ? Colors.amber : Colors.white);
|
||||
|
||||
final button = IconButton(
|
||||
@@ -85,9 +84,9 @@ class VideoControlButton extends StatelessWidget {
|
||||
semanticLabel: effectiveSemanticLabel,
|
||||
semanticValue: semanticValue,
|
||||
checked: checked,
|
||||
borderRadius: 20, // Circular for icon buttons
|
||||
autoScroll: false, // Video controls don't scroll
|
||||
useBackgroundFocus: true, // Use background highlight for video controls
|
||||
borderRadius: 20,
|
||||
autoScroll: false,
|
||||
useBackgroundFocus: true,
|
||||
child: result,
|
||||
);
|
||||
} else if (effectiveSemanticLabel != null) {
|
||||
|
||||
@@ -5,10 +5,8 @@ import '../../../../i18n/strings.g.dart';
|
||||
/// Contains metrics queried from the video player (MPV or ExoPlayer)
|
||||
/// including video/audio codec info, playback performance, and buffer state.
|
||||
class PerformanceStats {
|
||||
// Player info
|
||||
final String playerType; // 'mpv' or 'exoplayer'
|
||||
final String playerType;
|
||||
|
||||
// Video metrics
|
||||
final String? videoCodec;
|
||||
final int? videoWidth;
|
||||
final int? videoHeight;
|
||||
@@ -19,38 +17,32 @@ class PerformanceStats {
|
||||
final int? rotate;
|
||||
final String? videoDecoderName;
|
||||
|
||||
// Color/Format metrics
|
||||
final String? pixelformat;
|
||||
final String? hwPixelformat;
|
||||
final String? colormatrix;
|
||||
final String? primaries;
|
||||
final String? gamma;
|
||||
|
||||
// HDR metadata
|
||||
final double? maxLuma;
|
||||
final double? minLuma;
|
||||
final double? maxCll;
|
||||
final double? maxFall;
|
||||
|
||||
// Audio metrics
|
||||
final String? audioCodec;
|
||||
final int? audioSamplerate;
|
||||
final String? audioChannels;
|
||||
final int? audioBitrate;
|
||||
final String? audioDecoderName;
|
||||
|
||||
// Tunneling
|
||||
final bool tunneledPlayback;
|
||||
final String? tunnelingStatus;
|
||||
|
||||
// Performance metrics
|
||||
final double? actualFps;
|
||||
final double? avsyncChange;
|
||||
final double? displayFps;
|
||||
final int? frameDropCount;
|
||||
final int? decoderFrameDropCount;
|
||||
|
||||
// Buffer metrics
|
||||
final int? cacheUsed;
|
||||
final int? cacheLimit;
|
||||
final double? cacheSpeed;
|
||||
@@ -58,9 +50,8 @@ class PerformanceStats {
|
||||
final int? bufferTargetBytes;
|
||||
final int? bufferMaxMs;
|
||||
|
||||
// DV conversion
|
||||
final bool dvConversionActive;
|
||||
final String dvConversionMode; // "DV81", "HEVC_STRIP", "DISABLED"
|
||||
final String dvConversionMode;
|
||||
final int? dvConvertedRpus;
|
||||
final int? dvRpuConversionFailures;
|
||||
final int? dvRpuOutputTooSmall;
|
||||
@@ -70,7 +61,6 @@ class PerformanceStats {
|
||||
final String? dvPlaybackPath;
|
||||
final String? dvPlaybackReason;
|
||||
|
||||
// App metrics
|
||||
final int? appMemoryBytes;
|
||||
final double? uiFps;
|
||||
|
||||
|
||||
@@ -69,7 +69,6 @@ class TrackChapterControls extends StatelessWidget {
|
||||
|
||||
final key = event.logicalKey;
|
||||
|
||||
// LEFT arrow - move to previous button or exit to volume
|
||||
if (key == LogicalKeyboardKey.arrowLeft) {
|
||||
if (index > 0 && focusNodes != null && focusNodes!.length > index - 1) {
|
||||
focusNodes![index - 1].requestFocus();
|
||||
@@ -81,23 +80,19 @@ class TrackChapterControls extends StatelessWidget {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
// RIGHT arrow - move to next button
|
||||
if (key == LogicalKeyboardKey.arrowRight) {
|
||||
if (index < totalButtons - 1 && focusNodes != null && focusNodes!.length > index + 1) {
|
||||
focusNodes![index + 1].requestFocus();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
// At end, consume to prevent bubbling
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
// UP arrow - navigate up (e.g., to timeline)
|
||||
if (key == LogicalKeyboardKey.arrowUp) {
|
||||
onNavigateUp?.call();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
// DOWN arrow - navigate down (e.g., to content strip)
|
||||
if (key == LogicalKeyboardKey.arrowDown) {
|
||||
onNavigateDown?.call();
|
||||
return KeyEventResult.handled;
|
||||
@@ -146,11 +141,9 @@ class TrackChapterControls extends StatelessWidget {
|
||||
final isMobile = PlatformDetector.isMobile(context);
|
||||
final isDesktop = PlatformDetector.isDesktopOS();
|
||||
|
||||
// Build list of buttons dynamically to track indices
|
||||
final buttons = <Widget>[];
|
||||
int buttonIndex = 0;
|
||||
|
||||
// Settings button (always shown)
|
||||
buttons.add(
|
||||
ListenableBuilder(
|
||||
listenable: SleepTimerService(),
|
||||
@@ -193,7 +186,6 @@ class TrackChapterControls extends StatelessWidget {
|
||||
);
|
||||
buttonIndex++;
|
||||
|
||||
// Combined audio & subtitles button
|
||||
{
|
||||
final currentIndex = buttonIndex;
|
||||
buttons.add(
|
||||
@@ -232,7 +224,6 @@ class TrackChapterControls extends StatelessWidget {
|
||||
buttonIndex++;
|
||||
}
|
||||
|
||||
// Chapters button (hidden on mobile when content strip is available)
|
||||
if (chapters.isNotEmpty && !hideChaptersAndQueue) {
|
||||
final currentIndex = buttonIndex;
|
||||
buttons.add(
|
||||
@@ -264,7 +255,6 @@ class TrackChapterControls extends StatelessWidget {
|
||||
buttonIndex++;
|
||||
}
|
||||
|
||||
// Queue button (hidden on mobile when content strip is available)
|
||||
if (state.showQueueButton && state.onQueueItemSelected != null && !hideChaptersAndQueue) {
|
||||
final currentIndex = buttonIndex;
|
||||
buttons.add(
|
||||
@@ -286,7 +276,6 @@ class TrackChapterControls extends StatelessWidget {
|
||||
buttonIndex++;
|
||||
}
|
||||
|
||||
// Picture-in-Picture mode
|
||||
if (state.onTogglePIPMode != null) {
|
||||
final currentIndex = buttonIndex;
|
||||
buttons.add(
|
||||
|
||||
Reference in New Issue
Block a user