fix(a11y): label interactive controls

This commit is contained in:
zarzet
2026-08-04 16:40:48 +07:00
parent d3f9388ba8
commit 6742fe886f
35 changed files with 319 additions and 58 deletions
+12
View File
@@ -7434,6 +7434,18 @@ abstract class AppLocalizations {
/// **'Up next'**
String get nowPlayingUpNext;
/// Tooltip for the previous-track playback control
///
/// In en, this message translates to:
/// **'Previous track'**
String get nowPlayingPreviousTrack;
/// Tooltip for the next-track playback control
///
/// In en, this message translates to:
/// **'Next track'**
String get nowPlayingNextTrack;
/// Menu item and section title for track metadata details
///
/// In en, this message translates to:
+6
View File
@@ -4580,6 +4580,12 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get nowPlayingUpNext => 'Up next';
@override
String get nowPlayingPreviousTrack => 'Vorheriger Titel';
@override
String get nowPlayingNextTrack => 'Nächster Titel';
@override
String get nowPlayingDetails => 'Details';
+6
View File
@@ -4535,6 +4535,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get nowPlayingUpNext => 'Up next';
@override
String get nowPlayingPreviousTrack => 'Previous track';
@override
String get nowPlayingNextTrack => 'Next track';
@override
String get nowPlayingDetails => 'Details';
+12
View File
@@ -4529,6 +4529,12 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get nowPlayingUpNext => 'Up next';
@override
String get nowPlayingPreviousTrack => 'Pista anterior';
@override
String get nowPlayingNextTrack => 'Pista siguiente';
@override
String get nowPlayingDetails => 'Details';
@@ -9255,6 +9261,12 @@ class AppLocalizationsEsEs extends AppLocalizationsEs {
@override
String get nowPlayingUpNext => 'Up next';
@override
String get nowPlayingPreviousTrack => 'Pista anterior';
@override
String get nowPlayingNextTrack => 'Pista siguiente';
@override
String get nowPlayingDetails => 'Details';
+6
View File
@@ -4649,6 +4649,12 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get nowPlayingUpNext => 'À suivre';
@override
String get nowPlayingPreviousTrack => 'Piste précédente';
@override
String get nowPlayingNextTrack => 'Piste suivante';
@override
String get nowPlayingDetails => 'Détails';
+6
View File
@@ -4533,6 +4533,12 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get nowPlayingUpNext => 'Up next';
@override
String get nowPlayingPreviousTrack => 'Lagu sebelumnya';
@override
String get nowPlayingNextTrack => 'Lagu berikutnya';
@override
String get nowPlayingDetails => 'Details';
+6
View File
@@ -4524,6 +4524,12 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get nowPlayingUpNext => 'Up next';
@override
String get nowPlayingPreviousTrack => '前の曲';
@override
String get nowPlayingNextTrack => '次の曲';
@override
String get nowPlayingDetails => 'Details';
+6
View File
@@ -4409,6 +4409,12 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get nowPlayingUpNext => '다음 곡';
@override
String get nowPlayingPreviousTrack => '이전 곡';
@override
String get nowPlayingNextTrack => '다음 곡';
@override
String get nowPlayingDetails => '트랙 세부 정보';
+12
View File
@@ -4529,6 +4529,12 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get nowPlayingUpNext => 'Up next';
@override
String get nowPlayingPreviousTrack => 'Faixa anterior';
@override
String get nowPlayingNextTrack => 'Próxima faixa';
@override
String get nowPlayingDetails => 'Details';
@@ -9220,6 +9226,12 @@ class AppLocalizationsPtPt extends AppLocalizationsPt {
@override
String get nowPlayingUpNext => 'Up next';
@override
String get nowPlayingPreviousTrack => 'Faixa anterior';
@override
String get nowPlayingNextTrack => 'Próxima faixa';
@override
String get nowPlayingDetails => 'Details';
+6
View File
@@ -4566,6 +4566,12 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get nowPlayingUpNext => 'Up next';
@override
String get nowPlayingPreviousTrack => 'Предыдущий трек';
@override
String get nowPlayingNextTrack => 'Следующий трек';
@override
String get nowPlayingDetails => 'Details';
+6
View File
@@ -4565,6 +4565,12 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get nowPlayingUpNext => 'Up next';
@override
String get nowPlayingPreviousTrack => 'Önceki parça';
@override
String get nowPlayingNextTrack => 'Sonraki parça';
@override
String get nowPlayingDetails => 'Details';
+6
View File
@@ -4583,6 +4583,12 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get nowPlayingUpNext => 'Up next';
@override
String get nowPlayingPreviousTrack => 'Попередній трек';
@override
String get nowPlayingNextTrack => 'Наступний трек';
@override
String get nowPlayingDetails => 'Details';
+2
View File
@@ -4395,6 +4395,8 @@
"@nowPlayingUpNext": {
"description": "Title for the playback queue sheet"
},
"nowPlayingPreviousTrack": "Vorheriger Titel",
"nowPlayingNextTrack": "Nächster Titel",
"logAutoScrollOff": "Auto-Scrollen AUS",
"trackConvertFailed": "Konvertierung fehlgeschlagen",
"@trackConvertFailed": {
+8
View File
@@ -5861,6 +5861,14 @@
"@nowPlayingUpNext": {
"description": "Title for the playback queue sheet"
},
"nowPlayingPreviousTrack": "Previous track",
"@nowPlayingPreviousTrack": {
"description": "Tooltip for the previous-track playback control"
},
"nowPlayingNextTrack": "Next track",
"@nowPlayingNextTrack": {
"description": "Tooltip for the next-track playback control"
},
"nowPlayingDetails": "Details",
"@nowPlayingDetails": {
"description": "Menu item and section title for track metadata details"
+3 -1
View File
@@ -4286,5 +4286,7 @@
"settingsGroupContent": "Contenido y metadatos",
"settingsGroupDownloads": "Descargas y archivos",
"settingsGroupSystem": "Sistema",
"settingsGroupHelp": "Información y soporte"
"settingsGroupHelp": "Información y soporte",
"nowPlayingPreviousTrack": "Pista anterior",
"nowPlayingNextTrack": "Pista siguiente"
}
+2
View File
@@ -5502,6 +5502,8 @@
"@nowPlayingUpNext": {
"description": "Title for the playback queue sheet"
},
"nowPlayingPreviousTrack": "Pista anterior",
"nowPlayingNextTrack": "Pista siguiente",
"nowPlayingDetails": "Details",
"@nowPlayingDetails": {
"description": "Menu item and section title for track metadata details"
+2
View File
@@ -4395,6 +4395,8 @@
"@nowPlayingUpNext": {
"description": "Title for the playback queue sheet"
},
"nowPlayingPreviousTrack": "Piste précédente",
"nowPlayingNextTrack": "Piste suivante",
"logAutoScrollOff": "Défilement automatique désactivé",
"trackConvertFailed": "Échec de la conversion",
"@trackConvertFailed": {
+8
View File
@@ -4440,6 +4440,14 @@
"@nowPlayingUpNext": {
"description": "Title for the playback queue sheet"
},
"nowPlayingPreviousTrack": "Lagu sebelumnya",
"@nowPlayingPreviousTrack": {
"description": "Tooltip for the previous-track playback control"
},
"nowPlayingNextTrack": "Lagu berikutnya",
"@nowPlayingNextTrack": {
"description": "Tooltip for the next-track playback control"
},
"logAutoScrollOff": "Auto-scroll OFF",
"trackConvertFailed": "Conversion failed",
"@trackConvertFailed": {
+2
View File
@@ -4395,6 +4395,8 @@
"@nowPlayingUpNext": {
"description": "Title for the playback queue sheet"
},
"nowPlayingPreviousTrack": "前の曲",
"nowPlayingNextTrack": "次の曲",
"logAutoScrollOff": "Auto-scroll OFF",
"trackConvertFailed": "変換に失敗しました",
"@trackConvertFailed": {
+2
View File
@@ -5641,6 +5641,8 @@
"@nowPlayingUpNext": {
"description": "Title for the playback queue sheet"
},
"nowPlayingPreviousTrack": "이전 곡",
"nowPlayingNextTrack": "다음 곡",
"nowPlayingDetails": "트랙 세부 정보",
"@nowPlayingDetails": {
"description": "Menu item and section title for track metadata details"
+3 -1
View File
@@ -4286,5 +4286,7 @@
"settingsGroupContent": "Conteúdo e metadados",
"settingsGroupDownloads": "Downloads e arquivos",
"settingsGroupSystem": "Sistema",
"settingsGroupHelp": "Sobre e suporte"
"settingsGroupHelp": "Sobre e suporte",
"nowPlayingPreviousTrack": "Faixa anterior",
"nowPlayingNextTrack": "Próxima faixa"
}
+2
View File
@@ -4395,6 +4395,8 @@
"@nowPlayingUpNext": {
"description": "Title for the playback queue sheet"
},
"nowPlayingPreviousTrack": "Faixa anterior",
"nowPlayingNextTrack": "Próxima faixa",
"logAutoScrollOff": "Auto-scroll OFF",
"trackConvertFailed": "Conversion failed",
"@trackConvertFailed": {
+2
View File
@@ -4395,6 +4395,8 @@
"@nowPlayingUpNext": {
"description": "Title for the playback queue sheet"
},
"nowPlayingPreviousTrack": "Предыдущий трек",
"nowPlayingNextTrack": "Следующий трек",
"logAutoScrollOff": "Auto-scroll OFF",
"trackConvertFailed": "Ошибка конвертации",
"@trackConvertFailed": {
+2
View File
@@ -4395,6 +4395,8 @@
"@nowPlayingUpNext": {
"description": "Title for the playback queue sheet"
},
"nowPlayingPreviousTrack": "Önceki parça",
"nowPlayingNextTrack": "Sonraki parça",
"logAutoScrollOff": "Auto-scroll OFF",
"trackConvertFailed": "Dönüştürme başarısız oldu",
"@trackConvertFailed": {
+2
View File
@@ -4395,6 +4395,8 @@
"@nowPlayingUpNext": {
"description": "Title for the playback queue sheet"
},
"nowPlayingPreviousTrack": "Попередній трек",
"nowPlayingNextTrack": "Наступний трек",
"logAutoScrollOff": "Auto-scroll OFF",
"trackConvertFailed": "Конвертація не вдалася",
"@trackConvertFailed": {
@@ -639,6 +639,7 @@ class _LibraryTracksFolderScreenState
},
),
IconButton(
tooltip: context.l10n.collectionPlaylistChangeCover,
icon: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
+6
View File
@@ -337,6 +337,7 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
return Scaffold(
appBar: AppBar(
leading: IconButton(
tooltip: context.l10n.nowPlayingMinimize,
icon: const Icon(Icons.keyboard_arrow_down),
onPressed: () => Navigator.of(context).maybePop(),
),
@@ -1217,6 +1218,7 @@ class _PlaybackControls extends ConsumerWidget {
const SizedBox(width: 8),
IconButton(
iconSize: 44,
tooltip: context.l10n.nowPlayingPreviousTrack,
icon: const Icon(Icons.skip_previous),
onPressed: controller.previous,
),
@@ -1230,6 +1232,9 @@ class _PlaybackControls extends ConsumerWidget {
iconSize: 44,
padding: const EdgeInsets.all(12),
color: colorScheme.onPrimary,
tooltip: isPlaying
? context.l10n.actionPause
: context.l10n.tooltipPlay,
icon: isLoading
? const SizedBox.square(
dimension: 32,
@@ -1244,6 +1249,7 @@ class _PlaybackControls extends ConsumerWidget {
const SizedBox(width: 20),
IconButton(
iconSize: 44,
tooltip: context.l10n.nowPlayingNextTrack,
icon: const Icon(Icons.skip_next),
onPressed: controller.next,
),
+20 -10
View File
@@ -627,16 +627,26 @@ extension _QueueTabCollectionItemWidgets on _QueueTabState {
child: Row(
children: [
if (_isPlaylistSelectionMode)
GestureDetector(
onTap: () => _togglePlaylistSelection(playlist.id),
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.only(left: 8),
child: AnimatedSelectionCheckbox(
visible: true,
selected: isSelected,
colorScheme: colorScheme,
size: 24,
Semantics(
button: true,
checked: isSelected,
label: playlist.name,
child: GestureDetector(
onTap: () => _togglePlaylistSelection(playlist.id),
behavior: HitTestBehavior.opaque,
child: ConstrainedBox(
constraints: BoxConstraints.tightFor(
width: context.tokens.minTouchTarget,
height: context.tokens.minTouchTarget,
),
child: Center(
child: AnimatedSelectionCheckbox(
visible: true,
selected: isSelected,
colorScheme: colorScheme,
size: 24,
),
),
),
),
),
+8 -12
View File
@@ -1063,21 +1063,17 @@ extension _QueueTabItemWidgets on _QueueTabState {
valueListenable: fileExistsListenable,
builder: (context, fileExists, child) {
if (fileExists) {
return Semantics(
button: true,
label: context.l10n.a11yPlayTrackByArtist(
return IconButton.filled(
tooltip: context.l10n.a11yPlayTrackByArtist(
item.trackName,
item.artistName,
),
child: IconButton.filled(
onPressed: () => _playLibraryItem(item, libraryItems),
visualDensity: VisualDensity.compact,
iconSize: 18,
icon: const Icon(Icons.play_arrow),
style: IconButton.styleFrom(
minimumSize: const Size.square(36),
padding: EdgeInsets.zero,
),
onPressed: () => _playLibraryItem(item, libraryItems),
iconSize: 18,
icon: const Icon(Icons.play_arrow),
style: IconButton.styleFrom(
minimumSize: Size.square(context.tokens.minTouchTarget),
padding: EdgeInsets.zero,
),
);
}
+3 -3
View File
@@ -728,7 +728,7 @@ class _ExtensionItem extends StatelessWidget {
onPressed: onUpdate,
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 12),
minimumSize: const Size(0, 40),
minimumSize: Size(0, context.tokens.minTouchTarget),
),
child: Text(context.l10n.storeUpdate),
)
@@ -737,7 +737,7 @@ class _ExtensionItem extends StatelessWidget {
onPressed: null,
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 12),
minimumSize: const Size(0, 40),
minimumSize: Size(0, context.tokens.minTouchTarget),
),
child: Row(
mainAxisSize: MainAxisSize.min,
@@ -755,7 +755,7 @@ class _ExtensionItem extends StatelessWidget {
onPressed: onInstall,
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 12),
minimumSize: const Size(0, 40),
minimumSize: Size(0, context.tokens.minTouchTarget),
),
child: Text(context.l10n.storeInstall),
),
+5
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/providers/music_player_provider.dart';
import 'package:spotiflac_android/screens/now_playing_screen.dart';
import 'package:spotiflac_android/widgets/player_artwork.dart';
@@ -113,6 +114,9 @@ class _MiniPlayerState extends ConsumerState<MiniPlayer> {
),
),
IconButton(
tooltip: isPlaying
? context.l10n.actionPause
: context.l10n.tooltipPlay,
icon: isLoading
? const SizedBox.square(
dimension: 20,
@@ -126,6 +130,7 @@ class _MiniPlayerState extends ConsumerState<MiniPlayer> {
: () => controller.togglePlayPause(isPlaying),
),
IconButton(
tooltip: context.l10n.nowPlayingNextTrack,
icon: const Icon(Icons.skip_next),
onPressed: controller.next,
),
+44 -30
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:spotiflac_android/theme/app_tokens.dart';
/// Icon+label pill used in selection-mode bottom bars. Disabled state dims
/// both the fill and the content to 50% alpha.
@@ -19,42 +20,55 @@ class SelectionActionButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isDisabled = onPressed == null;
return Material(
color: isDisabled
? colorScheme.surfaceContainerHighest.withValues(alpha: 0.5)
: colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(14),
child: InkWell(
onTap: onPressed,
return Semantics(
button: true,
enabled: !isDisabled,
label: label,
excludeSemantics: true,
child: Material(
color: isDisabled
? colorScheme.surfaceContainerHighest.withValues(alpha: 0.5)
: colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(14),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: 18,
color: isDisabled
? colorScheme.onSurfaceVariant.withValues(alpha: 0.5)
: colorScheme.onSecondaryContainer,
),
const SizedBox(width: 6),
Flexible(
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
child: InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(14),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: context.tokens.minTouchTarget,
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: 18,
color: isDisabled
? colorScheme.onSurfaceVariant.withValues(alpha: 0.5)
: colorScheme.onSecondaryContainer,
),
),
const SizedBox(width: 6),
Flexible(
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: isDisabled
? colorScheme.onSurfaceVariant.withValues(
alpha: 0.5,
)
: colorScheme.onSecondaryContainer,
),
),
),
],
),
],
),
),
),
),