From 1220dca88e39a4ea0c02d77d41ff0e1278250637 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Sat, 15 Nov 2025 22:05:41 -0600 Subject: [PATCH 1/8] Prepared translation files with new addition for accessibility enhancements --- lib/i18n/strings.g.dart | 74 ++++++++++++++++++++++++++++++++++- lib/i18n/strings.i18n.json | 34 +++++++++++++++- lib/i18n/strings_de.i18n.json | 34 +++++++++++++++- lib/i18n/strings_it.i18n.json | 34 +++++++++++++++- lib/i18n/strings_nl.i18n.json | 34 +++++++++++++++- lib/i18n/strings_sv.i18n.json | 34 +++++++++++++++- lib/i18n/strings_zh.i18n.json | 34 +++++++++++++++- 7 files changed, 270 insertions(+), 8 deletions(-) diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index d95497f8..054c088c 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 6 -/// Strings: 2130 (355 per locale) +/// Strings: 2160 (360 per locale) /// -/// Built on 2025-11-15 at 11:48 UTC +/// Built on 2025-11-16 at 03:49 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -163,6 +163,7 @@ class Translations implements BaseTranslations { late final _StringsPinEntryEn pinEntry = _StringsPinEntryEn._(_root); late final _StringsFileInfoEn fileInfo = _StringsFileInfoEn._(_root); late final _StringsMediaMenuEn mediaMenu = _StringsMediaMenuEn._(_root); + late final _StringsAccessibilityEn accessibility = _StringsAccessibilityEn._(_root); late final _StringsTooltipsEn tooltips = _StringsTooltipsEn._(_root); late final _StringsVideoControlsEn videoControls = _StringsVideoControlsEn._(_root); late final _StringsUserStatusEn userStatus = _StringsUserStatusEn._(_root); @@ -441,6 +442,22 @@ class _StringsMediaMenuEn { String get fileInfo => 'File Info'; } +// Path: accessibility +class _StringsAccessibilityEn { + _StringsAccessibilityEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String mediaCardMovie({required Object title}) => '${title}, movie'; + String mediaCardShow({required Object title}) => '${title}, TV show'; + String mediaCardEpisode({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}'; + String mediaCardSeason({required Object title, required Object seasonInfo}) => '${title}, ${seasonInfo}'; + String get mediaCardWatched => 'watched'; + String mediaCardPartiallyWatched({required Object percent}) => '${percent} percent watched'; + String get mediaCardUnwatched => 'unwatched'; +} + // Path: tooltips class _StringsTooltipsEn { _StringsTooltipsEn._(this._root); @@ -473,6 +490,29 @@ class _StringsVideoControlsEn { String get stretch => 'Stretch'; String get lockRotation => 'Lock rotation'; String get unlockRotation => 'Unlock rotation'; + String get playButton => 'Play'; + String get pauseButton => 'Pause'; + String seekBackwardButton({required Object seconds}) => 'Seek backward ${seconds} seconds'; + String seekForwardButton({required Object seconds}) => 'Seek forward ${seconds} seconds'; + String get previousButton => 'Previous episode'; + String get nextButton => 'Next episode'; + String get previousChapterButton => 'Previous chapter'; + String get nextChapterButton => 'Next chapter'; + String get volumeButton => 'Volume'; + String get muteButton => 'Mute'; + String get unmuteButton => 'Unmute'; + String get settingsButton => 'Video settings'; + String get audioTrackButton => 'Audio tracks'; + String get subtitlesButton => 'Subtitles'; + String get chaptersButton => 'Chapters'; + String get versionsButton => 'Video versions'; + String get aspectRatioButton => 'Aspect ratio'; + String get fullscreenButton => 'Enter fullscreen'; + String get exitFullscreenButton => 'Exit fullscreen'; + String get rotationLockButton => 'Rotation lock'; + String get timelineSlider => 'Video timeline'; + String get volumeSlider => 'Volume level'; + String get backButton => 'Back'; } // Path: userStatus @@ -4260,6 +4300,13 @@ extension on Translations { case 'mediaMenu.goToSeason': return 'Go to season'; case 'mediaMenu.shufflePlay': return 'Shuffle Play'; case 'mediaMenu.fileInfo': return 'File Info'; + case 'accessibility.mediaCardMovie': return ({required Object title}) => '${title}, movie'; + case 'accessibility.mediaCardShow': return ({required Object title}) => '${title}, TV show'; + case 'accessibility.mediaCardEpisode': return ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}'; + case 'accessibility.mediaCardSeason': return ({required Object title, required Object seasonInfo}) => '${title}, ${seasonInfo}'; + case 'accessibility.mediaCardWatched': return 'watched'; + case 'accessibility.mediaCardPartiallyWatched': return ({required Object percent}) => '${percent} percent watched'; + case 'accessibility.mediaCardUnwatched': return 'unwatched'; case 'tooltips.shufflePlay': return 'Shuffle play'; case 'tooltips.markAsWatched': return 'Mark as watched'; case 'tooltips.markAsUnwatched': return 'Mark as unwatched'; @@ -4276,6 +4323,29 @@ extension on Translations { case 'videoControls.stretch': return 'Stretch'; case 'videoControls.lockRotation': return 'Lock rotation'; case 'videoControls.unlockRotation': return 'Unlock rotation'; + case 'videoControls.playButton': return 'Play'; + case 'videoControls.pauseButton': return 'Pause'; + case 'videoControls.seekBackwardButton': return ({required Object seconds}) => 'Seek backward ${seconds} seconds'; + case 'videoControls.seekForwardButton': return ({required Object seconds}) => 'Seek forward ${seconds} seconds'; + case 'videoControls.previousButton': return 'Previous episode'; + case 'videoControls.nextButton': return 'Next episode'; + case 'videoControls.previousChapterButton': return 'Previous chapter'; + case 'videoControls.nextChapterButton': return 'Next chapter'; + case 'videoControls.volumeButton': return 'Volume'; + case 'videoControls.muteButton': return 'Mute'; + case 'videoControls.unmuteButton': return 'Unmute'; + case 'videoControls.settingsButton': return 'Video settings'; + case 'videoControls.audioTrackButton': return 'Audio tracks'; + case 'videoControls.subtitlesButton': return 'Subtitles'; + case 'videoControls.chaptersButton': return 'Chapters'; + case 'videoControls.versionsButton': return 'Video versions'; + case 'videoControls.aspectRatioButton': return 'Aspect ratio'; + case 'videoControls.fullscreenButton': return 'Enter fullscreen'; + case 'videoControls.exitFullscreenButton': return 'Exit fullscreen'; + case 'videoControls.rotationLockButton': return 'Rotation lock'; + case 'videoControls.timelineSlider': return 'Video timeline'; + case 'videoControls.volumeSlider': return 'Volume level'; + case 'videoControls.backButton': return 'Back'; case 'userStatus.admin': return 'Admin'; case 'userStatus.restricted': return 'Restricted'; case 'userStatus.protected': return 'Protected'; diff --git a/lib/i18n/strings.i18n.json b/lib/i18n/strings.i18n.json index e57bc0fe..0184a993 100644 --- a/lib/i18n/strings.i18n.json +++ b/lib/i18n/strings.i18n.json @@ -181,6 +181,15 @@ "shufflePlay": "Shuffle Play", "fileInfo": "File Info" }, + "accessibility": { + "mediaCardMovie": "${title}, movie", + "mediaCardShow": "${title}, TV show", + "mediaCardEpisode": "${title}, ${episodeInfo}", + "mediaCardSeason": "${title}, ${seasonInfo}", + "mediaCardWatched": "watched", + "mediaCardPartiallyWatched": "${percent} percent watched", + "mediaCardUnwatched": "unwatched" + }, "tooltips": { "shufflePlay": "Shuffle play", "markAsWatched": "Mark as watched", @@ -199,7 +208,30 @@ "fillScreen": "Fill screen", "stretch": "Stretch", "lockRotation": "Lock rotation", - "unlockRotation": "Unlock rotation" + "unlockRotation": "Unlock rotation", + "playButton": "Play", + "pauseButton": "Pause", + "seekBackwardButton": "Seek backward ${seconds} seconds", + "seekForwardButton": "Seek forward ${seconds} seconds", + "previousButton": "Previous episode", + "nextButton": "Next episode", + "previousChapterButton": "Previous chapter", + "nextChapterButton": "Next chapter", + "volumeButton": "Volume", + "muteButton": "Mute", + "unmuteButton": "Unmute", + "settingsButton": "Video settings", + "audioTrackButton": "Audio tracks", + "subtitlesButton": "Subtitles", + "chaptersButton": "Chapters", + "versionsButton": "Video versions", + "aspectRatioButton": "Aspect ratio", + "fullscreenButton": "Enter fullscreen", + "exitFullscreenButton": "Exit fullscreen", + "rotationLockButton": "Rotation lock", + "timelineSlider": "Video timeline", + "volumeSlider": "Volume level", + "backButton": "Back" }, "userStatus": { "admin": "Admin", diff --git a/lib/i18n/strings_de.i18n.json b/lib/i18n/strings_de.i18n.json index e78c5411..0865ffbd 100644 --- a/lib/i18n/strings_de.i18n.json +++ b/lib/i18n/strings_de.i18n.json @@ -181,6 +181,15 @@ "shufflePlay": "Zufallswiedergabe", "fileInfo": "Dateiinfo" }, + "accessibility": { + "mediaCardMovie": "${title}, movie", + "mediaCardShow": "${title}, TV show", + "mediaCardEpisode": "${title}, ${episodeInfo}", + "mediaCardSeason": "${title}, ${seasonInfo}", + "mediaCardWatched": "watched", + "mediaCardPartiallyWatched": "${percent} percent watched", + "mediaCardUnwatched": "unwatched" + }, "tooltips": { "shufflePlay": "Zufallswiedergabe", "markAsWatched": "Als gesehen markieren", @@ -199,7 +208,30 @@ "fillScreen": "Bild füllen", "stretch": "Strecken", "lockRotation": "Rotation sperren", - "unlockRotation": "Rotation entsperren" + "unlockRotation": "Rotation entsperren", + "playButton": "Play", + "pauseButton": "Pause", + "seekBackwardButton": "Seek backward ${seconds} seconds", + "seekForwardButton": "Seek forward ${seconds} seconds", + "previousButton": "Previous episode", + "nextButton": "Next episode", + "previousChapterButton": "Previous chapter", + "nextChapterButton": "Next chapter", + "volumeButton": "Volume", + "muteButton": "Mute", + "unmuteButton": "Unmute", + "settingsButton": "Video settings", + "audioTrackButton": "Audio tracks", + "subtitlesButton": "Subtitles", + "chaptersButton": "Chapters", + "versionsButton": "Video versions", + "aspectRatioButton": "Aspect ratio", + "fullscreenButton": "Enter fullscreen", + "exitFullscreenButton": "Exit fullscreen", + "rotationLockButton": "Rotation lock", + "timelineSlider": "Video timeline", + "volumeSlider": "Volume level", + "backButton": "Back" }, "userStatus": { "admin": "Eigentümer", diff --git a/lib/i18n/strings_it.i18n.json b/lib/i18n/strings_it.i18n.json index 04a05e14..a8bd7c10 100644 --- a/lib/i18n/strings_it.i18n.json +++ b/lib/i18n/strings_it.i18n.json @@ -181,6 +181,15 @@ "shufflePlay": "Riproduzione casuale", "fileInfo": "Info sul file" }, + "accessibility": { + "mediaCardMovie": "${title}, movie", + "mediaCardShow": "${title}, TV show", + "mediaCardEpisode": "${title}, ${episodeInfo}", + "mediaCardSeason": "${title}, ${seasonInfo}", + "mediaCardWatched": "watched", + "mediaCardPartiallyWatched": "${percent} percent watched", + "mediaCardUnwatched": "unwatched" + }, "tooltips": { "shufflePlay": "Riproduzione casuale", "markAsWatched": "Segna come visto", @@ -199,7 +208,30 @@ "fillScreen": "Riempi schermo", "stretch": "Allunga", "lockRotation": "Blocca rotazione", - "unlockRotation": "Sblocca rotazione" + "unlockRotation": "Sblocca rotazione", + "playButton": "Play", + "pauseButton": "Pause", + "seekBackwardButton": "Seek backward ${seconds} seconds", + "seekForwardButton": "Seek forward ${seconds} seconds", + "previousButton": "Previous episode", + "nextButton": "Next episode", + "previousChapterButton": "Previous chapter", + "nextChapterButton": "Next chapter", + "volumeButton": "Volume", + "muteButton": "Mute", + "unmuteButton": "Unmute", + "settingsButton": "Video settings", + "audioTrackButton": "Audio tracks", + "subtitlesButton": "Subtitles", + "chaptersButton": "Chapters", + "versionsButton": "Video versions", + "aspectRatioButton": "Aspect ratio", + "fullscreenButton": "Enter fullscreen", + "exitFullscreenButton": "Exit fullscreen", + "rotationLockButton": "Rotation lock", + "timelineSlider": "Video timeline", + "volumeSlider": "Volume level", + "backButton": "Back" }, "userStatus": { "admin": "Admin", diff --git a/lib/i18n/strings_nl.i18n.json b/lib/i18n/strings_nl.i18n.json index bced748e..5fd3c65a 100644 --- a/lib/i18n/strings_nl.i18n.json +++ b/lib/i18n/strings_nl.i18n.json @@ -181,6 +181,15 @@ "shufflePlay": "Willekeurig afspelen", "fileInfo": "Bestand info" }, + "accessibility": { + "mediaCardMovie": "${title}, movie", + "mediaCardShow": "${title}, TV show", + "mediaCardEpisode": "${title}, ${episodeInfo}", + "mediaCardSeason": "${title}, ${seasonInfo}", + "mediaCardWatched": "watched", + "mediaCardPartiallyWatched": "${percent} percent watched", + "mediaCardUnwatched": "unwatched" + }, "tooltips": { "shufflePlay": "Willekeurig afspelen", "markAsWatched": "Markeer als gekeken", @@ -199,7 +208,30 @@ "fillScreen": "Vul scherm", "stretch": "Uitrekken", "lockRotation": "Vergrendel rotatie", - "unlockRotation": "Ontgrendel rotatie" + "unlockRotation": "Ontgrendel rotatie", + "playButton": "Play", + "pauseButton": "Pause", + "seekBackwardButton": "Seek backward ${seconds} seconds", + "seekForwardButton": "Seek forward ${seconds} seconds", + "previousButton": "Previous episode", + "nextButton": "Next episode", + "previousChapterButton": "Previous chapter", + "nextChapterButton": "Next chapter", + "volumeButton": "Volume", + "muteButton": "Mute", + "unmuteButton": "Unmute", + "settingsButton": "Video settings", + "audioTrackButton": "Audio tracks", + "subtitlesButton": "Subtitles", + "chaptersButton": "Chapters", + "versionsButton": "Video versions", + "aspectRatioButton": "Aspect ratio", + "fullscreenButton": "Enter fullscreen", + "exitFullscreenButton": "Exit fullscreen", + "rotationLockButton": "Rotation lock", + "timelineSlider": "Video timeline", + "volumeSlider": "Volume level", + "backButton": "Back" }, "userStatus": { "admin": "Beheerder", diff --git a/lib/i18n/strings_sv.i18n.json b/lib/i18n/strings_sv.i18n.json index b97094d9..e47b4e04 100644 --- a/lib/i18n/strings_sv.i18n.json +++ b/lib/i18n/strings_sv.i18n.json @@ -181,6 +181,15 @@ "shufflePlay": "Blanda uppspelning", "fileInfo": "Filinformation" }, + "accessibility": { + "mediaCardMovie": "${title}, movie", + "mediaCardShow": "${title}, TV show", + "mediaCardEpisode": "${title}, ${episodeInfo}", + "mediaCardSeason": "${title}, ${seasonInfo}", + "mediaCardWatched": "watched", + "mediaCardPartiallyWatched": "${percent} percent watched", + "mediaCardUnwatched": "unwatched" + }, "tooltips": { "shufflePlay": "Blanda uppspelning", "markAsWatched": "Markera som sedd", @@ -199,7 +208,30 @@ "fillScreen": "Fyll skärm", "stretch": "Sträck", "lockRotation": "Lås rotation", - "unlockRotation": "Lås upp rotation" + "unlockRotation": "Lås upp rotation", + "playButton": "Play", + "pauseButton": "Pause", + "seekBackwardButton": "Seek backward ${seconds} seconds", + "seekForwardButton": "Seek forward ${seconds} seconds", + "previousButton": "Previous episode", + "nextButton": "Next episode", + "previousChapterButton": "Previous chapter", + "nextChapterButton": "Next chapter", + "volumeButton": "Volume", + "muteButton": "Mute", + "unmuteButton": "Unmute", + "settingsButton": "Video settings", + "audioTrackButton": "Audio tracks", + "subtitlesButton": "Subtitles", + "chaptersButton": "Chapters", + "versionsButton": "Video versions", + "aspectRatioButton": "Aspect ratio", + "fullscreenButton": "Enter fullscreen", + "exitFullscreenButton": "Exit fullscreen", + "rotationLockButton": "Rotation lock", + "timelineSlider": "Video timeline", + "volumeSlider": "Volume level", + "backButton": "Back" }, "userStatus": { "admin": "Admin", diff --git a/lib/i18n/strings_zh.i18n.json b/lib/i18n/strings_zh.i18n.json index 8daa0990..a3218bd7 100644 --- a/lib/i18n/strings_zh.i18n.json +++ b/lib/i18n/strings_zh.i18n.json @@ -181,6 +181,15 @@ "shufflePlay": "随机播放", "fileInfo": "文件信息" }, + "accessibility": { + "mediaCardMovie": "${title}, movie", + "mediaCardShow": "${title}, TV show", + "mediaCardEpisode": "${title}, ${episodeInfo}", + "mediaCardSeason": "${title}, ${seasonInfo}", + "mediaCardWatched": "watched", + "mediaCardPartiallyWatched": "${percent} percent watched", + "mediaCardUnwatched": "unwatched" + }, "tooltips": { "shufflePlay": "随机播放", "markAsWatched": "标记为已观看", @@ -199,7 +208,30 @@ "fillScreen": "填充屏幕", "stretch": "拉伸", "lockRotation": "锁定旋转", - "unlockRotation": "解锁旋转" + "unlockRotation": "解锁旋转", + "playButton": "Play", + "pauseButton": "Pause", + "seekBackwardButton": "Seek backward ${seconds} seconds", + "seekForwardButton": "Seek forward ${seconds} seconds", + "previousButton": "Previous episode", + "nextButton": "Next episode", + "previousChapterButton": "Previous chapter", + "nextChapterButton": "Next chapter", + "volumeButton": "Volume", + "muteButton": "Mute", + "unmuteButton": "Unmute", + "settingsButton": "Video settings", + "audioTrackButton": "Audio tracks", + "subtitlesButton": "Subtitles", + "chaptersButton": "Chapters", + "versionsButton": "Video versions", + "aspectRatioButton": "Aspect ratio", + "fullscreenButton": "Enter fullscreen", + "exitFullscreenButton": "Exit fullscreen", + "rotationLockButton": "Rotation lock", + "timelineSlider": "Video timeline", + "volumeSlider": "Volume level", + "backButton": "Back" }, "userStatus": { "admin": "管理员", From 3f4d5c2923055d1be518214049b7bdde1052e3d6 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Sat, 15 Nov 2025 22:07:30 -0600 Subject: [PATCH 2/8] video_control_button: sets a semantic label properly for IconButtons --- lib/widgets/video_controls/video_control_button.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/widgets/video_controls/video_control_button.dart b/lib/widgets/video_controls/video_control_button.dart index 0b0f6b3a..b492d66b 100644 --- a/lib/widgets/video_controls/video_control_button.dart +++ b/lib/widgets/video_controls/video_control_button.dart @@ -18,6 +18,10 @@ class VideoControlButton extends StatelessWidget { /// Optional tooltip text shown on hover or long press. final String? tooltip; + /// Optional semantic label for screen readers. + /// If not provided, falls back to tooltip. + final String? semanticLabel; + /// Whether this button represents an active state (e.g., a feature is enabled). /// When true, the icon color defaults to amber instead of white. final bool isActive; @@ -28,6 +32,7 @@ class VideoControlButton extends StatelessWidget { required this.onPressed, this.color, this.tooltip, + this.semanticLabel, this.isActive = false, }); @@ -37,7 +42,7 @@ class VideoControlButton extends StatelessWidget { final effectiveColor = color ?? (isActive ? Colors.amber : Colors.white); return IconButton( - icon: Icon(icon, color: effectiveColor), + icon: Icon(icon, color: effectiveColor, semanticLabel: semanticLabel), onPressed: onPressed, tooltip: tooltip, constraints: const BoxConstraints(minWidth: 40, minHeight: 40), From 931bc9b93108b140ab9e8fe5347c230ecd74edf8 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Sat, 15 Nov 2025 22:17:04 -0600 Subject: [PATCH 3/8] 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), + ), ), ), ), From c6bd1ef8674efeccc23471498a98464c52e3c27f Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Sat, 15 Nov 2025 22:22:24 -0600 Subject: [PATCH 4/8] Added missing translation file --- lib/i18n/strings.g.dart | 354 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 352 insertions(+), 2 deletions(-) diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index 054c088c..2df8c7fa 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 6 -/// Strings: 2160 (360 per locale) +/// Strings: 2310 (385 per locale) /// -/// Built on 2025-11-16 at 03:49 UTC +/// Built on 2025-11-16 at 04:20 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -864,6 +864,7 @@ class _StringsDe implements Translations { @override late final _StringsPinEntryDe pinEntry = _StringsPinEntryDe._(_root); @override late final _StringsFileInfoDe fileInfo = _StringsFileInfoDe._(_root); @override late final _StringsMediaMenuDe mediaMenu = _StringsMediaMenuDe._(_root); + @override late final _StringsAccessibilityDe accessibility = _StringsAccessibilityDe._(_root); @override late final _StringsTooltipsDe tooltips = _StringsTooltipsDe._(_root); @override late final _StringsVideoControlsDe videoControls = _StringsVideoControlsDe._(_root); @override late final _StringsUserStatusDe userStatus = _StringsUserStatusDe._(_root); @@ -1142,6 +1143,22 @@ class _StringsMediaMenuDe implements _StringsMediaMenuEn { @override String get fileInfo => 'Dateiinfo'; } +// Path: accessibility +class _StringsAccessibilityDe implements _StringsAccessibilityEn { + _StringsAccessibilityDe._(this._root); + + @override final _StringsDe _root; // ignore: unused_field + + // Translations + @override String mediaCardMovie({required Object title}) => '${title}, movie'; + @override String mediaCardShow({required Object title}) => '${title}, TV show'; + @override String mediaCardEpisode({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}'; + @override String mediaCardSeason({required Object title, required Object seasonInfo}) => '${title}, ${seasonInfo}'; + @override String get mediaCardWatched => 'watched'; + @override String mediaCardPartiallyWatched({required Object percent}) => '${percent} percent watched'; + @override String get mediaCardUnwatched => 'unwatched'; +} + // Path: tooltips class _StringsTooltipsDe implements _StringsTooltipsEn { _StringsTooltipsDe._(this._root); @@ -1174,6 +1191,29 @@ class _StringsVideoControlsDe implements _StringsVideoControlsEn { @override String get stretch => 'Strecken'; @override String get lockRotation => 'Rotation sperren'; @override String get unlockRotation => 'Rotation entsperren'; + @override String get playButton => 'Play'; + @override String get pauseButton => 'Pause'; + @override String seekBackwardButton({required Object seconds}) => 'Seek backward ${seconds} seconds'; + @override String seekForwardButton({required Object seconds}) => 'Seek forward ${seconds} seconds'; + @override String get previousButton => 'Previous episode'; + @override String get nextButton => 'Next episode'; + @override String get previousChapterButton => 'Previous chapter'; + @override String get nextChapterButton => 'Next chapter'; + @override String get volumeButton => 'Volume'; + @override String get muteButton => 'Mute'; + @override String get unmuteButton => 'Unmute'; + @override String get settingsButton => 'Video settings'; + @override String get audioTrackButton => 'Audio tracks'; + @override String get subtitlesButton => 'Subtitles'; + @override String get chaptersButton => 'Chapters'; + @override String get versionsButton => 'Video versions'; + @override String get aspectRatioButton => 'Aspect ratio'; + @override String get fullscreenButton => 'Enter fullscreen'; + @override String get exitFullscreenButton => 'Exit fullscreen'; + @override String get rotationLockButton => 'Rotation lock'; + @override String get timelineSlider => 'Video timeline'; + @override String get volumeSlider => 'Volume level'; + @override String get backButton => 'Back'; } // Path: userStatus @@ -1525,6 +1565,7 @@ class _StringsIt implements Translations { @override late final _StringsPinEntryIt pinEntry = _StringsPinEntryIt._(_root); @override late final _StringsFileInfoIt fileInfo = _StringsFileInfoIt._(_root); @override late final _StringsMediaMenuIt mediaMenu = _StringsMediaMenuIt._(_root); + @override late final _StringsAccessibilityIt accessibility = _StringsAccessibilityIt._(_root); @override late final _StringsTooltipsIt tooltips = _StringsTooltipsIt._(_root); @override late final _StringsVideoControlsIt videoControls = _StringsVideoControlsIt._(_root); @override late final _StringsUserStatusIt userStatus = _StringsUserStatusIt._(_root); @@ -1803,6 +1844,22 @@ class _StringsMediaMenuIt implements _StringsMediaMenuEn { @override String get fileInfo => 'Info sul file'; } +// Path: accessibility +class _StringsAccessibilityIt implements _StringsAccessibilityEn { + _StringsAccessibilityIt._(this._root); + + @override final _StringsIt _root; // ignore: unused_field + + // Translations + @override String mediaCardMovie({required Object title}) => '${title}, movie'; + @override String mediaCardShow({required Object title}) => '${title}, TV show'; + @override String mediaCardEpisode({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}'; + @override String mediaCardSeason({required Object title, required Object seasonInfo}) => '${title}, ${seasonInfo}'; + @override String get mediaCardWatched => 'watched'; + @override String mediaCardPartiallyWatched({required Object percent}) => '${percent} percent watched'; + @override String get mediaCardUnwatched => 'unwatched'; +} + // Path: tooltips class _StringsTooltipsIt implements _StringsTooltipsEn { _StringsTooltipsIt._(this._root); @@ -1835,6 +1892,29 @@ class _StringsVideoControlsIt implements _StringsVideoControlsEn { @override String get stretch => 'Allunga'; @override String get lockRotation => 'Blocca rotazione'; @override String get unlockRotation => 'Sblocca rotazione'; + @override String get playButton => 'Play'; + @override String get pauseButton => 'Pause'; + @override String seekBackwardButton({required Object seconds}) => 'Seek backward ${seconds} seconds'; + @override String seekForwardButton({required Object seconds}) => 'Seek forward ${seconds} seconds'; + @override String get previousButton => 'Previous episode'; + @override String get nextButton => 'Next episode'; + @override String get previousChapterButton => 'Previous chapter'; + @override String get nextChapterButton => 'Next chapter'; + @override String get volumeButton => 'Volume'; + @override String get muteButton => 'Mute'; + @override String get unmuteButton => 'Unmute'; + @override String get settingsButton => 'Video settings'; + @override String get audioTrackButton => 'Audio tracks'; + @override String get subtitlesButton => 'Subtitles'; + @override String get chaptersButton => 'Chapters'; + @override String get versionsButton => 'Video versions'; + @override String get aspectRatioButton => 'Aspect ratio'; + @override String get fullscreenButton => 'Enter fullscreen'; + @override String get exitFullscreenButton => 'Exit fullscreen'; + @override String get rotationLockButton => 'Rotation lock'; + @override String get timelineSlider => 'Video timeline'; + @override String get volumeSlider => 'Volume level'; + @override String get backButton => 'Back'; } // Path: userStatus @@ -2186,6 +2266,7 @@ class _StringsNl implements Translations { @override late final _StringsPinEntryNl pinEntry = _StringsPinEntryNl._(_root); @override late final _StringsFileInfoNl fileInfo = _StringsFileInfoNl._(_root); @override late final _StringsMediaMenuNl mediaMenu = _StringsMediaMenuNl._(_root); + @override late final _StringsAccessibilityNl accessibility = _StringsAccessibilityNl._(_root); @override late final _StringsTooltipsNl tooltips = _StringsTooltipsNl._(_root); @override late final _StringsVideoControlsNl videoControls = _StringsVideoControlsNl._(_root); @override late final _StringsUserStatusNl userStatus = _StringsUserStatusNl._(_root); @@ -2464,6 +2545,22 @@ class _StringsMediaMenuNl implements _StringsMediaMenuEn { @override String get fileInfo => 'Bestand info'; } +// Path: accessibility +class _StringsAccessibilityNl implements _StringsAccessibilityEn { + _StringsAccessibilityNl._(this._root); + + @override final _StringsNl _root; // ignore: unused_field + + // Translations + @override String mediaCardMovie({required Object title}) => '${title}, movie'; + @override String mediaCardShow({required Object title}) => '${title}, TV show'; + @override String mediaCardEpisode({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}'; + @override String mediaCardSeason({required Object title, required Object seasonInfo}) => '${title}, ${seasonInfo}'; + @override String get mediaCardWatched => 'watched'; + @override String mediaCardPartiallyWatched({required Object percent}) => '${percent} percent watched'; + @override String get mediaCardUnwatched => 'unwatched'; +} + // Path: tooltips class _StringsTooltipsNl implements _StringsTooltipsEn { _StringsTooltipsNl._(this._root); @@ -2496,6 +2593,29 @@ class _StringsVideoControlsNl implements _StringsVideoControlsEn { @override String get stretch => 'Uitrekken'; @override String get lockRotation => 'Vergrendel rotatie'; @override String get unlockRotation => 'Ontgrendel rotatie'; + @override String get playButton => 'Play'; + @override String get pauseButton => 'Pause'; + @override String seekBackwardButton({required Object seconds}) => 'Seek backward ${seconds} seconds'; + @override String seekForwardButton({required Object seconds}) => 'Seek forward ${seconds} seconds'; + @override String get previousButton => 'Previous episode'; + @override String get nextButton => 'Next episode'; + @override String get previousChapterButton => 'Previous chapter'; + @override String get nextChapterButton => 'Next chapter'; + @override String get volumeButton => 'Volume'; + @override String get muteButton => 'Mute'; + @override String get unmuteButton => 'Unmute'; + @override String get settingsButton => 'Video settings'; + @override String get audioTrackButton => 'Audio tracks'; + @override String get subtitlesButton => 'Subtitles'; + @override String get chaptersButton => 'Chapters'; + @override String get versionsButton => 'Video versions'; + @override String get aspectRatioButton => 'Aspect ratio'; + @override String get fullscreenButton => 'Enter fullscreen'; + @override String get exitFullscreenButton => 'Exit fullscreen'; + @override String get rotationLockButton => 'Rotation lock'; + @override String get timelineSlider => 'Video timeline'; + @override String get volumeSlider => 'Volume level'; + @override String get backButton => 'Back'; } // Path: userStatus @@ -2847,6 +2967,7 @@ class _StringsSv implements Translations { @override late final _StringsPinEntrySv pinEntry = _StringsPinEntrySv._(_root); @override late final _StringsFileInfoSv fileInfo = _StringsFileInfoSv._(_root); @override late final _StringsMediaMenuSv mediaMenu = _StringsMediaMenuSv._(_root); + @override late final _StringsAccessibilitySv accessibility = _StringsAccessibilitySv._(_root); @override late final _StringsTooltipsSv tooltips = _StringsTooltipsSv._(_root); @override late final _StringsVideoControlsSv videoControls = _StringsVideoControlsSv._(_root); @override late final _StringsUserStatusSv userStatus = _StringsUserStatusSv._(_root); @@ -3125,6 +3246,22 @@ class _StringsMediaMenuSv implements _StringsMediaMenuEn { @override String get fileInfo => 'Filinformation'; } +// Path: accessibility +class _StringsAccessibilitySv implements _StringsAccessibilityEn { + _StringsAccessibilitySv._(this._root); + + @override final _StringsSv _root; // ignore: unused_field + + // Translations + @override String mediaCardMovie({required Object title}) => '${title}, movie'; + @override String mediaCardShow({required Object title}) => '${title}, TV show'; + @override String mediaCardEpisode({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}'; + @override String mediaCardSeason({required Object title, required Object seasonInfo}) => '${title}, ${seasonInfo}'; + @override String get mediaCardWatched => 'watched'; + @override String mediaCardPartiallyWatched({required Object percent}) => '${percent} percent watched'; + @override String get mediaCardUnwatched => 'unwatched'; +} + // Path: tooltips class _StringsTooltipsSv implements _StringsTooltipsEn { _StringsTooltipsSv._(this._root); @@ -3157,6 +3294,29 @@ class _StringsVideoControlsSv implements _StringsVideoControlsEn { @override String get stretch => 'Sträck'; @override String get lockRotation => 'Lås rotation'; @override String get unlockRotation => 'Lås upp rotation'; + @override String get playButton => 'Play'; + @override String get pauseButton => 'Pause'; + @override String seekBackwardButton({required Object seconds}) => 'Seek backward ${seconds} seconds'; + @override String seekForwardButton({required Object seconds}) => 'Seek forward ${seconds} seconds'; + @override String get previousButton => 'Previous episode'; + @override String get nextButton => 'Next episode'; + @override String get previousChapterButton => 'Previous chapter'; + @override String get nextChapterButton => 'Next chapter'; + @override String get volumeButton => 'Volume'; + @override String get muteButton => 'Mute'; + @override String get unmuteButton => 'Unmute'; + @override String get settingsButton => 'Video settings'; + @override String get audioTrackButton => 'Audio tracks'; + @override String get subtitlesButton => 'Subtitles'; + @override String get chaptersButton => 'Chapters'; + @override String get versionsButton => 'Video versions'; + @override String get aspectRatioButton => 'Aspect ratio'; + @override String get fullscreenButton => 'Enter fullscreen'; + @override String get exitFullscreenButton => 'Exit fullscreen'; + @override String get rotationLockButton => 'Rotation lock'; + @override String get timelineSlider => 'Video timeline'; + @override String get volumeSlider => 'Volume level'; + @override String get backButton => 'Back'; } // Path: userStatus @@ -3508,6 +3668,7 @@ class _StringsZh implements Translations { @override late final _StringsPinEntryZh pinEntry = _StringsPinEntryZh._(_root); @override late final _StringsFileInfoZh fileInfo = _StringsFileInfoZh._(_root); @override late final _StringsMediaMenuZh mediaMenu = _StringsMediaMenuZh._(_root); + @override late final _StringsAccessibilityZh accessibility = _StringsAccessibilityZh._(_root); @override late final _StringsTooltipsZh tooltips = _StringsTooltipsZh._(_root); @override late final _StringsVideoControlsZh videoControls = _StringsVideoControlsZh._(_root); @override late final _StringsUserStatusZh userStatus = _StringsUserStatusZh._(_root); @@ -3786,6 +3947,22 @@ class _StringsMediaMenuZh implements _StringsMediaMenuEn { @override String get fileInfo => '文件信息'; } +// Path: accessibility +class _StringsAccessibilityZh implements _StringsAccessibilityEn { + _StringsAccessibilityZh._(this._root); + + @override final _StringsZh _root; // ignore: unused_field + + // Translations + @override String mediaCardMovie({required Object title}) => '${title}, movie'; + @override String mediaCardShow({required Object title}) => '${title}, TV show'; + @override String mediaCardEpisode({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}'; + @override String mediaCardSeason({required Object title, required Object seasonInfo}) => '${title}, ${seasonInfo}'; + @override String get mediaCardWatched => 'watched'; + @override String mediaCardPartiallyWatched({required Object percent}) => '${percent} percent watched'; + @override String get mediaCardUnwatched => 'unwatched'; +} + // Path: tooltips class _StringsTooltipsZh implements _StringsTooltipsEn { _StringsTooltipsZh._(this._root); @@ -3818,6 +3995,29 @@ class _StringsVideoControlsZh implements _StringsVideoControlsEn { @override String get stretch => '拉伸'; @override String get lockRotation => '锁定旋转'; @override String get unlockRotation => '解锁旋转'; + @override String get playButton => 'Play'; + @override String get pauseButton => 'Pause'; + @override String seekBackwardButton({required Object seconds}) => 'Seek backward ${seconds} seconds'; + @override String seekForwardButton({required Object seconds}) => 'Seek forward ${seconds} seconds'; + @override String get previousButton => 'Previous episode'; + @override String get nextButton => 'Next episode'; + @override String get previousChapterButton => 'Previous chapter'; + @override String get nextChapterButton => 'Next chapter'; + @override String get volumeButton => 'Volume'; + @override String get muteButton => 'Mute'; + @override String get unmuteButton => 'Unmute'; + @override String get settingsButton => 'Video settings'; + @override String get audioTrackButton => 'Audio tracks'; + @override String get subtitlesButton => 'Subtitles'; + @override String get chaptersButton => 'Chapters'; + @override String get versionsButton => 'Video versions'; + @override String get aspectRatioButton => 'Aspect ratio'; + @override String get fullscreenButton => 'Enter fullscreen'; + @override String get exitFullscreenButton => 'Exit fullscreen'; + @override String get rotationLockButton => 'Rotation lock'; + @override String get timelineSlider => 'Video timeline'; + @override String get volumeSlider => 'Volume level'; + @override String get backButton => 'Back'; } // Path: userStatus @@ -4693,6 +4893,13 @@ extension on _StringsDe { case 'mediaMenu.goToSeason': return 'Zur Staffel'; case 'mediaMenu.shufflePlay': return 'Zufallswiedergabe'; case 'mediaMenu.fileInfo': return 'Dateiinfo'; + case 'accessibility.mediaCardMovie': return ({required Object title}) => '${title}, movie'; + case 'accessibility.mediaCardShow': return ({required Object title}) => '${title}, TV show'; + case 'accessibility.mediaCardEpisode': return ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}'; + case 'accessibility.mediaCardSeason': return ({required Object title, required Object seasonInfo}) => '${title}, ${seasonInfo}'; + case 'accessibility.mediaCardWatched': return 'watched'; + case 'accessibility.mediaCardPartiallyWatched': return ({required Object percent}) => '${percent} percent watched'; + case 'accessibility.mediaCardUnwatched': return 'unwatched'; case 'tooltips.shufflePlay': return 'Zufallswiedergabe'; case 'tooltips.markAsWatched': return 'Als gesehen markieren'; case 'tooltips.markAsUnwatched': return 'Als ungesehen markieren'; @@ -4709,6 +4916,29 @@ extension on _StringsDe { case 'videoControls.stretch': return 'Strecken'; case 'videoControls.lockRotation': return 'Rotation sperren'; case 'videoControls.unlockRotation': return 'Rotation entsperren'; + case 'videoControls.playButton': return 'Play'; + case 'videoControls.pauseButton': return 'Pause'; + case 'videoControls.seekBackwardButton': return ({required Object seconds}) => 'Seek backward ${seconds} seconds'; + case 'videoControls.seekForwardButton': return ({required Object seconds}) => 'Seek forward ${seconds} seconds'; + case 'videoControls.previousButton': return 'Previous episode'; + case 'videoControls.nextButton': return 'Next episode'; + case 'videoControls.previousChapterButton': return 'Previous chapter'; + case 'videoControls.nextChapterButton': return 'Next chapter'; + case 'videoControls.volumeButton': return 'Volume'; + case 'videoControls.muteButton': return 'Mute'; + case 'videoControls.unmuteButton': return 'Unmute'; + case 'videoControls.settingsButton': return 'Video settings'; + case 'videoControls.audioTrackButton': return 'Audio tracks'; + case 'videoControls.subtitlesButton': return 'Subtitles'; + case 'videoControls.chaptersButton': return 'Chapters'; + case 'videoControls.versionsButton': return 'Video versions'; + case 'videoControls.aspectRatioButton': return 'Aspect ratio'; + case 'videoControls.fullscreenButton': return 'Enter fullscreen'; + case 'videoControls.exitFullscreenButton': return 'Exit fullscreen'; + case 'videoControls.rotationLockButton': return 'Rotation lock'; + case 'videoControls.timelineSlider': return 'Video timeline'; + case 'videoControls.volumeSlider': return 'Volume level'; + case 'videoControls.backButton': return 'Back'; case 'userStatus.admin': return 'Eigentümer'; case 'userStatus.restricted': return 'Eingeschränkt'; case 'userStatus.protected': return 'Geschützt'; @@ -5056,6 +5286,13 @@ extension on _StringsIt { case 'mediaMenu.goToSeason': return 'Vai alla stagione'; case 'mediaMenu.shufflePlay': return 'Riproduzione casuale'; case 'mediaMenu.fileInfo': return 'Info sul file'; + case 'accessibility.mediaCardMovie': return ({required Object title}) => '${title}, movie'; + case 'accessibility.mediaCardShow': return ({required Object title}) => '${title}, TV show'; + case 'accessibility.mediaCardEpisode': return ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}'; + case 'accessibility.mediaCardSeason': return ({required Object title, required Object seasonInfo}) => '${title}, ${seasonInfo}'; + case 'accessibility.mediaCardWatched': return 'watched'; + case 'accessibility.mediaCardPartiallyWatched': return ({required Object percent}) => '${percent} percent watched'; + case 'accessibility.mediaCardUnwatched': return 'unwatched'; case 'tooltips.shufflePlay': return 'Riproduzione casuale'; case 'tooltips.markAsWatched': return 'Segna come visto'; case 'tooltips.markAsUnwatched': return 'Segna come non visto'; @@ -5072,6 +5309,29 @@ extension on _StringsIt { case 'videoControls.stretch': return 'Allunga'; case 'videoControls.lockRotation': return 'Blocca rotazione'; case 'videoControls.unlockRotation': return 'Sblocca rotazione'; + case 'videoControls.playButton': return 'Play'; + case 'videoControls.pauseButton': return 'Pause'; + case 'videoControls.seekBackwardButton': return ({required Object seconds}) => 'Seek backward ${seconds} seconds'; + case 'videoControls.seekForwardButton': return ({required Object seconds}) => 'Seek forward ${seconds} seconds'; + case 'videoControls.previousButton': return 'Previous episode'; + case 'videoControls.nextButton': return 'Next episode'; + case 'videoControls.previousChapterButton': return 'Previous chapter'; + case 'videoControls.nextChapterButton': return 'Next chapter'; + case 'videoControls.volumeButton': return 'Volume'; + case 'videoControls.muteButton': return 'Mute'; + case 'videoControls.unmuteButton': return 'Unmute'; + case 'videoControls.settingsButton': return 'Video settings'; + case 'videoControls.audioTrackButton': return 'Audio tracks'; + case 'videoControls.subtitlesButton': return 'Subtitles'; + case 'videoControls.chaptersButton': return 'Chapters'; + case 'videoControls.versionsButton': return 'Video versions'; + case 'videoControls.aspectRatioButton': return 'Aspect ratio'; + case 'videoControls.fullscreenButton': return 'Enter fullscreen'; + case 'videoControls.exitFullscreenButton': return 'Exit fullscreen'; + case 'videoControls.rotationLockButton': return 'Rotation lock'; + case 'videoControls.timelineSlider': return 'Video timeline'; + case 'videoControls.volumeSlider': return 'Volume level'; + case 'videoControls.backButton': return 'Back'; case 'userStatus.admin': return 'Admin'; case 'userStatus.restricted': return 'Limitato'; case 'userStatus.protected': return 'Protetto'; @@ -5419,6 +5679,13 @@ extension on _StringsNl { case 'mediaMenu.goToSeason': return 'Ga naar seizoen'; case 'mediaMenu.shufflePlay': return 'Willekeurig afspelen'; case 'mediaMenu.fileInfo': return 'Bestand info'; + case 'accessibility.mediaCardMovie': return ({required Object title}) => '${title}, movie'; + case 'accessibility.mediaCardShow': return ({required Object title}) => '${title}, TV show'; + case 'accessibility.mediaCardEpisode': return ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}'; + case 'accessibility.mediaCardSeason': return ({required Object title, required Object seasonInfo}) => '${title}, ${seasonInfo}'; + case 'accessibility.mediaCardWatched': return 'watched'; + case 'accessibility.mediaCardPartiallyWatched': return ({required Object percent}) => '${percent} percent watched'; + case 'accessibility.mediaCardUnwatched': return 'unwatched'; case 'tooltips.shufflePlay': return 'Willekeurig afspelen'; case 'tooltips.markAsWatched': return 'Markeer als gekeken'; case 'tooltips.markAsUnwatched': return 'Markeer als ongekeken'; @@ -5435,6 +5702,29 @@ extension on _StringsNl { case 'videoControls.stretch': return 'Uitrekken'; case 'videoControls.lockRotation': return 'Vergrendel rotatie'; case 'videoControls.unlockRotation': return 'Ontgrendel rotatie'; + case 'videoControls.playButton': return 'Play'; + case 'videoControls.pauseButton': return 'Pause'; + case 'videoControls.seekBackwardButton': return ({required Object seconds}) => 'Seek backward ${seconds} seconds'; + case 'videoControls.seekForwardButton': return ({required Object seconds}) => 'Seek forward ${seconds} seconds'; + case 'videoControls.previousButton': return 'Previous episode'; + case 'videoControls.nextButton': return 'Next episode'; + case 'videoControls.previousChapterButton': return 'Previous chapter'; + case 'videoControls.nextChapterButton': return 'Next chapter'; + case 'videoControls.volumeButton': return 'Volume'; + case 'videoControls.muteButton': return 'Mute'; + case 'videoControls.unmuteButton': return 'Unmute'; + case 'videoControls.settingsButton': return 'Video settings'; + case 'videoControls.audioTrackButton': return 'Audio tracks'; + case 'videoControls.subtitlesButton': return 'Subtitles'; + case 'videoControls.chaptersButton': return 'Chapters'; + case 'videoControls.versionsButton': return 'Video versions'; + case 'videoControls.aspectRatioButton': return 'Aspect ratio'; + case 'videoControls.fullscreenButton': return 'Enter fullscreen'; + case 'videoControls.exitFullscreenButton': return 'Exit fullscreen'; + case 'videoControls.rotationLockButton': return 'Rotation lock'; + case 'videoControls.timelineSlider': return 'Video timeline'; + case 'videoControls.volumeSlider': return 'Volume level'; + case 'videoControls.backButton': return 'Back'; case 'userStatus.admin': return 'Beheerder'; case 'userStatus.restricted': return 'Beperkt'; case 'userStatus.protected': return 'Beschermd'; @@ -5782,6 +6072,13 @@ extension on _StringsSv { case 'mediaMenu.goToSeason': return 'Gå till säsong'; case 'mediaMenu.shufflePlay': return 'Blanda uppspelning'; case 'mediaMenu.fileInfo': return 'Filinformation'; + case 'accessibility.mediaCardMovie': return ({required Object title}) => '${title}, movie'; + case 'accessibility.mediaCardShow': return ({required Object title}) => '${title}, TV show'; + case 'accessibility.mediaCardEpisode': return ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}'; + case 'accessibility.mediaCardSeason': return ({required Object title, required Object seasonInfo}) => '${title}, ${seasonInfo}'; + case 'accessibility.mediaCardWatched': return 'watched'; + case 'accessibility.mediaCardPartiallyWatched': return ({required Object percent}) => '${percent} percent watched'; + case 'accessibility.mediaCardUnwatched': return 'unwatched'; case 'tooltips.shufflePlay': return 'Blanda uppspelning'; case 'tooltips.markAsWatched': return 'Markera som sedd'; case 'tooltips.markAsUnwatched': return 'Markera som osedd'; @@ -5798,6 +6095,29 @@ extension on _StringsSv { case 'videoControls.stretch': return 'Sträck'; case 'videoControls.lockRotation': return 'Lås rotation'; case 'videoControls.unlockRotation': return 'Lås upp rotation'; + case 'videoControls.playButton': return 'Play'; + case 'videoControls.pauseButton': return 'Pause'; + case 'videoControls.seekBackwardButton': return ({required Object seconds}) => 'Seek backward ${seconds} seconds'; + case 'videoControls.seekForwardButton': return ({required Object seconds}) => 'Seek forward ${seconds} seconds'; + case 'videoControls.previousButton': return 'Previous episode'; + case 'videoControls.nextButton': return 'Next episode'; + case 'videoControls.previousChapterButton': return 'Previous chapter'; + case 'videoControls.nextChapterButton': return 'Next chapter'; + case 'videoControls.volumeButton': return 'Volume'; + case 'videoControls.muteButton': return 'Mute'; + case 'videoControls.unmuteButton': return 'Unmute'; + case 'videoControls.settingsButton': return 'Video settings'; + case 'videoControls.audioTrackButton': return 'Audio tracks'; + case 'videoControls.subtitlesButton': return 'Subtitles'; + case 'videoControls.chaptersButton': return 'Chapters'; + case 'videoControls.versionsButton': return 'Video versions'; + case 'videoControls.aspectRatioButton': return 'Aspect ratio'; + case 'videoControls.fullscreenButton': return 'Enter fullscreen'; + case 'videoControls.exitFullscreenButton': return 'Exit fullscreen'; + case 'videoControls.rotationLockButton': return 'Rotation lock'; + case 'videoControls.timelineSlider': return 'Video timeline'; + case 'videoControls.volumeSlider': return 'Volume level'; + case 'videoControls.backButton': return 'Back'; case 'userStatus.admin': return 'Admin'; case 'userStatus.restricted': return 'Begränsad'; case 'userStatus.protected': return 'Skyddad'; @@ -6145,6 +6465,13 @@ extension on _StringsZh { case 'mediaMenu.goToSeason': return '转到季'; case 'mediaMenu.shufflePlay': return '随机播放'; case 'mediaMenu.fileInfo': return '文件信息'; + case 'accessibility.mediaCardMovie': return ({required Object title}) => '${title}, movie'; + case 'accessibility.mediaCardShow': return ({required Object title}) => '${title}, TV show'; + case 'accessibility.mediaCardEpisode': return ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}'; + case 'accessibility.mediaCardSeason': return ({required Object title, required Object seasonInfo}) => '${title}, ${seasonInfo}'; + case 'accessibility.mediaCardWatched': return 'watched'; + case 'accessibility.mediaCardPartiallyWatched': return ({required Object percent}) => '${percent} percent watched'; + case 'accessibility.mediaCardUnwatched': return 'unwatched'; case 'tooltips.shufflePlay': return '随机播放'; case 'tooltips.markAsWatched': return '标记为已观看'; case 'tooltips.markAsUnwatched': return '标记为未观看'; @@ -6161,6 +6488,29 @@ extension on _StringsZh { case 'videoControls.stretch': return '拉伸'; case 'videoControls.lockRotation': return '锁定旋转'; case 'videoControls.unlockRotation': return '解锁旋转'; + case 'videoControls.playButton': return 'Play'; + case 'videoControls.pauseButton': return 'Pause'; + case 'videoControls.seekBackwardButton': return ({required Object seconds}) => 'Seek backward ${seconds} seconds'; + case 'videoControls.seekForwardButton': return ({required Object seconds}) => 'Seek forward ${seconds} seconds'; + case 'videoControls.previousButton': return 'Previous episode'; + case 'videoControls.nextButton': return 'Next episode'; + case 'videoControls.previousChapterButton': return 'Previous chapter'; + case 'videoControls.nextChapterButton': return 'Next chapter'; + case 'videoControls.volumeButton': return 'Volume'; + case 'videoControls.muteButton': return 'Mute'; + case 'videoControls.unmuteButton': return 'Unmute'; + case 'videoControls.settingsButton': return 'Video settings'; + case 'videoControls.audioTrackButton': return 'Audio tracks'; + case 'videoControls.subtitlesButton': return 'Subtitles'; + case 'videoControls.chaptersButton': return 'Chapters'; + case 'videoControls.versionsButton': return 'Video versions'; + case 'videoControls.aspectRatioButton': return 'Aspect ratio'; + case 'videoControls.fullscreenButton': return 'Enter fullscreen'; + case 'videoControls.exitFullscreenButton': return 'Exit fullscreen'; + case 'videoControls.rotationLockButton': return 'Rotation lock'; + case 'videoControls.timelineSlider': return 'Video timeline'; + case 'videoControls.volumeSlider': return 'Volume level'; + case 'videoControls.backButton': return 'Back'; case 'userStatus.admin': return '管理员'; case 'userStatus.restricted': return '受限'; case 'userStatus.protected': return '受保护'; From 2f307ee613380dee4251fe005d3a78d29a954367 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Mon, 17 Nov 2025 22:29:08 -0600 Subject: [PATCH 5/8] added proper semantic labels to media cards --- lib/widgets/media_card.dart | 55 ++++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/lib/widgets/media_card.dart b/lib/widgets/media_card.dart index 0801f86e..a71a80bb 100644 --- a/lib/widgets/media_card.dart +++ b/lib/widgets/media_card.dart @@ -39,6 +39,47 @@ class MediaCard extends StatefulWidget { } class _MediaCardState extends State { + String _buildSemanticLabel() { + final item = widget.item; + final itemType = item.type.toLowerCase(); + + // Build base label based on type + String baseLabel; + if (itemType == 'episode') { + final episodeInfo = item.parentIndex != null && item.index != null + ? 'S${item.parentIndex} E${item.index}' + : ''; + baseLabel = t.accessibility.mediaCardEpisode( + title: item.displayTitle, + episodeInfo: episodeInfo, + ); + } else if (itemType == 'season') { + final seasonInfo = item.parentIndex != null + ? 'Season ${item.parentIndex}' + : ''; + baseLabel = t.accessibility.mediaCardSeason( + title: item.displayTitle, + seasonInfo: seasonInfo, + ); + } else if (itemType == 'movie') { + baseLabel = t.accessibility.mediaCardMovie(title: item.displayTitle); + } else { + baseLabel = t.accessibility.mediaCardShow(title: item.displayTitle); + } + + // Add watched status + if (item.isWatched) { + baseLabel = '$baseLabel, ${t.accessibility.mediaCardWatched}'; + } 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)}'; + } else { + baseLabel = '$baseLabel, ${t.accessibility.mediaCardUnwatched}'; + } + + return baseLabel; + } + void _handleTap(BuildContext context) async { final client = context.client; if (client == null) return; @@ -100,6 +141,8 @@ class _MediaCardState extends State { ? ViewMode.grid : settingsProvider.viewMode; + final semanticLabel = _buildSemanticLabel(); + return MediaContextMenu( metadata: widget.item, onRefresh: widget.onRefresh, @@ -111,10 +154,12 @@ class _MediaCardState extends State { item: widget.item, width: widget.width, height: widget.height, + semanticLabel: semanticLabel, onTap: () => _handleTap(context), ) : _MediaCardList( item: widget.item, + semanticLabel: semanticLabel, onTap: () => _handleTap(context), density: settingsProvider.libraryDensity, ), @@ -127,12 +172,14 @@ class _MediaCardGrid extends StatelessWidget { final PlexMetadata item; final double? width; final double? height; + final String semanticLabel; final VoidCallback onTap; const _MediaCardGrid({ required this.item, this.width, this.height, + required this.semanticLabel, required this.onTap, }); @@ -141,8 +188,7 @@ class _MediaCardGrid extends StatelessWidget { return SizedBox( width: width, child: Semantics( - label: "media-card-${item.ratingKey}", - identifier: "media-card-${item.ratingKey}", + label: semanticLabel, button: true, child: InkWell( borderRadius: BorderRadius.circular(8), @@ -272,11 +318,13 @@ class _MediaCardGrid extends StatelessWidget { /// List layout for media cards class _MediaCardList extends StatelessWidget { final PlexMetadata item; + final String semanticLabel; final VoidCallback onTap; final LibraryDensity density; const _MediaCardList({ required this.item, + required this.semanticLabel, required this.onTap, required this.density, }); @@ -420,8 +468,7 @@ class _MediaCardList extends StatelessWidget { final subtitle = _buildSubtitleText(); return Semantics( - label: "media-card-${item.ratingKey}", - identifier: "media-card-${item.ratingKey}", + label: semanticLabel, button: true, child: InkWell( borderRadius: BorderRadius.circular(8), From d0bfcb107d301500c77e0a0e612878de7c0341bb Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Mon, 17 Nov 2025 22:30:49 -0600 Subject: [PATCH 6/8] Enhance semantic labels to hero section on discover screen --- lib/screens/discover_screen.dart | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/screens/discover_screen.dart b/lib/screens/discover_screen.dart index 051e1d79..348196e7 100644 --- a/lib/screens/discover_screen.dart +++ b/lib/screens/discover_screen.dart @@ -839,11 +839,15 @@ class _DiscoverScreenState extends State ? t.discover.movie : t.discover.tvShow; + // Build semantic label for hero item + final heroLabel = isEpisode + ? "${heroItem.grandparentTitle}, ${heroItem.title}" + : heroItem.title; + return Semantics( - label: "media-hero-${heroItem.ratingKey}", - identifier: "media-hero-${heroItem.ratingKey}", + label: heroLabel, button: true, - hint: "Tap to play ${heroItem.title}", + hint: t.accessibility.tapToPlay, child: GestureDetector( onTap: () { final clientProvider = context.plexClient; From e519304f2ef22179b78cc15dec5345656988c815 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Mon, 17 Nov 2025 22:42:11 -0600 Subject: [PATCH 7/8] added translation for 'tap to play' hint on media cards --- lib/i18n/strings.g.dart | 16 ++++++++++++++-- lib/i18n/strings.i18n.json | 3 ++- lib/i18n/strings_de.i18n.json | 3 ++- lib/i18n/strings_it.i18n.json | 3 ++- lib/i18n/strings_nl.i18n.json | 3 ++- lib/i18n/strings_sv.i18n.json | 3 ++- lib/i18n/strings_zh.i18n.json | 3 ++- 7 files changed, 26 insertions(+), 8 deletions(-) diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index 2df8c7fa..00a85dd3 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 6 -/// Strings: 2310 (385 per locale) +/// Strings: 2316 (386 per locale) /// -/// Built on 2025-11-16 at 04:20 UTC +/// Built on 2025-11-17 at 03:47 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -456,6 +456,7 @@ class _StringsAccessibilityEn { String get mediaCardWatched => 'watched'; String mediaCardPartiallyWatched({required Object percent}) => '${percent} percent watched'; String get mediaCardUnwatched => 'unwatched'; + String get tapToPlay => 'Tap to play'; } // Path: tooltips @@ -1157,6 +1158,7 @@ class _StringsAccessibilityDe implements _StringsAccessibilityEn { @override String get mediaCardWatched => 'watched'; @override String mediaCardPartiallyWatched({required Object percent}) => '${percent} percent watched'; @override String get mediaCardUnwatched => 'unwatched'; + @override String get tapToPlay => 'Tap to play'; } // Path: tooltips @@ -1858,6 +1860,7 @@ class _StringsAccessibilityIt implements _StringsAccessibilityEn { @override String get mediaCardWatched => 'watched'; @override String mediaCardPartiallyWatched({required Object percent}) => '${percent} percent watched'; @override String get mediaCardUnwatched => 'unwatched'; + @override String get tapToPlay => 'Tap to play'; } // Path: tooltips @@ -2559,6 +2562,7 @@ class _StringsAccessibilityNl implements _StringsAccessibilityEn { @override String get mediaCardWatched => 'watched'; @override String mediaCardPartiallyWatched({required Object percent}) => '${percent} percent watched'; @override String get mediaCardUnwatched => 'unwatched'; + @override String get tapToPlay => 'Tap to play'; } // Path: tooltips @@ -3260,6 +3264,7 @@ class _StringsAccessibilitySv implements _StringsAccessibilityEn { @override String get mediaCardWatched => 'watched'; @override String mediaCardPartiallyWatched({required Object percent}) => '${percent} percent watched'; @override String get mediaCardUnwatched => 'unwatched'; + @override String get tapToPlay => 'Tap to play'; } // Path: tooltips @@ -3961,6 +3966,7 @@ class _StringsAccessibilityZh implements _StringsAccessibilityEn { @override String get mediaCardWatched => 'watched'; @override String mediaCardPartiallyWatched({required Object percent}) => '${percent} percent watched'; @override String get mediaCardUnwatched => 'unwatched'; + @override String get tapToPlay => 'Tap to play'; } // Path: tooltips @@ -4507,6 +4513,7 @@ extension on Translations { case 'accessibility.mediaCardWatched': return 'watched'; case 'accessibility.mediaCardPartiallyWatched': return ({required Object percent}) => '${percent} percent watched'; case 'accessibility.mediaCardUnwatched': return 'unwatched'; + case 'accessibility.tapToPlay': return 'Tap to play'; case 'tooltips.shufflePlay': return 'Shuffle play'; case 'tooltips.markAsWatched': return 'Mark as watched'; case 'tooltips.markAsUnwatched': return 'Mark as unwatched'; @@ -4900,6 +4907,7 @@ extension on _StringsDe { case 'accessibility.mediaCardWatched': return 'watched'; case 'accessibility.mediaCardPartiallyWatched': return ({required Object percent}) => '${percent} percent watched'; case 'accessibility.mediaCardUnwatched': return 'unwatched'; + case 'accessibility.tapToPlay': return 'Tap to play'; case 'tooltips.shufflePlay': return 'Zufallswiedergabe'; case 'tooltips.markAsWatched': return 'Als gesehen markieren'; case 'tooltips.markAsUnwatched': return 'Als ungesehen markieren'; @@ -5293,6 +5301,7 @@ extension on _StringsIt { case 'accessibility.mediaCardWatched': return 'watched'; case 'accessibility.mediaCardPartiallyWatched': return ({required Object percent}) => '${percent} percent watched'; case 'accessibility.mediaCardUnwatched': return 'unwatched'; + case 'accessibility.tapToPlay': return 'Tap to play'; case 'tooltips.shufflePlay': return 'Riproduzione casuale'; case 'tooltips.markAsWatched': return 'Segna come visto'; case 'tooltips.markAsUnwatched': return 'Segna come non visto'; @@ -5686,6 +5695,7 @@ extension on _StringsNl { case 'accessibility.mediaCardWatched': return 'watched'; case 'accessibility.mediaCardPartiallyWatched': return ({required Object percent}) => '${percent} percent watched'; case 'accessibility.mediaCardUnwatched': return 'unwatched'; + case 'accessibility.tapToPlay': return 'Tap to play'; case 'tooltips.shufflePlay': return 'Willekeurig afspelen'; case 'tooltips.markAsWatched': return 'Markeer als gekeken'; case 'tooltips.markAsUnwatched': return 'Markeer als ongekeken'; @@ -6079,6 +6089,7 @@ extension on _StringsSv { case 'accessibility.mediaCardWatched': return 'watched'; case 'accessibility.mediaCardPartiallyWatched': return ({required Object percent}) => '${percent} percent watched'; case 'accessibility.mediaCardUnwatched': return 'unwatched'; + case 'accessibility.tapToPlay': return 'Tap to play'; case 'tooltips.shufflePlay': return 'Blanda uppspelning'; case 'tooltips.markAsWatched': return 'Markera som sedd'; case 'tooltips.markAsUnwatched': return 'Markera som osedd'; @@ -6472,6 +6483,7 @@ extension on _StringsZh { case 'accessibility.mediaCardWatched': return 'watched'; case 'accessibility.mediaCardPartiallyWatched': return ({required Object percent}) => '${percent} percent watched'; case 'accessibility.mediaCardUnwatched': return 'unwatched'; + case 'accessibility.tapToPlay': return 'Tap to play'; case 'tooltips.shufflePlay': return '随机播放'; case 'tooltips.markAsWatched': return '标记为已观看'; case 'tooltips.markAsUnwatched': return '标记为未观看'; diff --git a/lib/i18n/strings.i18n.json b/lib/i18n/strings.i18n.json index 0184a993..2235af6a 100644 --- a/lib/i18n/strings.i18n.json +++ b/lib/i18n/strings.i18n.json @@ -188,7 +188,8 @@ "mediaCardSeason": "${title}, ${seasonInfo}", "mediaCardWatched": "watched", "mediaCardPartiallyWatched": "${percent} percent watched", - "mediaCardUnwatched": "unwatched" + "mediaCardUnwatched": "unwatched", + "tapToPlay": "Tap to play" }, "tooltips": { "shufflePlay": "Shuffle play", diff --git a/lib/i18n/strings_de.i18n.json b/lib/i18n/strings_de.i18n.json index 0865ffbd..f041b5d6 100644 --- a/lib/i18n/strings_de.i18n.json +++ b/lib/i18n/strings_de.i18n.json @@ -188,7 +188,8 @@ "mediaCardSeason": "${title}, ${seasonInfo}", "mediaCardWatched": "watched", "mediaCardPartiallyWatched": "${percent} percent watched", - "mediaCardUnwatched": "unwatched" + "mediaCardUnwatched": "unwatched", + "tapToPlay": "Tap to play" }, "tooltips": { "shufflePlay": "Zufallswiedergabe", diff --git a/lib/i18n/strings_it.i18n.json b/lib/i18n/strings_it.i18n.json index a8bd7c10..2ed401cb 100644 --- a/lib/i18n/strings_it.i18n.json +++ b/lib/i18n/strings_it.i18n.json @@ -188,7 +188,8 @@ "mediaCardSeason": "${title}, ${seasonInfo}", "mediaCardWatched": "watched", "mediaCardPartiallyWatched": "${percent} percent watched", - "mediaCardUnwatched": "unwatched" + "mediaCardUnwatched": "unwatched", + "tapToPlay": "Tap to play" }, "tooltips": { "shufflePlay": "Riproduzione casuale", diff --git a/lib/i18n/strings_nl.i18n.json b/lib/i18n/strings_nl.i18n.json index 5fd3c65a..0437b044 100644 --- a/lib/i18n/strings_nl.i18n.json +++ b/lib/i18n/strings_nl.i18n.json @@ -188,7 +188,8 @@ "mediaCardSeason": "${title}, ${seasonInfo}", "mediaCardWatched": "watched", "mediaCardPartiallyWatched": "${percent} percent watched", - "mediaCardUnwatched": "unwatched" + "mediaCardUnwatched": "unwatched", + "tapToPlay": "Tap to play" }, "tooltips": { "shufflePlay": "Willekeurig afspelen", diff --git a/lib/i18n/strings_sv.i18n.json b/lib/i18n/strings_sv.i18n.json index e47b4e04..6d5efa5c 100644 --- a/lib/i18n/strings_sv.i18n.json +++ b/lib/i18n/strings_sv.i18n.json @@ -188,7 +188,8 @@ "mediaCardSeason": "${title}, ${seasonInfo}", "mediaCardWatched": "watched", "mediaCardPartiallyWatched": "${percent} percent watched", - "mediaCardUnwatched": "unwatched" + "mediaCardUnwatched": "unwatched", + "tapToPlay": "Tap to play" }, "tooltips": { "shufflePlay": "Blanda uppspelning", diff --git a/lib/i18n/strings_zh.i18n.json b/lib/i18n/strings_zh.i18n.json index a3218bd7..c5e6e865 100644 --- a/lib/i18n/strings_zh.i18n.json +++ b/lib/i18n/strings_zh.i18n.json @@ -188,7 +188,8 @@ "mediaCardSeason": "${title}, ${seasonInfo}", "mediaCardWatched": "watched", "mediaCardPartiallyWatched": "${percent} percent watched", - "mediaCardUnwatched": "unwatched" + "mediaCardUnwatched": "unwatched", + "tapToPlay": "Tap to play" }, "tooltips": { "shufflePlay": "随机播放", 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 8/8] 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(