Added semantic labels to media player controls
This commit is contained in:
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user