From 9af87d2e020fcf6a6f3e9869475d854e42c3d8b6 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Tue, 18 Nov 2025 14:44:00 +0100 Subject: [PATCH] refactor: wrap buttons in semantics --- lib/widgets/app_bar_back_button.dart | 6 +- lib/widgets/media_card.dart | 7 +- .../video_controls/video_control_button.dart | 13 +- .../video_controls/video_controls.dart | 118 +++++++++++++----- 4 files changed, 101 insertions(+), 43 deletions(-) diff --git a/lib/widgets/app_bar_back_button.dart b/lib/widgets/app_bar_back_button.dart index 7a46a585..979c350e 100644 --- a/lib/widgets/app_bar_back_button.dart +++ b/lib/widgets/app_bar_back_button.dart @@ -152,11 +152,7 @@ class _AppBarBackButtonState extends State color: currentColor, shape: BoxShape.circle, ), - child: Icon( - Icons.arrow_back, - color: effectiveColor, - size: 20, - ), + child: Icon(Icons.arrow_back, color: effectiveColor, size: 20), ); }, ), diff --git a/lib/widgets/media_card.dart b/lib/widgets/media_card.dart index cd814ae3..629a33b9 100644 --- a/lib/widgets/media_card.dart +++ b/lib/widgets/media_card.dart @@ -80,9 +80,12 @@ class _MediaCardState extends State { // Add watched status if (item.isWatched) { baseLabel = '$baseLabel, ${t.accessibility.mediaCardWatched}'; - } else if (item.viewOffset != null && item.duration != null && item.viewOffset! > 0) { + } else if (item.viewOffset != null && + item.duration != null && + item.viewOffset! > 0) { final percent = ((item.viewOffset! / item.duration!) * 100).round(); - baseLabel = '$baseLabel, ${t.accessibility.mediaCardPartiallyWatched(percent: percent)}'; + baseLabel = + '$baseLabel, ${t.accessibility.mediaCardPartiallyWatched(percent: percent)}'; } else { baseLabel = '$baseLabel, ${t.accessibility.mediaCardUnwatched}'; } diff --git a/lib/widgets/video_controls/video_control_button.dart b/lib/widgets/video_controls/video_control_button.dart index b492d66b..e48fd441 100644 --- a/lib/widgets/video_controls/video_control_button.dart +++ b/lib/widgets/video_controls/video_control_button.dart @@ -41,11 +41,20 @@ class VideoControlButton extends StatelessWidget { // Determine the effective color: explicit color > active amber > default white final effectiveColor = color ?? (isActive ? Colors.amber : Colors.white); - return IconButton( - icon: Icon(icon, color: effectiveColor, semanticLabel: semanticLabel), + final button = IconButton( + icon: Icon(icon, color: effectiveColor), onPressed: onPressed, tooltip: tooltip, constraints: const BoxConstraints(minWidth: 40, minHeight: 40), ); + + return semanticLabel != null + ? Semantics( + label: semanticLabel, + button: true, + excludeSemantics: true, + child: button, + ) + : button; } } diff --git a/lib/widgets/video_controls/video_controls.dart b/lib/widgets/video_controls/video_controls.dart index b6996c9c..be939e5f 100644 --- a/lib/widgets/video_controls/video_controls.dart +++ b/lib/widgets/video_controls/video_controls.dart @@ -1105,18 +1105,24 @@ class _PlexVideoControlsState extends State color: Colors.black.withValues(alpha: 0.5), shape: BoxShape.circle, ), - child: IconButton( + child: Semantics( + label: t.videoControls.seekBackwardButton( + seconds: _seekTimeSmall, + ), + button: true, + excludeSemantics: true, + child: IconButton( icon: Icon( _getReplayIcon(_seekTimeSmall), color: Colors.white, size: 48, - semanticLabel: t.videoControls.seekBackwardButton(seconds: _seekTimeSmall), ), iconSize: 48, onPressed: () { _seekWithClamping(Duration(seconds: -_seekTimeSmall)); }, ), + ), ), const SizedBox(width: 48), Container( @@ -1124,12 +1130,17 @@ class _PlexVideoControlsState extends State color: Colors.black.withValues(alpha: 0.5), shape: BoxShape.circle, ), - child: IconButton( + child: Semantics( + label: isPlaying + ? t.videoControls.pauseButton + : t.videoControls.playButton, + button: true, + excludeSemantics: true, + child: IconButton( icon: Icon( isPlaying ? Icons.pause : Icons.play_arrow, color: Colors.white, size: 72, - semanticLabel: isPlaying ? t.videoControls.pauseButton : t.videoControls.playButton, ), iconSize: 72, onPressed: () { @@ -1142,6 +1153,7 @@ class _PlexVideoControlsState extends State } }, ), + ), ), const SizedBox(width: 48), Container( @@ -1149,18 +1161,24 @@ class _PlexVideoControlsState extends State color: Colors.black.withValues(alpha: 0.5), shape: BoxShape.circle, ), - child: IconButton( + child: Semantics( + label: t.videoControls.seekForwardButton( + seconds: _seekTimeSmall, + ), + button: true, + excludeSemantics: true, + child: IconButton( icon: Icon( _getForwardIcon(_seekTimeSmall), color: Colors.white, size: 48, - semanticLabel: t.videoControls.seekForwardButton(seconds: _seekTimeSmall), ), iconSize: 48, onPressed: () { _seekWithClamping(Duration(seconds: _seekTimeSmall)); }, ), + ), ), ], ); @@ -1375,41 +1393,56 @@ class _PlexVideoControlsState extends State Row( children: [ // Previous item - IconButton( + Semantics( + label: t.videoControls.previousButton, + button: true, + excludeSemantics: true, + child: IconButton( icon: Icon( Icons.skip_previous, color: widget.onPrevious != null ? Colors.white : Colors.white54, - semanticLabel: t.videoControls.previousButton, ), onPressed: widget.onPrevious, ), + ), // Previous chapter (or skip backward if no chapters) - IconButton( + Semantics( + label: _chapters.isEmpty + ? t.videoControls.seekBackwardButton( + seconds: _seekTimeSmall, + ) + : t.videoControls.previousChapterButton, + button: true, + excludeSemantics: true, + child: IconButton( 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, ), + ), // Play/Pause StreamBuilder( stream: widget.player.stream.playing, initialData: widget.player.state.playing, builder: (context, snapshot) { final isPlaying = snapshot.data ?? false; - return IconButton( + return Semantics( + label: isPlaying + ? t.videoControls.pauseButton + : t.videoControls.playButton, + button: true, + excludeSemantics: true, + child: IconButton( icon: Icon( isPlaying ? Icons.pause : Icons.play_arrow, color: Colors.white, size: 32, - semanticLabel: isPlaying ? t.videoControls.pauseButton : t.videoControls.playButton, ), iconSize: 32, onPressed: () { @@ -1421,30 +1454,41 @@ class _PlexVideoControlsState extends State _startHideTimer(); // Start auto-hide when playing } }, + ), ); }, ), // Next chapter (or skip forward if no chapters) - IconButton( + Semantics( + label: _chapters.isEmpty + ? t.videoControls.seekForwardButton(seconds: _seekTimeSmall) + : t.videoControls.nextChapterButton, + button: true, + excludeSemantics: true, + child: IconButton( 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, ), + ), // Next item - IconButton( + Semantics( + label: t.videoControls.nextButton, + button: true, + excludeSemantics: true, + child: IconButton( icon: Icon( Icons.skip_next, - semanticLabel: t.videoControls.nextButton, - color: widget.onNext != null ? Colors.white : Colors.white54, + color: widget.onNext != null + ? Colors.white + : Colors.white54, ), onPressed: widget.onNext, + ), ), const Spacer(), // Volume control @@ -1554,20 +1598,26 @@ class _PlexVideoControlsState extends State return Row( mainAxisSize: MainAxisSize.min, children: [ - IconButton( - icon: Icon( - isMuted ? Icons.volume_off : Icons.volume_up, - color: Colors.white, - semanticLabel: isMuted ? t.videoControls.unmuteButton : t.videoControls.muteButton, + Semantics( + label: isMuted + ? t.videoControls.unmuteButton + : t.videoControls.muteButton, + button: true, + excludeSemantics: true, + child: IconButton( + icon: Icon( + isMuted ? Icons.volume_off : Icons.volume_up, + color: Colors.white, + ), + onPressed: () async { + final newVolume = isMuted ? 100.0 : 0.0; + widget.player.setVolume(newVolume); + final settings = await SettingsService.getInstance(); + await settings.setVolume(newVolume); + }, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), ), - onPressed: () async { - final newVolume = isMuted ? 100.0 : 0.0; - widget.player.setVolume(newVolume); - final settings = await SettingsService.getInstance(); - await settings.setVolume(newVolume); - }, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), ), const SizedBox(width: 8), SizedBox(