From 931bc9b93108b140ab9e8fe5347c230ecd74edf8 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Sat, 15 Nov 2025 22:17:04 -0600 Subject: [PATCH] Added semantic labels to media player controls --- .../video_controls/video_controls.dart | 235 ++++++++++-------- 1 file changed, 134 insertions(+), 101 deletions(-) diff --git a/lib/widgets/video_controls/video_controls.dart b/lib/widgets/video_controls/video_controls.dart index fde60db0..9d2638df 100644 --- a/lib/widgets/video_controls/video_controls.dart +++ b/lib/widgets/video_controls/video_controls.dart @@ -454,6 +454,7 @@ class _PlexVideoControlsState extends State return VideoControlButton( icon: Icons.tune, isActive: isActive, + semanticLabel: t.videoControls.settingsButton, onPressed: () async { await VideoSettingsSheet.show( context, @@ -472,6 +473,7 @@ class _PlexVideoControlsState extends State if (_hasMultipleAudioTracks(tracks)) VideoControlButton( icon: Icons.audiotrack, + semanticLabel: t.videoControls.audioTrackButton, onPressed: () => AudioTrackSheet.show( context, widget.player, @@ -481,6 +483,7 @@ class _PlexVideoControlsState extends State if (_hasSubtitles(tracks)) VideoControlButton( icon: Icons.subtitles, + semanticLabel: t.videoControls.subtitlesButton, onPressed: () => SubtitleTrackSheet.show( context, widget.player, @@ -490,6 +493,7 @@ class _PlexVideoControlsState extends State if (_chapters.isNotEmpty) VideoControlButton( icon: Icons.video_library, + semanticLabel: t.videoControls.chaptersButton, onPressed: () => ChapterSheet.show( context, widget.player, @@ -500,6 +504,7 @@ class _PlexVideoControlsState extends State if (widget.availableVersions.length > 1) VideoControlButton( icon: Icons.video_file, + semanticLabel: t.videoControls.versionsButton, onPressed: () => VersionSheet.show( context, widget.availableVersions, @@ -512,6 +517,7 @@ class _PlexVideoControlsState extends State VideoControlButton( icon: _getBoxFitIcon(widget.boxFitMode), tooltip: _getBoxFitTooltip(widget.boxFitMode), + semanticLabel: t.videoControls.aspectRatioButton, onPressed: widget.onCycleBoxFitMode, ), // Rotation lock toggle (mobile only) @@ -523,6 +529,7 @@ class _PlexVideoControlsState extends State tooltip: _isRotationLocked ? t.videoControls.unlockRotation : t.videoControls.lockRotation, + semanticLabel: t.videoControls.rotationLockButton, onPressed: _toggleRotationLock, ), // Fullscreen toggle (desktop only) @@ -531,6 +538,9 @@ class _PlexVideoControlsState extends State icon: _isFullscreen ? Icons.fullscreen_exit : Icons.fullscreen, + semanticLabel: _isFullscreen + ? t.videoControls.exitFullscreenButton + : t.videoControls.fullscreenButton, onPressed: _toggleFullscreen, ), ], @@ -1029,6 +1039,7 @@ class _PlexVideoControlsState extends State children: [ AppBarBackButton( style: BackButtonStyle.video, + semanticLabel: t.videoControls.backButton, onPressed: () => Navigator.of(context).pop(true), ), const SizedBox(width: 16), @@ -1094,16 +1105,17 @@ class _PlexVideoControlsState extends State shape: BoxShape.circle, ), child: IconButton( - icon: Icon( - _getReplayIcon(_seekTimeSmall), - color: Colors.white, - size: 48, + icon: Icon( + _getReplayIcon(_seekTimeSmall), + color: Colors.white, + size: 48, + semanticLabel: t.videoControls.seekBackwardButton(seconds: _seekTimeSmall), + ), + iconSize: 48, + onPressed: () { + _seekWithClamping(Duration(seconds: -_seekTimeSmall)); + }, ), - iconSize: 48, - onPressed: () { - _seekWithClamping(Duration(seconds: -_seekTimeSmall)); - }, - ), ), const SizedBox(width: 48), Container( @@ -1112,22 +1124,23 @@ class _PlexVideoControlsState extends State shape: BoxShape.circle, ), child: IconButton( - icon: Icon( - isPlaying ? Icons.pause : Icons.play_arrow, - color: Colors.white, - size: 72, + icon: Icon( + isPlaying ? Icons.pause : Icons.play_arrow, + color: Colors.white, + size: 72, + semanticLabel: isPlaying ? t.videoControls.pauseButton : t.videoControls.playButton, + ), + iconSize: 72, + onPressed: () { + if (isPlaying) { + widget.player.pause(); + _hideTimer?.cancel(); // Cancel auto-hide when paused + } else { + widget.player.play(); + _startHideTimer(); // Start auto-hide when playing + } + }, ), - iconSize: 72, - onPressed: () { - if (isPlaying) { - widget.player.pause(); - _hideTimer?.cancel(); // Cancel auto-hide when paused - } else { - widget.player.play(); - _startHideTimer(); // Start auto-hide when playing - } - }, - ), ), const SizedBox(width: 48), Container( @@ -1136,16 +1149,17 @@ class _PlexVideoControlsState extends State shape: BoxShape.circle, ), child: IconButton( - icon: Icon( - _getForwardIcon(_seekTimeSmall), - color: Colors.white, - size: 48, + icon: Icon( + _getForwardIcon(_seekTimeSmall), + color: Colors.white, + size: 48, + semanticLabel: t.videoControls.seekForwardButton(seconds: _seekTimeSmall), + ), + iconSize: 48, + onPressed: () { + _seekWithClamping(Duration(seconds: _seekTimeSmall)); + }, ), - iconSize: 48, - onPressed: () { - _seekWithClamping(Duration(seconds: _seekTimeSmall)); - }, - ), ), ], ); @@ -1234,6 +1248,7 @@ class _PlexVideoControlsState extends State children: [ AppBarBackButton( style: BackButtonStyle.video, + semanticLabel: t.videoControls.backButton, onPressed: () => Navigator.of(context).pop(true), ), const SizedBox(width: 16), @@ -1360,24 +1375,28 @@ class _PlexVideoControlsState extends State children: [ // Previous item IconButton( - icon: Icon( - Icons.skip_previous, - color: widget.onPrevious != null - ? Colors.white - : Colors.white54, + icon: Icon( + Icons.skip_previous, + color: widget.onPrevious != null + ? Colors.white + : Colors.white54, + semanticLabel: t.videoControls.previousButton, + ), + onPressed: widget.onPrevious, ), - onPressed: widget.onPrevious, - ), // Previous chapter (or skip backward if no chapters) IconButton( - icon: Icon( - _chapters.isEmpty - ? _getReplayIcon(_seekTimeSmall) - : Icons.fast_rewind, - color: Colors.white, + icon: Icon( + _chapters.isEmpty + ? _getReplayIcon(_seekTimeSmall) + : Icons.fast_rewind, + color: Colors.white, + semanticLabel: _chapters.isEmpty + ? t.videoControls.seekBackwardButton(seconds: _seekTimeSmall) + : t.videoControls.previousChapterButton, + ), + onPressed: _seekToPreviousChapter, ), - onPressed: _seekToPreviousChapter, - ), // Play/Pause StreamBuilder( stream: widget.player.stream.playing, @@ -1385,41 +1404,46 @@ class _PlexVideoControlsState extends State builder: (context, snapshot) { final isPlaying = snapshot.data ?? false; return IconButton( - icon: Icon( - isPlaying ? Icons.pause : Icons.play_arrow, - color: Colors.white, - size: 32, - ), - iconSize: 32, - onPressed: () { - if (isPlaying) { - widget.player.pause(); - _hideTimer?.cancel(); // Cancel auto-hide when paused - } else { - widget.player.play(); - _startHideTimer(); // Start auto-hide when playing - } - }, + icon: Icon( + isPlaying ? Icons.pause : Icons.play_arrow, + color: Colors.white, + size: 32, + semanticLabel: isPlaying ? t.videoControls.pauseButton : t.videoControls.playButton, + ), + iconSize: 32, + onPressed: () { + if (isPlaying) { + widget.player.pause(); + _hideTimer?.cancel(); // Cancel auto-hide when paused + } else { + widget.player.play(); + _startHideTimer(); // Start auto-hide when playing + } + }, ); }, ), // Next chapter (or skip forward if no chapters) IconButton( - icon: Icon( - _chapters.isEmpty - ? _getForwardIcon(_seekTimeSmall) - : Icons.fast_forward, - color: Colors.white, + icon: Icon( + _chapters.isEmpty + ? _getForwardIcon(_seekTimeSmall) + : Icons.fast_forward, + color: Colors.white, + semanticLabel: _chapters.isEmpty + ? t.videoControls.seekForwardButton(seconds: _seekTimeSmall) + : t.videoControls.nextChapterButton, + ), + onPressed: _seekToNextChapter, ), - onPressed: _seekToNextChapter, - ), // Next item IconButton( - icon: Icon( - Icons.skip_next, - color: widget.onNext != null ? Colors.white : Colors.white54, - ), - onPressed: widget.onNext, + icon: Icon( + Icons.skip_next, + semanticLabel: t.videoControls.nextButton, + color: widget.onNext != null ? Colors.white : Colors.white54, + ), + onPressed: widget.onNext, ), const Spacer(), // Volume control @@ -1478,20 +1502,24 @@ class _PlexVideoControlsState extends State ), ), // Slider - Slider( - value: duration.inMilliseconds > 0 - ? position.inMilliseconds.toDouble() - : 0.0, - min: 0.0, - max: duration.inMilliseconds.toDouble(), - onChanged: (value) { - _throttledSeek(Duration(milliseconds: value.toInt())); - }, - onChangeEnd: (value) { - _finalizeSeek(Duration(milliseconds: value.toInt())); - }, - activeColor: Colors.white, - inactiveColor: Colors.white.withValues(alpha: 0.3), + Semantics( + label: t.videoControls.timelineSlider, + slider: true, + child: Slider( + value: duration.inMilliseconds > 0 + ? position.inMilliseconds.toDouble() + : 0.0, + min: 0.0, + max: duration.inMilliseconds.toDouble(), + onChanged: (value) { + _throttledSeek(Duration(milliseconds: value.toInt())); + }, + onChangeEnd: (value) { + _finalizeSeek(Duration(milliseconds: value.toInt())); + }, + activeColor: Colors.white, + inactiveColor: Colors.white.withValues(alpha: 0.3), + ), ), // Chapter marker indicators if (_chaptersLoaded && @@ -1529,6 +1557,7 @@ class _PlexVideoControlsState extends State icon: Icon( isMuted ? Icons.volume_off : Icons.volume_up, color: Colors.white, + semanticLabel: isMuted ? t.videoControls.unmuteButton : t.videoControls.muteButton, ), onPressed: () async { final newVolume = isMuted ? 100.0 : 0.0; @@ -1552,19 +1581,23 @@ class _PlexVideoControlsState extends State overlayRadius: 12, ), ), - child: Slider( - value: volume, - min: 0.0, - max: 100.0, - onChanged: (value) { - widget.player.setVolume(value); - }, - onChangeEnd: (value) async { - final settings = await SettingsService.getInstance(); - await settings.setVolume(value); - }, - activeColor: Colors.white, - inactiveColor: Colors.white.withValues(alpha: 0.3), + child: Semantics( + label: t.videoControls.volumeSlider, + slider: true, + child: Slider( + value: volume, + min: 0.0, + max: 100.0, + onChanged: (value) { + widget.player.setVolume(value); + }, + onChangeEnd: (value) async { + final settings = await SettingsService.getInstance(); + await settings.setVolume(value); + }, + activeColor: Colors.white, + inactiveColor: Colors.white.withValues(alpha: 0.3), + ), ), ), ),