Skip to content

Commit 2618e61

Browse files
authored
fix(accessibility): improve screen reader feedback for playback controls (#763)
- Added semantic labels for play, pause, next, previous, shuffle, and repeat buttons. - Implemented semanticFormatterCallback in PositionSlider for time-based feedback. - Added tooltips to playback controls for better TalkBack navigation. - Increased tap target size for PlaybackIconButton to improve touch accessibility.
1 parent 8a0d8d8 commit 2618e61

File tree

4 files changed

+35
-1
lines changed

4 files changed

+35
-1
lines changed

lib/localization/app_en.arb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@
122122
"lyricsNotAvailable": "Lyrics not available",
123123
"makeOffline": "Make available offline",
124124
"minutes": "Minutes",
125+
"moreOptions": "More options",
125126
"mostPlayed": "Most played",
126127
"moveToFolder": "Move to folder",
127128
"name": "Name",
@@ -138,8 +139,11 @@
138139
"offlinePlaylists": "Offline playlists",
139140
"offlineSongs": "Offline songs",
140141
"others": "Others",
142+
"pause": "Pause",
141143
"pickImageFromDevice": "Pick image from device",
144+
"play": "Play",
142145
"playNext": "Play next",
146+
"playbackProgress": "Playback progress",
143147
"playlist": "Playlist",
144148
"playlistAlreadyDownloaded": "Playlist already downloaded",
145149
"playlistAlreadyExists": "Playlist already exists",
@@ -170,6 +174,7 @@
170174
"removeSearchQueryQuestion": "Are you sure you want to remove this search query?",
171175
"removedFromLikedSongs": "Removed from liked songs",
172176
"renameSong": "Rename song",
177+
"repeat": "Repeat",
173178
"restoreError": "Error restoring data",
174179
"restoreUserData": "Restore user data",
175180
"restoredSuccess": "Data restored successfully",
@@ -180,6 +185,9 @@
180185
"setTimer": "Set timer",
181186
"settingChangedMsg": "Settings updated successfully",
182187
"settings": "Settings",
188+
"shuffle": "Shuffle",
189+
"skipToNext": "Skip to next",
190+
"skipToPrevious": "Skip to previous",
183191
"sleepTimerCancelled": "Sleep timer cancelled",
184192
"sleepTimerSet": "Sleep timer set",
185193
"songAdded": "Song added",

lib/widgets/now_playing/now_playing_controls.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import 'package:audio_service/audio_service.dart';
2323
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
2424
import 'package:flutter/material.dart';
25+
import 'package:musify/extensions/l10n.dart';
2526
import 'package:musify/main.dart';
2627
import 'package:musify/services/settings_manager.dart';
2728
import 'package:musify/utilities/utils.dart';
@@ -205,6 +206,7 @@ class PlayerControlButtons extends StatelessWidget {
205206
child: Row(
206207
children: <Widget>[
207208
_buildShuffleButton(
209+
context,
208210
colorScheme,
209211
miniControlSize,
210212
buttonConstraints,
@@ -233,6 +235,7 @@ class PlayerControlButtons extends StatelessWidget {
233235
alpha: 0.3,
234236
),
235237
),
238+
tooltip: context.l10n!.skipToPrevious,
236239
constraints: buttonConstraints,
237240
iconSize: controlIconSize * 0.65,
238241
onPressed: audioHandler.hasPrevious
@@ -270,6 +273,7 @@ class PlayerControlButtons extends StatelessWidget {
270273
alpha: 0.3,
271274
),
272275
),
276+
tooltip: context.l10n!.skipToNext,
273277
constraints: buttonConstraints,
274278
iconSize: controlIconSize * 0.65,
275279
onPressed: () =>
@@ -303,6 +307,7 @@ class PlayerControlButtons extends StatelessWidget {
303307
),
304308
SizedBox(width: buttonSpacing),
305309
_buildRepeatButton(
310+
context,
306311
colorScheme,
307312
miniControlSize,
308313
buttonConstraints,
@@ -316,6 +321,7 @@ class PlayerControlButtons extends StatelessWidget {
316321
}
317322

318323
Widget _buildShuffleButton(
324+
BuildContext context,
319325
ColorScheme colorScheme,
320326
double size,
321327
BoxConstraints buttonConstraints,
@@ -331,6 +337,7 @@ class PlayerControlButtons extends StatelessWidget {
331337
: FluentIcons.arrow_shuffle_off_24_filled,
332338
color: value ? colorScheme.onPrimary : colorScheme.onSurfaceVariant,
333339
),
340+
tooltip: context.l10n!.shuffle,
334341
iconSize: size,
335342
constraints: buttonConstraints,
336343
padding: buttonPadding,
@@ -355,6 +362,7 @@ class PlayerControlButtons extends StatelessWidget {
355362
}
356363

357364
Widget _buildRepeatButton(
365+
BuildContext context,
358366
ColorScheme colorScheme,
359367
double size,
360368
BoxConstraints buttonConstraints,
@@ -380,6 +388,7 @@ class PlayerControlButtons extends StatelessWidget {
380388
? colorScheme.onPrimary
381389
: colorScheme.onSurfaceVariant,
382390
),
391+
tooltip: context.l10n!.repeat,
383392
iconSize: size,
384393
constraints: buttonConstraints,
385394
padding: buttonPadding,

lib/widgets/playback_icon_button.dart

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import 'package:audio_service/audio_service.dart';
2323
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
2424
import 'package:flutter/material.dart';
25+
import 'package:musify/extensions/l10n.dart';
2526
import 'package:musify/main.dart';
2627

2728
Widget buildPlaybackIconButton(
@@ -43,6 +44,7 @@ Widget buildPlaybackIconButton(
4344

4445
Widget iconWidget;
4546
VoidCallback? onPressed;
47+
String? semanticLabel;
4648

4749
if (processingState == AudioProcessingState.loading ||
4850
processingState == AudioProcessingState.buffering) {
@@ -62,13 +64,15 @@ Widget buildPlaybackIconButton(
6264
size: iconSize,
6365
);
6466
onPressed = () => audioHandler.seek(Duration.zero);
67+
semanticLabel = context.l10n!.play;
6568
} else {
6669
iconWidget = Icon(
6770
isPlaying ? FluentIcons.pause_24_filled : FluentIcons.play_24_filled,
6871
color: iconColor,
6972
size: iconSize,
7073
);
7174
onPressed = isPlaying ? audioHandler.pause : audioHandler.play;
75+
semanticLabel = isPlaying ? context.l10n!.pause : context.l10n!.play;
7276
}
7377

7478
return RawMaterialButton(
@@ -78,7 +82,16 @@ Widget buildPlaybackIconButton(
7882
splashColor: Colors.transparent,
7983
padding: padding ?? EdgeInsets.all(iconSize * 0.35),
8084
shape: const CircleBorder(),
81-
child: iconWidget,
85+
constraints: BoxConstraints.tightFor(
86+
width: iconSize * 2,
87+
height: iconSize * 2,
88+
),
89+
materialTapTargetSize: MaterialTapTargetSize.padded,
90+
child: Semantics(
91+
label: semanticLabel,
92+
button: true,
93+
child: iconWidget,
94+
),
8295
);
8396
},
8497
);

lib/widgets/position_slider.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
*/
2121

2222
import 'package:flutter/material.dart';
23+
import 'package:musify/extensions/l10n.dart';
2324
import 'package:musify/main.dart';
2425
import 'package:musify/models/position_data.dart';
2526
import 'package:musify/utilities/formatter.dart';
@@ -77,6 +78,9 @@ class _PositionSliderState extends State<PositionSlider> {
7778
}
7879
: null,
7980
max: maxDuration,
81+
label: formatDuration(currentValue.toInt()),
82+
semanticFormatterCallback: (value) =>
83+
formatDuration(value.toInt()),
8084
),
8185
_buildPositionRow(context, primaryColor, positionData),
8286
],

0 commit comments

Comments
 (0)