fix(player): keep right-arrow alive on the last control when rotation and screen lock are both visible

_getButtonCount re-implemented every button-visibility condition from build() and had already drifted: the screen-lock button incremented the built index but was never counted, so with rotation lock and screen lock both visible the reported total was one short and arrowRight on the last button silently died. Navigation now bounds against the actual built list, so there is no condition table left to drift; the dead onLoadSeekTimes sheet-compat callback invocation goes with it.
This commit is contained in:
edde746
2026-08-17 19:02:22 +02:00
parent 057ff98ec1
commit 49fce5ace9
@@ -107,8 +107,9 @@ class TrackChapterControls extends StatelessWidget {
required IconData icon,
required String semanticLabel,
required VoidCallback? onPressed,
required bool isMobile,
required bool isDesktop,
// The row being built. Arrow-key navigation reads its length lazily, so
// by event time it reflects every button that build() actually added.
required List<Widget> buttons,
String? tooltip,
String? semanticValue,
bool? checked,
@@ -123,7 +124,7 @@ class TrackChapterControls extends StatelessWidget {
isActive: isActive,
focusNode: focusNodes != null && focusNodes!.length > buttonIndex ? focusNodes![buttonIndex] : null,
onKeyEvent: focusNodes != null
? (node, event) => _handleButtonKeyEvent(node, event, buttonIndex, _getButtonCount(isMobile, isDesktop))
? (node, event) => _handleButtonKeyEvent(node, event, buttonIndex, buttons.length)
: null,
onFocusChange: onFocusChange,
onPressed: onPressed,
@@ -167,18 +168,14 @@ class TrackChapterControls extends StatelessWidget {
tooltip: t.videoControls.settingsButton,
semanticValue: _versionQualitySemanticValue(),
semanticLabel: t.videoControls.settingsButton,
isMobile: isMobile,
isDesktop: isDesktop,
buttons: buttons,
onPressed: () {
state.onCancelAutoHide?.call();
OverlaySheetController.of(context)
.show(
builder: (_) => VideoSettingsSheet(player: player, trackControlsState: state),
)
.whenComplete(() {
state.onStartAutoHide?.call();
state.onLoadSeekTimes?.call();
});
.whenComplete(() => state.onStartAutoHide?.call());
},
);
},
@@ -207,8 +204,7 @@ class TrackChapterControls extends StatelessWidget {
tooltip: t.videoControls.tracksButton,
semanticLabel: t.videoControls.tracksButton,
semanticValue: _selectionSemanticValue(tracks, selection),
isMobile: isMobile,
isDesktop: isDesktop,
buttons: buttons,
onPressed: () {
state.onCancelAutoHide?.call();
OverlaySheetController.of(context)
@@ -232,8 +228,7 @@ class TrackChapterControls extends StatelessWidget {
icon: Symbols.bookmarks_rounded,
tooltip: t.videoControls.chaptersButton,
semanticLabel: t.videoControls.chaptersButton,
isMobile: isMobile,
isDesktop: isDesktop,
buttons: buttons,
onPressed: () {
state.onCancelAutoHide?.call();
OverlaySheetController.of(context)
@@ -263,8 +258,7 @@ class TrackChapterControls extends StatelessWidget {
icon: Symbols.queue_rounded,
tooltip: t.videoControls.queue,
semanticLabel: t.videoControls.queue,
isMobile: isMobile,
isDesktop: isDesktop,
buttons: buttons,
onPressed: () {
state.onCancelAutoHide?.call();
OverlaySheetController.of(context)
@@ -284,8 +278,7 @@ class TrackChapterControls extends StatelessWidget {
icon: Symbols.picture_in_picture_alt_rounded,
tooltip: t.videoControls.pipButton,
semanticLabel: t.videoControls.pipButton,
isMobile: isMobile,
isDesktop: isDesktop,
buttons: buttons,
onPressed: state.onTogglePIPMode,
),
);
@@ -302,8 +295,7 @@ class TrackChapterControls extends StatelessWidget {
tooltip: _getBoxFitTooltip(state.boxFitMode),
semanticLabel: t.videoControls.aspectRatioButton,
semanticValue: _getBoxFitTooltip(state.boxFitMode),
isMobile: isMobile,
isDesktop: isDesktop,
buttons: buttons,
onPressed: state.onCycleBoxFitMode,
),
);
@@ -320,8 +312,7 @@ class TrackChapterControls extends StatelessWidget {
tooltip: state.isRotationLocked ? t.videoControls.unlockRotation : t.videoControls.lockRotation,
semanticLabel: t.videoControls.rotationLockButton,
checked: state.isRotationLocked,
isMobile: isMobile,
isDesktop: isDesktop,
buttons: buttons,
onPressed: state.onToggleRotationLock,
),
);
@@ -337,8 +328,7 @@ class TrackChapterControls extends StatelessWidget {
icon: Symbols.lock_rounded,
tooltip: t.videoControls.lockScreen,
semanticLabel: t.videoControls.screenLockButton,
isMobile: isMobile,
isDesktop: isDesktop,
buttons: buttons,
onPressed: state.onToggleScreenLock,
),
);
@@ -356,8 +346,7 @@ class TrackChapterControls extends StatelessWidget {
semanticLabel: t.videoControls.alwaysOnTopButton,
isActive: state.isAlwaysOnTop,
checked: state.isAlwaysOnTop,
isMobile: isMobile,
isDesktop: isDesktop,
buttons: buttons,
onPressed: state.onToggleAlwaysOnTop,
),
);
@@ -376,8 +365,7 @@ class TrackChapterControls extends StatelessWidget {
? t.videoControls.exitFullscreenButton
: t.videoControls.fullscreenButton,
checked: state.isFullscreen,
isMobile: isMobile,
isDesktop: isDesktop,
buttons: buttons,
onPressed: state.onToggleFullscreen,
),
);
@@ -438,21 +426,6 @@ class TrackChapterControls extends StatelessWidget {
return values.isEmpty ? null : values.join(', ');
}
/// Calculate total button count for navigation
int _getButtonCount(bool isMobile, bool isDesktop) {
final state = trackControlsState;
int count = 1; // Settings button always shown
count++; // Audio & subtitles button always shown
if (chapters.isNotEmpty && !hideChaptersAndQueue) count++;
if (state.showQueueButton && state.onQueueItemSelected != null && !hideChaptersAndQueue) count++;
if (state.onTogglePIPMode != null) count++;
if (state.onCycleBoxFitMode != null) count++;
if (isMobile && !PlatformDetector.isTV()) count++; // Rotation lock (not on TV)
if (isDesktop && state.onToggleAlwaysOnTop != null) count++; // Always on top
if (isDesktop) count++; // Fullscreen
return count;
}
IconData _getBoxFitIcon(int mode) {
switch (mode) {
case 0: