mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-21 15:02:21 +02:00
feat(a11y): label controls and improve settings contrast and targets
This commit is contained in:
1 parent
1e73ffea3b
commit
befcd2adec
29 files changed
+470
-276
No files matched your search
@@ -1285,11 +1285,14 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
final rowHeight = _recentDownloadsRowHeight(context);
|
||||
final header = Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Text(
|
||||
context.l10n.homeRecent,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant),
|
||||
child: Semantics(
|
||||
header: true,
|
||||
child: Text(
|
||||
context.l10n.homeRecent,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -46,10 +46,13 @@ extension _HomeTabRecentUI on _HomeTabState {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.homeRecent,
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
Semantics(
|
||||
header: true,
|
||||
child: Text(
|
||||
context.l10n.homeRecent,
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (uniqueItems.isNotEmpty)
|
||||
|
||||
@@ -153,6 +153,8 @@ class _MornyeLibraryScreenState extends ConsumerState<MornyeLibraryScreen> {
|
||||
])
|
||||
ListTile(
|
||||
title: Text(option.$2),
|
||||
selected: _sort == option.$1,
|
||||
selectedColor: Theme.of(context).colorScheme.onSurface,
|
||||
trailing: _sort == option.$1 ? const Icon(Icons.check) : null,
|
||||
onTap: () => Navigator.pop(context, option.$1),
|
||||
),
|
||||
@@ -183,6 +185,8 @@ class _MornyeLibraryScreenState extends ConsumerState<MornyeLibraryScreen> {
|
||||
])
|
||||
ListTile(
|
||||
title: Text(option.$2),
|
||||
selected: (_completeness ?? '') == option.$1,
|
||||
selectedColor: Theme.of(context).colorScheme.onSurface,
|
||||
trailing: (_completeness ?? '') == option.$1
|
||||
? const Icon(Icons.check)
|
||||
: null,
|
||||
@@ -419,12 +423,14 @@ class _MornyeLibraryScreenState extends ConsumerState<MornyeLibraryScreen> {
|
||||
24 + listInset,
|
||||
16,
|
||||
),
|
||||
child: Text(
|
||||
_query.isEmpty
|
||||
? context.l10n.libraryRecentlyAdded
|
||||
: context.l10n.searchAlbums,
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
child: Semantics(
|
||||
header: true,
|
||||
child: Text(
|
||||
_query.isEmpty
|
||||
? context.l10n.libraryRecentlyAdded
|
||||
: context.l10n.searchAlbums,
|
||||
style: Theme.of(context).textTheme.headlineSmall
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -38,6 +38,7 @@ import 'package:material_3_expressive/material_3_expressive.dart'
|
||||
M3EButtonGroup,
|
||||
M3EButtonGroupAction,
|
||||
M3EButtonGroupType,
|
||||
M3EButtonSize,
|
||||
M3EButtonStyle;
|
||||
import 'package:spotiflac_android/providers/library_browse_provider.dart';
|
||||
import 'package:spotiflac_android/services/platform_bridge.dart';
|
||||
|
||||
@@ -223,6 +223,7 @@ extension _QueueTabSections on _QueueTabState {
|
||||
if (sheetContext.isMornye)
|
||||
SettingsItem(
|
||||
title: _sortLabel(sheetContext, mode),
|
||||
selected: mode == _sortMode,
|
||||
trailing: mode == _sortMode
|
||||
? AppIcon(
|
||||
Icons.check,
|
||||
@@ -273,6 +274,8 @@ extension _QueueTabSections on _QueueTabState {
|
||||
child: M3EButtonGroup(
|
||||
type: M3EButtonGroupType.connected,
|
||||
style: M3EButtonStyle.filled,
|
||||
// Connected segments get no inflated target; keep 48 dp.
|
||||
size: M3EButtonSize.sm.copyWith(height: 48),
|
||||
selectedIndex: selected,
|
||||
selectionRequired: true,
|
||||
onSelectedIndexChanged: (index) {
|
||||
@@ -446,13 +449,16 @@ class _LibrarySectionHeader extends StatelessWidget {
|
||||
color: theme.scaffoldBackgroundColor,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 6),
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: theme.colorScheme.primary,
|
||||
child: Semantics(
|
||||
header: true,
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -982,6 +982,7 @@ class _LanguageSelector extends StatelessWidget {
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
selected: isSelected,
|
||||
trailing: isSelected
|
||||
? AppIcon(Icons.check, color: colorScheme.primary)
|
||||
: null,
|
||||
|
||||
@@ -432,6 +432,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
'opus' => context.l10n.downloadAutoConvertOpusSubtitle,
|
||||
_ => context.l10n.downloadAutoConvertMp3Subtitle,
|
||||
}),
|
||||
selected: normalizedCurrent == format,
|
||||
trailing: normalizedCurrent == format
|
||||
? AppIcon(
|
||||
Icons.check,
|
||||
@@ -471,6 +472,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
AppSheetOption(
|
||||
leading: const AppIcon(Icons.speed_outlined),
|
||||
title: Text(bitrate.replaceAll('k', ' kbps')),
|
||||
selected: normalizedCurrent == bitrate,
|
||||
trailing: normalizedCurrent == bitrate
|
||||
? AppIcon(
|
||||
Icons.check,
|
||||
@@ -511,6 +513,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
leading: const AppIcon(Icons.signal_cellular_alt),
|
||||
title: Text(context.l10n.settingsDownloadNetworkAny),
|
||||
subtitle: Text(context.l10n.downloadNetworkAnySubtitle),
|
||||
selected: current == 'any',
|
||||
trailing: current == 'any'
|
||||
? AppIcon(Icons.check, color: colorScheme.primary)
|
||||
: null,
|
||||
@@ -525,6 +528,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
leading: const AppIcon(Icons.wifi),
|
||||
title: Text(context.l10n.settingsDownloadNetworkWifiOnly),
|
||||
subtitle: Text(context.l10n.downloadNetworkWifiOnlySubtitle),
|
||||
selected: current == 'wifi_only',
|
||||
trailing: current == 'wifi_only'
|
||||
? AppIcon(Icons.check, color: colorScheme.primary)
|
||||
: null,
|
||||
@@ -572,6 +576,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
? context.l10n.concurrentDownloadsOne
|
||||
: context.l10n.concurrentDownloadsCount(count),
|
||||
),
|
||||
selected: current == count,
|
||||
trailing: current == count
|
||||
? AppIcon(Icons.check, color: colorScheme.primary)
|
||||
: null,
|
||||
@@ -884,6 +889,7 @@ class _RegionPickerSheetState extends State<_RegionPickerSheet> {
|
||||
return AppSheetOption(
|
||||
title: Text(countryName != code ? countryName : code),
|
||||
subtitle: countryName != code ? Text(code) : null,
|
||||
selected: isSelected,
|
||||
trailing: isSelected
|
||||
? AppIcon(Icons.check, color: colorScheme.primary)
|
||||
: null,
|
||||
@@ -921,6 +927,7 @@ class _QualityOption extends StatelessWidget {
|
||||
icon: icon,
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
selected: isSelected,
|
||||
trailing: isSelected
|
||||
? AppIcon(Icons.check, color: colorScheme.primary)
|
||||
: null,
|
||||
|
||||
@@ -847,6 +847,7 @@ class _SearchProviderSelector extends ConsumerWidget {
|
||||
ext.searchBehavior?.placeholder ??
|
||||
ctx.l10n.extensionsCustomSearch,
|
||||
),
|
||||
selected: settings.searchProvider == ext.id,
|
||||
trailing: settings.searchProvider == ext.id
|
||||
? AppIcon(Icons.check_circle, color: colorScheme.primary)
|
||||
: AppIcon(
|
||||
@@ -1004,6 +1005,8 @@ class _HomeFeedProviderSelector extends ConsumerWidget {
|
||||
leading: AppIcon(Icons.block, color: colorScheme.error),
|
||||
title: Text(ctx.l10n.extensionsHomeFeedOff),
|
||||
subtitle: Text(ctx.l10n.extensionsHomeFeedOffSubtitle),
|
||||
selected:
|
||||
settings.homeFeedProvider == AppSettings.homeFeedProviderOff,
|
||||
trailing:
|
||||
settings.homeFeedProvider == AppSettings.homeFeedProviderOff
|
||||
? AppIcon(Icons.check_circle, color: colorScheme.primary)
|
||||
@@ -1030,6 +1033,7 @@ class _HomeFeedProviderSelector extends ConsumerWidget {
|
||||
leading: AppIcon(Icons.extension, color: colorScheme.secondary),
|
||||
title: Text(ext.displayName),
|
||||
subtitle: Text(ctx.l10n.extensionsHomeFeedUse(ext.displayName)),
|
||||
selected: settings.homeFeedProvider == ext.id,
|
||||
trailing: settings.homeFeedProvider == ext.id
|
||||
? AppIcon(Icons.check_circle, color: colorScheme.primary)
|
||||
: AppIcon(
|
||||
|
||||
@@ -539,6 +539,7 @@ class _FilesSettingsPageState extends ConsumerState<FilesSettingsPage> {
|
||||
),
|
||||
title: Text(context.l10n.storageAutomaticFolder),
|
||||
subtitle: Text(context.l10n.storageAutomaticDownloadFolder),
|
||||
selected: !isSafMode,
|
||||
trailing: !isSafMode ? const AppIcon(Icons.check) : null,
|
||||
onTap: () async {
|
||||
Navigator.pop(ctx);
|
||||
@@ -553,6 +554,7 @@ class _FilesSettingsPageState extends ConsumerState<FilesSettingsPage> {
|
||||
leading: AppIcon(Icons.folder_open, color: colorScheme.primary),
|
||||
title: Text(context.l10n.storageSafRecommended),
|
||||
subtitle: Text(context.l10n.storageDownloadFolderHint),
|
||||
selected: isSafMode,
|
||||
trailing: isSafMode ? const AppIcon(Icons.check) : null,
|
||||
onTap: () async {
|
||||
Navigator.pop(ctx);
|
||||
@@ -759,6 +761,7 @@ class _FilesSettingsPageState extends ConsumerState<FilesSettingsPage> {
|
||||
leading: AppIcon(option.$4),
|
||||
title: Text(option.$2),
|
||||
subtitle: Text(option.$3),
|
||||
selected: current == option.$1,
|
||||
trailing: current == option.$1
|
||||
? const AppIcon(Icons.check)
|
||||
: null,
|
||||
@@ -876,6 +879,7 @@ class _FolderOption extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
selected: isSelected,
|
||||
trailing: isSelected
|
||||
? AppIcon(Icons.check_circle, color: colorScheme.primary)
|
||||
: AppIcon(Icons.circle_outlined, color: colorScheme.outline),
|
||||
|
||||
@@ -1557,6 +1557,7 @@ class _AutoScanOption extends StatelessWidget {
|
||||
return AppSheetOption(
|
||||
leading: AppIcon(icon),
|
||||
title: Text(title),
|
||||
selected: selected,
|
||||
trailing: selected
|
||||
? AppIcon(Icons.check, color: colorScheme.primary)
|
||||
: null,
|
||||
|
||||
@@ -245,6 +245,7 @@ class _LogScreenState extends State<LogScreen> {
|
||||
for (final level in _levels)
|
||||
SettingsItem(
|
||||
title: level,
|
||||
selected: level == _selectedLevel,
|
||||
trailing: level == _selectedLevel
|
||||
? AppIcon(Icons.check, color: colorScheme.primary)
|
||||
: null,
|
||||
|
||||
@@ -244,6 +244,7 @@ class LyricsSettingsPage extends ConsumerWidget {
|
||||
leading: const AppIcon(Icons.audiotrack),
|
||||
title: Text(context.l10n.lyricsModeEmbed),
|
||||
subtitle: Text(context.l10n.lyricsModeEmbedSubtitle),
|
||||
selected: current == 'embed',
|
||||
trailing: current == 'embed' ? const AppIcon(Icons.check) : null,
|
||||
onTap: () {
|
||||
ref.read(settingsProvider.notifier).setLyricsMode('embed');
|
||||
@@ -254,6 +255,7 @@ class LyricsSettingsPage extends ConsumerWidget {
|
||||
leading: const AppIcon(Icons.insert_drive_file_outlined),
|
||||
title: Text(context.l10n.lyricsModeExternal),
|
||||
subtitle: Text(context.l10n.lyricsModeExternalSubtitle),
|
||||
selected: current == 'external',
|
||||
trailing: current == 'external'
|
||||
? const AppIcon(Icons.check)
|
||||
: null,
|
||||
@@ -266,6 +268,7 @@ class LyricsSettingsPage extends ConsumerWidget {
|
||||
leading: const AppIcon(Icons.library_music_outlined),
|
||||
title: Text(context.l10n.lyricsModeBoth),
|
||||
subtitle: Text(context.l10n.lyricsModeBothSubtitle),
|
||||
selected: current == 'both',
|
||||
trailing: current == 'both' ? const AppIcon(Icons.check) : null,
|
||||
onTap: () {
|
||||
ref.read(settingsProvider.notifier).setLyricsMode('both');
|
||||
|
||||
@@ -204,6 +204,7 @@ class MetadataSettingsPage extends ConsumerWidget {
|
||||
: Icons.compress_outlined,
|
||||
),
|
||||
title: Text(_getEmbeddedCoverSizeLabel(context, maxDimension)),
|
||||
selected: currentMaxDimension == maxDimension,
|
||||
trailing: currentMaxDimension == maxDimension
|
||||
? const AppIcon(Icons.check)
|
||||
: null,
|
||||
@@ -240,6 +241,7 @@ class MetadataSettingsPage extends ConsumerWidget {
|
||||
leading: const AppIcon(Icons.segment_outlined),
|
||||
title: Text(context.l10n.optionsArtistTagModeJoined),
|
||||
subtitle: Text(context.l10n.optionsArtistTagModeJoinedSubtitle),
|
||||
selected: currentMode == artistTagModeJoined,
|
||||
trailing: currentMode == artistTagModeJoined
|
||||
? const AppIcon(Icons.check)
|
||||
: null,
|
||||
@@ -256,6 +258,7 @@ class MetadataSettingsPage extends ConsumerWidget {
|
||||
subtitle: Text(
|
||||
context.l10n.optionsArtistTagModeSplitVorbisSubtitle,
|
||||
),
|
||||
selected: currentMode == artistTagModeSplitVorbis,
|
||||
trailing: currentMode == artistTagModeSplitVorbis
|
||||
? const AppIcon(Icons.check)
|
||||
: null,
|
||||
@@ -270,6 +273,7 @@ class MetadataSettingsPage extends ConsumerWidget {
|
||||
leading: const AppIcon(Icons.person_outline),
|
||||
title: Text(context.l10n.optionsArtistTagModePrimary),
|
||||
subtitle: Text(context.l10n.optionsArtistTagModePrimarySubtitle),
|
||||
selected: currentMode == artistTagModePrimary,
|
||||
trailing: currentMode == artistTagModePrimary
|
||||
? const AppIcon(Icons.check)
|
||||
: null,
|
||||
|
||||
@@ -65,6 +65,7 @@ class _PlaybackSettingsPageState extends ConsumerState<PlaybackSettingsPage> {
|
||||
icon: Icons.open_in_new,
|
||||
title: context.l10n.libraryExternalPlayer,
|
||||
subtitle: context.l10n.libraryExternalPlayerSubtitle,
|
||||
selected: settings.playerMode == 'external',
|
||||
trailing: settings.playerMode == 'external'
|
||||
? AppIcon(Icons.check, color: colorScheme.primary)
|
||||
: null,
|
||||
@@ -76,6 +77,7 @@ class _PlaybackSettingsPageState extends ConsumerState<PlaybackSettingsPage> {
|
||||
icon: Icons.play_circle_outline,
|
||||
title: context.l10n.libraryBuiltInPreviewPlayer,
|
||||
subtitle: context.l10n.libraryBuiltInPreviewPlayerSubtitle,
|
||||
selected: settings.playerMode == 'internal',
|
||||
trailing: settings.playerMode == 'internal'
|
||||
? AppIcon(Icons.check, color: colorScheme.primary)
|
||||
: null,
|
||||
|
||||
@@ -597,11 +597,14 @@ class _DonationGoalProgress extends StatelessWidget {
|
||||
),
|
||||
if (goal.showPercentage) ...[
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
percentage,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
// The progress bar already reads the percentage as its value.
|
||||
ExcludeSemantics(
|
||||
child: Text(
|
||||
percentage,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -66,7 +66,8 @@ class MornyeTheme extends ThemeExtension<MornyeTheme> {
|
||||
}
|
||||
|
||||
static const lightAccent = Color.fromRGBO(204, 46, 51, 1);
|
||||
static const darkAccent = Color.fromRGBO(224, 61, 60, 1);
|
||||
// iOS dark systemRed: accent text stays readable on #1C1C1E cells.
|
||||
static const darkAccent = Color.fromRGBO(255, 69, 58, 1);
|
||||
|
||||
static Color accentColor(MornyeAccent accent, Brightness brightness) {
|
||||
final dark = brightness == Brightness.dark;
|
||||
@@ -223,9 +224,7 @@ class MornyeTheme extends ThemeExtension<MornyeTheme> {
|
||||
brightness: brightness,
|
||||
).copyWith(
|
||||
primary: accent,
|
||||
onPrimary:
|
||||
selectedAccent == MornyeAccent.red ||
|
||||
accent.computeLuminance() < 0.179
|
||||
onPrimary: accent.computeLuminance() < 0.179
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
primaryContainer: grouped,
|
||||
|
||||
@@ -220,6 +220,7 @@ class _AccountFormDialogState extends State<_AccountFormDialog> {
|
||||
selected: option == _choices[key],
|
||||
child: AppSheetOption(
|
||||
title: Text(option),
|
||||
selected: option == _choices[key],
|
||||
trailing: option == _choices[key]
|
||||
? const Icon(Icons.check)
|
||||
: null,
|
||||
|
||||
@@ -354,6 +354,7 @@ class AppSheetOption extends StatelessWidget {
|
||||
this.onTap,
|
||||
this.contentPadding,
|
||||
this.enabled = true,
|
||||
this.selected,
|
||||
});
|
||||
|
||||
final Widget title;
|
||||
@@ -364,21 +365,37 @@ class AppSheetOption extends StatelessWidget {
|
||||
final EdgeInsetsGeometry? contentPadding;
|
||||
final bool enabled;
|
||||
|
||||
/// Announces a checkmark choice's state to screen readers; null for
|
||||
/// options that are actions rather than choices.
|
||||
final bool? selected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scheme = theme.colorScheme;
|
||||
if (!context.isMornye) {
|
||||
return ListTile(
|
||||
title: title,
|
||||
title: this.title,
|
||||
subtitle: subtitle,
|
||||
leading: leading == null ? null : symbolIconWidget(leading!),
|
||||
trailing: trailing == null ? null : symbolIconWidget(trailing!),
|
||||
onTap: onTap,
|
||||
contentPadding: contentPadding,
|
||||
enabled: enabled,
|
||||
// The checkmark already shows the choice; selection only feeds
|
||||
// screen readers, so the row keeps its ordinary text color.
|
||||
selected: selected ?? false,
|
||||
selectedColor: scheme.onSurface,
|
||||
);
|
||||
}
|
||||
final theme = Theme.of(context);
|
||||
final scheme = theme.colorScheme;
|
||||
// Annotated on the title so it merges into the option's button node.
|
||||
final title = selected == null
|
||||
? this.title
|
||||
: Semantics(
|
||||
selected: selected,
|
||||
inMutuallyExclusiveGroup: true,
|
||||
child: this.title,
|
||||
);
|
||||
Widget adaptedIcon(Widget widget, double size) {
|
||||
final (icon, color) = switch (widget) {
|
||||
Icon(:final icon?, :final color) => (icon, color),
|
||||
|
||||
@@ -176,28 +176,34 @@ class AppIconButton extends StatelessWidget {
|
||||
data: IconThemeData(size: iconSize),
|
||||
child: icon,
|
||||
);
|
||||
final button = MaterialExpressiveScope(
|
||||
child: M3EIconButton(
|
||||
icon: sized(icon),
|
||||
selectedIcon: selectedIcon == null ? null : sized(selectedIcon),
|
||||
semanticLabel: tooltip,
|
||||
onPressed: onPressed,
|
||||
isSelected: isSelected,
|
||||
variant: switch (_variant) {
|
||||
_AppIconButtonVariant.standard => M3EIconButtonVariant.standard,
|
||||
_AppIconButtonVariant.filled => M3EIconButtonVariant.filled,
|
||||
_AppIconButtonVariant.tonal => M3EIconButtonVariant.tonal,
|
||||
_AppIconButtonVariant.outlined => M3EIconButtonVariant.outlined,
|
||||
},
|
||||
decoration: M3EIconButtonDecoration(
|
||||
foregroundColor: foreground,
|
||||
backgroundColor: style?.backgroundColor,
|
||||
overlayColor: style?.overlayColor,
|
||||
side: style?.side,
|
||||
// M3EIconButton labels an outer node and leaves its inner tap target
|
||||
// unnamed; merge them so screen readers find one labeled button.
|
||||
final button = MergeSemantics(
|
||||
child: MaterialExpressiveScope(
|
||||
child: M3EIconButton(
|
||||
icon: sized(icon),
|
||||
selectedIcon: selectedIcon == null ? null : sized(selectedIcon),
|
||||
semanticLabel: tooltip,
|
||||
onPressed: onPressed,
|
||||
isSelected: isSelected,
|
||||
variant: switch (_variant) {
|
||||
_AppIconButtonVariant.standard => M3EIconButtonVariant.standard,
|
||||
_AppIconButtonVariant.filled => M3EIconButtonVariant.filled,
|
||||
_AppIconButtonVariant.tonal => M3EIconButtonVariant.tonal,
|
||||
_AppIconButtonVariant.outlined => M3EIconButtonVariant.outlined,
|
||||
},
|
||||
decoration: M3EIconButtonDecoration(
|
||||
foregroundColor: foreground,
|
||||
backgroundColor: style?.backgroundColor,
|
||||
overlayColor: style?.overlayColor,
|
||||
side: style?.side,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
final message = tooltip;
|
||||
return message == null ? button : Tooltip(message: message, child: button);
|
||||
return message == null
|
||||
? button
|
||||
: Tooltip(message: message, excludeFromSemantics: true, child: button);
|
||||
}
|
||||
}
|
||||
+88
-15
@@ -17,6 +17,9 @@ class AppSlider extends StatelessWidget {
|
||||
this.semanticFormatterCallback,
|
||||
this.trackThickness,
|
||||
this.thumbLength,
|
||||
this.centered = false,
|
||||
this.vertical = false,
|
||||
this.semanticLabel,
|
||||
});
|
||||
|
||||
final double value;
|
||||
@@ -36,10 +39,29 @@ class AppSlider extends StatelessWidget {
|
||||
/// the handle stays in proportion.
|
||||
final double? thumbLength;
|
||||
|
||||
/// Grows the active track from the middle of the range, for bipolar values
|
||||
/// such as gain or balance.
|
||||
final bool centered;
|
||||
|
||||
/// Lays the slider out bottom (min) to top (max).
|
||||
final bool vertical;
|
||||
|
||||
/// What the slider adjusts, read before its value; [label] only names the
|
||||
/// value bubble.
|
||||
final String? semanticLabel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final slider = _buildSlider(context);
|
||||
if (semanticLabel == null) return slider;
|
||||
return MergeSemantics(
|
||||
child: Semantics(label: semanticLabel, child: slider),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSlider(BuildContext context) {
|
||||
if (!materialExpressiveEnabled(context)) {
|
||||
return Slider(
|
||||
final slider = Slider(
|
||||
value: value,
|
||||
min: min,
|
||||
max: max,
|
||||
@@ -49,21 +71,72 @@ class AppSlider extends StatelessWidget {
|
||||
onChanged: onChanged,
|
||||
onChangeEnd: onChangeEnd,
|
||||
);
|
||||
return vertical ? RotatedBox(quarterTurns: 3, child: slider) : slider;
|
||||
}
|
||||
return MaterialExpressiveScope(
|
||||
child: M3ESlider(
|
||||
value: value,
|
||||
min: min,
|
||||
max: max,
|
||||
divisions: divisions,
|
||||
label: label,
|
||||
semanticFormatterCallback: semanticFormatterCallback,
|
||||
trackThickness: trackThickness,
|
||||
thumbLength: thumbLength,
|
||||
enabled: onChanged != null,
|
||||
onChanged: onChanged,
|
||||
onChangeEnd: onChangeEnd,
|
||||
),
|
||||
final enabled = onChanged != null;
|
||||
final slider = MaterialExpressiveScope(
|
||||
child: switch ((vertical, centered)) {
|
||||
(true, true) => M3ESlider.verticalCentered(
|
||||
value: value,
|
||||
min: min,
|
||||
max: max,
|
||||
divisions: divisions,
|
||||
label: label,
|
||||
semanticFormatterCallback: semanticFormatterCallback,
|
||||
trackThickness: trackThickness,
|
||||
thumbLength: thumbLength,
|
||||
enabled: enabled,
|
||||
onChanged: onChanged,
|
||||
onChangeEnd: onChangeEnd,
|
||||
),
|
||||
(true, false) => M3ESlider.vertical(
|
||||
value: value,
|
||||
min: min,
|
||||
max: max,
|
||||
divisions: divisions,
|
||||
label: label,
|
||||
semanticFormatterCallback: semanticFormatterCallback,
|
||||
trackThickness: trackThickness,
|
||||
thumbLength: thumbLength,
|
||||
enabled: enabled,
|
||||
onChanged: onChanged,
|
||||
onChangeEnd: onChangeEnd,
|
||||
),
|
||||
(false, true) => M3ESlider.centered(
|
||||
value: value,
|
||||
min: min,
|
||||
max: max,
|
||||
divisions: divisions,
|
||||
label: label,
|
||||
semanticFormatterCallback: semanticFormatterCallback,
|
||||
trackThickness: trackThickness,
|
||||
thumbLength: thumbLength,
|
||||
enabled: enabled,
|
||||
onChanged: onChanged,
|
||||
onChangeEnd: onChangeEnd,
|
||||
),
|
||||
(false, false) => M3ESlider(
|
||||
value: value,
|
||||
min: min,
|
||||
max: max,
|
||||
divisions: divisions,
|
||||
label: label,
|
||||
semanticFormatterCallback: semanticFormatterCallback,
|
||||
trackThickness: trackThickness,
|
||||
thumbLength: thumbLength,
|
||||
enabled: enabled,
|
||||
onChanged: onChanged,
|
||||
onChangeEnd: onChangeEnd,
|
||||
),
|
||||
},
|
||||
);
|
||||
// The expressive slider lays out 44 dp across; stretch it to the 48 dp
|
||||
// touch target the standard Slider already has.
|
||||
return vertical
|
||||
? ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 48),
|
||||
child: slider,
|
||||
)
|
||||
: SizedBox(height: 48, child: slider);
|
||||
}
|
||||
}
|
||||
@@ -145,17 +145,20 @@ class AppSliverHeader extends StatelessWidget {
|
||||
right: tokens.gapLg,
|
||||
bottom: tokens.gapLg,
|
||||
),
|
||||
title: Text(
|
||||
title,
|
||||
// The title grows as the header expands; without a cap a long
|
||||
// localized title overflowed instead of ellipsizing.
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: fontSize,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: context.isMornye ? -0.7 * expandRatio : null,
|
||||
color: colorScheme.onSurface,
|
||||
title: Semantics(
|
||||
header: true,
|
||||
child: Text(
|
||||
title,
|
||||
// The title grows as the header expands; without a cap a long
|
||||
// localized title overflowed instead of ellipsizing.
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: fontSize,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: context.isMornye ? -0.7 * expandRatio : null,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -47,6 +47,7 @@ class _AutoMixSettingsState extends ConsumerState<AutoMixSettings> {
|
||||
AutoMixEffect.custom => Icons.tune,
|
||||
}),
|
||||
title: Text(names[effect]!),
|
||||
selected: effect == current,
|
||||
trailing: effect == current
|
||||
? Icon(
|
||||
Icons.check,
|
||||
@@ -101,6 +102,7 @@ class _AutoMixSettingsState extends ConsumerState<AutoMixSettings> {
|
||||
max: max,
|
||||
divisions: divisions,
|
||||
label: value.toStringAsFixed(2),
|
||||
semanticLabel: label,
|
||||
onChanged: enabled ? draft : null,
|
||||
onChangeEnd: enabled ? save : null,
|
||||
),
|
||||
|
||||
@@ -60,26 +60,30 @@ class ExpressiveIconButton extends StatelessWidget {
|
||||
}
|
||||
return Tooltip(
|
||||
message: tooltip,
|
||||
child: MaterialExpressiveScope(
|
||||
child: M3EIconButton(
|
||||
icon: IconTheme.merge(
|
||||
data: IconThemeData(size: iconSize),
|
||||
child: icon,
|
||||
),
|
||||
semanticLabel: tooltip,
|
||||
onPressed: onPressed,
|
||||
isSelected: selected,
|
||||
// Widens with an expressive button group's squish.
|
||||
visualSize: Size(
|
||||
ExpressiveButtonGroupSlot.widthOf(context) ?? size,
|
||||
size,
|
||||
),
|
||||
variant: backgroundColor == null
|
||||
? M3EIconButtonVariant.standard
|
||||
: M3EIconButtonVariant.filled,
|
||||
decoration: M3EIconButtonDecoration(
|
||||
foregroundColor: foreground,
|
||||
backgroundColor: background,
|
||||
excludeFromSemantics: true,
|
||||
// One labeled node instead of a label plus an unnamed tap target.
|
||||
child: MergeSemantics(
|
||||
child: MaterialExpressiveScope(
|
||||
child: M3EIconButton(
|
||||
icon: IconTheme.merge(
|
||||
data: IconThemeData(size: iconSize),
|
||||
child: icon,
|
||||
),
|
||||
semanticLabel: tooltip,
|
||||
onPressed: onPressed,
|
||||
isSelected: selected,
|
||||
// Widens with an expressive button group's squish.
|
||||
visualSize: Size(
|
||||
ExpressiveButtonGroupSlot.widthOf(context) ?? size,
|
||||
size,
|
||||
),
|
||||
variant: backgroundColor == null
|
||||
? M3EIconButtonVariant.standard
|
||||
: M3EIconButtonVariant.filled,
|
||||
decoration: M3EIconButtonDecoration(
|
||||
foregroundColor: foreground,
|
||||
backgroundColor: background,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -206,30 +206,34 @@ class _ExpressivePlaybackTransportState
|
||||
);
|
||||
return Tooltip(
|
||||
message: tooltip,
|
||||
child: MaterialExpressiveScope(
|
||||
child: M3EIconButton(
|
||||
icon: sized(icon),
|
||||
selectedIcon: selectedIcon == null ? null : sized(selectedIcon),
|
||||
isSelected: selected,
|
||||
semanticLabel: tooltip,
|
||||
onPressed: onPressed,
|
||||
size: widget.compact ? M3EIconButtonSize.md : M3EIconButtonSize.lg,
|
||||
shape: round
|
||||
? M3EIconButtonShapeVariant.round
|
||||
: M3EIconButtonShapeVariant.square,
|
||||
variant: M3EIconButtonVariant.filled,
|
||||
matchParentConstraints: true,
|
||||
inflateHitTarget: false,
|
||||
decoration: M3EIconButtonDecoration(
|
||||
foregroundColor: WidgetStateProperty.resolveWith(
|
||||
(states) => states.contains(WidgetState.disabled)
|
||||
? scheme.onSurface.withValues(alpha: 0.38)
|
||||
: foreground,
|
||||
),
|
||||
backgroundColor: WidgetStateProperty.resolveWith(
|
||||
(states) => states.contains(WidgetState.disabled)
|
||||
? scheme.onSurface.withValues(alpha: 0.12)
|
||||
: background,
|
||||
excludeFromSemantics: true,
|
||||
// One labeled node instead of a label plus an unnamed tap target.
|
||||
child: MergeSemantics(
|
||||
child: MaterialExpressiveScope(
|
||||
child: M3EIconButton(
|
||||
icon: sized(icon),
|
||||
selectedIcon: selectedIcon == null ? null : sized(selectedIcon),
|
||||
isSelected: selected,
|
||||
semanticLabel: tooltip,
|
||||
onPressed: onPressed,
|
||||
size: widget.compact ? M3EIconButtonSize.md : M3EIconButtonSize.lg,
|
||||
shape: round
|
||||
? M3EIconButtonShapeVariant.round
|
||||
: M3EIconButtonShapeVariant.square,
|
||||
variant: M3EIconButtonVariant.filled,
|
||||
matchParentConstraints: true,
|
||||
inflateHitTarget: false,
|
||||
decoration: M3EIconButtonDecoration(
|
||||
foregroundColor: WidgetStateProperty.resolveWith(
|
||||
(states) => states.contains(WidgetState.disabled)
|
||||
? scheme.onSurface.withValues(alpha: 0.38)
|
||||
: foreground,
|
||||
),
|
||||
backgroundColor: WidgetStateProperty.resolveWith(
|
||||
(states) => states.contains(WidgetState.disabled)
|
||||
? scheme.onSurface.withValues(alpha: 0.12)
|
||||
: background,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -51,6 +51,7 @@ class LyricsSizeSetting extends ConsumerWidget {
|
||||
max: 1.4,
|
||||
divisions: 12,
|
||||
label: percent,
|
||||
semanticLabel: context.l10n.lyricsTextSize,
|
||||
semanticFormatterCallback: (value) => '${(value * 100).round()}%',
|
||||
onChanged: (value) =>
|
||||
notifier.setLyricsTextScale(value, persist: false),
|
||||
|
||||
@@ -25,7 +25,11 @@ class MornyeSegmentedControl extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
final height = MediaQuery.textScalerOf(context).scale(15) + 36;
|
||||
// At least 56 so each segment keeps a 48 dp target inside the pill inset.
|
||||
final height = (MediaQuery.textScalerOf(context).scale(15) + 36).clamp(
|
||||
56.0,
|
||||
double.infinity,
|
||||
);
|
||||
final contrastBlocked = ref.watch(
|
||||
mornyeGlassContrastBlockedProvider(MediaQuery.highContrastOf(context)),
|
||||
);
|
||||
|
||||
@@ -373,6 +373,8 @@ class _ReEnrichFieldSheetState extends State<_ReEnrichFieldSheet> {
|
||||
leading: Icon(icon),
|
||||
title: Text(title),
|
||||
subtitle: Text(subtitle),
|
||||
selected: _mode == mode,
|
||||
selectedColor: scheme.onSurface,
|
||||
trailing: _mode == mode
|
||||
? Icon(Icons.check, color: scheme.primary)
|
||||
: null,
|
||||
|
||||
+171
-149
@@ -409,6 +409,10 @@ class SettingsItem extends StatelessWidget {
|
||||
final VoidCallback? onTap;
|
||||
final bool showDivider;
|
||||
|
||||
/// Announces a checkmark choice's state to screen readers; null for rows
|
||||
/// that are not choices.
|
||||
final bool? selected;
|
||||
|
||||
const SettingsItem({
|
||||
super.key,
|
||||
this.icon,
|
||||
@@ -421,6 +425,7 @@ class SettingsItem extends StatelessWidget {
|
||||
this.trailing,
|
||||
this.onTap,
|
||||
this.showDivider = true,
|
||||
this.selected,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -431,84 +436,90 @@ class SettingsItem extends StatelessWidget {
|
||||
final content = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: onTap,
|
||||
splashColor: colorScheme.primary.withValues(alpha: 0.12),
|
||||
highlightColor: Colors.transparent,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: context.tokens.rowPaddingH,
|
||||
vertical: context.tokens.rowPaddingV,
|
||||
),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: context.tokens.rowMinHeight,
|
||||
Semantics(
|
||||
selected: selected,
|
||||
inMutuallyExclusiveGroup: selected != null,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
splashColor: colorScheme.primary.withValues(alpha: 0.12),
|
||||
highlightColor: Colors.transparent,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: context.tokens.rowPaddingH,
|
||||
vertical: context.tokens.rowPaddingV,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (showIcon) ...[
|
||||
AppIcon.adaptive(
|
||||
icon!,
|
||||
color: iconColor ?? colorScheme.onSurfaceVariant,
|
||||
size: 24,
|
||||
),
|
||||
SizedBox(width: context.tokens.rowIconGap),
|
||||
],
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: context.tokens.rowMinHeight,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (showIcon) ...[
|
||||
AppIcon.adaptive(
|
||||
icon!,
|
||||
color: iconColor ?? colorScheme.onSurfaceVariant,
|
||||
size: 24,
|
||||
),
|
||||
SizedBox(width: context.tokens.rowIconGap),
|
||||
],
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (titleTrailing != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
titleTrailing!,
|
||||
if (titleTrailing != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
titleTrailing!,
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
subtitle!,
|
||||
style:
|
||||
(context.isMornye
|
||||
? Theme.of(context).textTheme.bodySmall
|
||||
: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium)
|
||||
?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
subtitle!,
|
||||
style:
|
||||
(context.isMornye
|
||||
? Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall
|
||||
: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium)
|
||||
?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (footer != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
footer!,
|
||||
],
|
||||
],
|
||||
if (footer != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
footer!,
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (trailing != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
trailing!,
|
||||
] else if (onTap != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
AppIcon(
|
||||
context.isMornye
|
||||
? CupertinoIcons.chevron_forward
|
||||
: Icons.chevron_right,
|
||||
size: context.tokens.rowChevronSize,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
if (trailing != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
trailing!,
|
||||
] else if (onTap != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
AppIcon(
|
||||
context.isMornye
|
||||
? CupertinoIcons.chevron_forward
|
||||
: Icons.chevron_right,
|
||||
size: context.tokens.rowChevronSize,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -561,75 +572,77 @@ class SettingsSwitchItem extends StatelessWidget {
|
||||
final content = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Opacity(
|
||||
opacity: isDisabled ? 0.5 : 1.0,
|
||||
child: InkWell(
|
||||
onTap: isDisabled ? null : () => onChanged!(!value),
|
||||
splashColor: colorScheme.primary.withValues(alpha: 0.12),
|
||||
highlightColor: Colors.transparent,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: context.tokens.rowPaddingH,
|
||||
vertical: context.tokens.rowPaddingVCompact,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (showIcon) ...[
|
||||
AppIcon(
|
||||
icon,
|
||||
color: isDisabled
|
||||
? colorScheme.outline
|
||||
: colorScheme.onSurfaceVariant,
|
||||
size: 24,
|
||||
),
|
||||
SizedBox(width: context.tokens.rowIconGap),
|
||||
],
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.bodyLarge
|
||||
?.copyWith(
|
||||
color: isDisabled
|
||||
? colorScheme.outline
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (titleTrailing != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
titleTrailing!,
|
||||
],
|
||||
],
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: Theme.of(context).textTheme.bodyMedium
|
||||
?.copyWith(
|
||||
color: isDisabled
|
||||
? colorScheme.outline
|
||||
: colorScheme.onSurfaceVariant,
|
||||
// One focus stop that reads the title, subtitle and on/off state.
|
||||
MergeSemantics(
|
||||
child: Opacity(
|
||||
opacity: isDisabled ? 0.5 : 1.0,
|
||||
child: InkWell(
|
||||
onTap: isDisabled ? null : () => onChanged!(!value),
|
||||
splashColor: colorScheme.primary.withValues(alpha: 0.12),
|
||||
highlightColor: Colors.transparent,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: context.tokens.rowPaddingH,
|
||||
vertical: context.tokens.rowPaddingVCompact,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (showIcon) ...[
|
||||
AppIcon(
|
||||
icon,
|
||||
color: isDisabled
|
||||
? colorScheme.outline
|
||||
: colorScheme.onSurfaceVariant,
|
||||
size: 24,
|
||||
),
|
||||
SizedBox(width: context.tokens.rowIconGap),
|
||||
],
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.bodyLarge
|
||||
?.copyWith(
|
||||
color: isDisabled
|
||||
? colorScheme.outline
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (titleTrailing != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
titleTrailing!,
|
||||
],
|
||||
],
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: Theme.of(context).textTheme.bodyMedium
|
||||
?.copyWith(
|
||||
color: isDisabled
|
||||
? colorScheme.outline
|
||||
: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
AppSwitch(
|
||||
value: value,
|
||||
semanticLabel: title,
|
||||
onChanged: isDisabled ? null : onChanged,
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 8),
|
||||
AppSwitch(
|
||||
value: value,
|
||||
onChanged: isDisabled ? null : onChanged,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -677,7 +690,7 @@ class SettingsSectionHeader extends StatelessWidget {
|
||||
final inset = settingsHorizontalInset(constraints.maxWidth) + 16;
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(inset, 24, inset, 8),
|
||||
child: label,
|
||||
child: Semantics(header: true, child: label),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -744,7 +757,11 @@ class SettingsChoiceChip extends StatelessWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||
color: foreground,
|
||||
// Like an iOS segment, the filled chip marks the choice; accent text
|
||||
// on Mornye's grey fill falls below readable contrast.
|
||||
color: context.isMornye && isSelected
|
||||
? colorScheme.onSurface
|
||||
: foreground,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -778,18 +795,23 @@ class SettingsChoiceChip extends StatelessWidget {
|
||||
: colorScheme.primaryContainer)
|
||||
: unselectedColor,
|
||||
borderRadius: borderRadius,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: borderRadius,
|
||||
child: ConstrainedBox(
|
||||
// Keeps every chip a comfortable tap target regardless of layout.
|
||||
constraints: BoxConstraints(minHeight: tokens.minTouchTarget),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: tokens.gapMd,
|
||||
horizontal: tokens.gapMd,
|
||||
child: Semantics(
|
||||
button: true,
|
||||
selected: isSelected,
|
||||
inMutuallyExclusiveGroup: true,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: borderRadius,
|
||||
child: ConstrainedBox(
|
||||
// Keeps every chip a comfortable tap target regardless of layout.
|
||||
constraints: BoxConstraints(minHeight: tokens.minTouchTarget),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: tokens.gapMd,
|
||||
horizontal: tokens.gapMd,
|
||||
),
|
||||
child: content,
|
||||
),
|
||||
child: content,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in new issue
Block a user