Added semantic labels to media player controls

This commit is contained in:
Manuel Cortez
2025-11-15 22:17:04 -06:00
parent 3f4d5c2923
commit 931bc9b931
+134 -101
View File
@@ -454,6 +454,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
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<PlexVideoControls>
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<PlexVideoControls>
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<PlexVideoControls>
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<PlexVideoControls>
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<PlexVideoControls>
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<PlexVideoControls>
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<PlexVideoControls>
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<PlexVideoControls>
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<PlexVideoControls>
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<PlexVideoControls>
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<PlexVideoControls>
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<PlexVideoControls>
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<PlexVideoControls>
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<bool>(
stream: widget.player.stream.playing,
@@ -1385,41 +1404,46 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
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<PlexVideoControls>
),
),
// 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<PlexVideoControls>
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<PlexVideoControls>
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),
),
),
),
),