mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-30 21:29:36 +02:00
feat(material): adopt expressive controls across the app
This commit is contained in:
1 parent
a9ed5a19cd
commit
d390a214c3
23 files changed
+988
-138
No files matched your search
@@ -10,6 +10,8 @@ import 'package:spotiflac_android/theme/cover_palette.dart';
|
||||
import 'package:spotiflac_android/theme/mornye_theme.dart';
|
||||
import 'package:spotiflac_android/theme/mornye_icons.dart';
|
||||
import 'package:spotiflac_android/widgets/mornye_chrome.dart';
|
||||
import 'package:spotiflac_android/widgets/expressive_button.dart';
|
||||
import 'package:spotiflac_android/widgets/expressive_icon_button.dart';
|
||||
import 'package:spotiflac_android/utils/adaptive_layout.dart';
|
||||
|
||||
/// Collapsing album-detail header shared by the album, local-album, and
|
||||
@@ -565,32 +567,30 @@ class AlbumPlayActions extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Flexible(
|
||||
child: FilledButton.icon(
|
||||
child: ExpressiveButton(
|
||||
onPressed: onPlay,
|
||||
icon: const Icon(Icons.play_arrow, size: 20),
|
||||
label: Text(
|
||||
playLabel,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: scheme.primary,
|
||||
foregroundColor: scheme.onPrimary,
|
||||
minimumSize: Size(0, tokens.minTouchTarget),
|
||||
shape: const StadiumBorder(),
|
||||
),
|
||||
child: Text(
|
||||
playLabel,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: tokens.gapMd),
|
||||
IconButton.filledTonal(
|
||||
ExpressiveIconButton(
|
||||
tooltip: shuffleTooltip,
|
||||
onPressed: onShuffle,
|
||||
icon: const Icon(Icons.shuffle),
|
||||
style: IconButton.styleFrom(
|
||||
minimumSize: Size.square(tokens.minTouchTarget),
|
||||
backgroundColor: scheme.secondaryContainer.withValues(alpha: 0.8),
|
||||
foregroundColor: scheme.onSecondaryContainer,
|
||||
),
|
||||
size: tokens.minTouchTarget,
|
||||
backgroundColor: scheme.secondaryContainer.withValues(alpha: 0.8),
|
||||
foregroundColor: scheme.onSecondaryContainer,
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -625,10 +625,9 @@ class HeaderFilledButton extends StatelessWidget {
|
||||
prominent: !tonal,
|
||||
);
|
||||
}
|
||||
return FilledButton.icon(
|
||||
return ExpressiveButton(
|
||||
onPressed: onPressed,
|
||||
icon: Icon(icon, size: 18),
|
||||
label: Text(label, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: scheme.primary,
|
||||
foregroundColor: scheme.onPrimary,
|
||||
@@ -637,6 +636,7 @@ class HeaderFilledButton extends StatelessWidget {
|
||||
minimumSize: Size(0, tokens.minTouchTarget),
|
||||
shape: const StadiumBorder(),
|
||||
),
|
||||
child: Text(label, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -730,15 +730,13 @@ class HeaderCircleButton extends ConsumerWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
return IconButton.filledTonal(
|
||||
return ExpressiveIconButton(
|
||||
onPressed: onPressed,
|
||||
icon: Icon(icon, size: iconSize),
|
||||
tooltip: tooltip,
|
||||
style: IconButton.styleFrom(
|
||||
minimumSize: Size.square(buttonSize ?? tokens.minTouchTarget),
|
||||
backgroundColor: scheme.surfaceContainerHighest.withValues(alpha: 0.7),
|
||||
foregroundColor: iconColor ?? scheme.onSurfaceVariant,
|
||||
),
|
||||
size: buttonSize ?? tokens.minTouchTarget,
|
||||
backgroundColor: scheme.surfaceContainerHighest.withValues(alpha: 0.7),
|
||||
foregroundColor: iconColor ?? scheme.onSurfaceVariant,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:spotiflac_android/theme/mornye_icons.dart';
|
||||
import 'package:spotiflac_android/theme/mornye_theme.dart';
|
||||
import 'package:spotiflac_android/widgets/mornye_chrome.dart';
|
||||
import 'package:spotiflac_android/widgets/expressive_button.dart';
|
||||
|
||||
/// Shared sheet actions follow the selected design on every platform.
|
||||
class AppActionButton extends StatelessWidget {
|
||||
@@ -32,19 +33,14 @@ class AppActionButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!context.isMornye) {
|
||||
return outlined
|
||||
? OutlinedButton.icon(
|
||||
onPressed: onPressed,
|
||||
icon: icon,
|
||||
label: label,
|
||||
style: style,
|
||||
)
|
||||
: FilledButton.icon(
|
||||
onPressed: onPressed,
|
||||
icon: icon,
|
||||
label: label,
|
||||
style: style,
|
||||
);
|
||||
return ExpressiveButton(
|
||||
onPressed: onPressed,
|
||||
icon: icon,
|
||||
style: style,
|
||||
outlined: outlined,
|
||||
tonal: tonal,
|
||||
child: label,
|
||||
);
|
||||
}
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final prominent = isDestructive || !outlined;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_3_expressive/material_3_expressive.dart';
|
||||
import 'package:spotiflac_android/theme/material_expressive.dart';
|
||||
import 'package:spotiflac_android/theme/mornye_theme.dart';
|
||||
|
||||
/// A single tonal selection inside a sheet, without a second glass layer.
|
||||
@@ -21,6 +23,17 @@ class AppChoiceChip extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (materialExpressiveEnabled(context) && label is Text) {
|
||||
final text = label as Text;
|
||||
return MaterialExpressiveScope(
|
||||
child: M3EChip(
|
||||
label: text.data ?? text.textSpan?.toPlainText() ?? '',
|
||||
type: M3EChipType.filter,
|
||||
selected: selected,
|
||||
onPressed: onSelected == null ? null : () => onSelected!(!selected),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!context.isMornye) {
|
||||
if (singleChoice) {
|
||||
return ChoiceChip(
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_3_expressive/material_3_expressive.dart';
|
||||
import 'package:spotiflac_android/theme/material_expressive.dart';
|
||||
import 'package:spotiflac_android/theme/mornye_theme.dart';
|
||||
|
||||
/// One inline surface, matching the metadata groups and dialog controls.
|
||||
@@ -23,6 +25,30 @@ class AppContentCard extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!context.isMornye) {
|
||||
if (materialExpressiveEnabled(context) &&
|
||||
(shape == null || shape is RoundedRectangleBorder)) {
|
||||
return Padding(
|
||||
padding: CardTheme.of(context).margin ?? const EdgeInsets.all(4),
|
||||
child: MaterialExpressiveScope(
|
||||
child: M3ECard(
|
||||
variant: M3ECardVariant.filled,
|
||||
padding: EdgeInsets.zero,
|
||||
color: color,
|
||||
elevation: elevation ?? 0,
|
||||
border: shape is RoundedRectangleBorder
|
||||
? (shape as RoundedRectangleBorder).side
|
||||
: null,
|
||||
borderRadius: shape is RoundedRectangleBorder
|
||||
? (shape as RoundedRectangleBorder).borderRadius.resolve(
|
||||
Directionality.of(context),
|
||||
)
|
||||
: BorderRadius.circular(20),
|
||||
clipBehavior: clipBehavior,
|
||||
child: Material(color: Colors.transparent, child: child),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Card(
|
||||
color: color,
|
||||
shape: shape,
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_3_expressive/material_3_expressive.dart';
|
||||
import 'package:spotiflac_android/theme/material_expressive.dart';
|
||||
|
||||
/// Indeterminate loading only. Downloads/analysis keep their real progress.
|
||||
class AppLoadingIndicator extends StatelessWidget {
|
||||
const AppLoadingIndicator({
|
||||
super.key,
|
||||
this.size = 32,
|
||||
this.color,
|
||||
this.semanticLabel,
|
||||
});
|
||||
|
||||
final double size;
|
||||
final Color? color;
|
||||
final String? semanticLabel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SizedBox.square(
|
||||
dimension: size,
|
||||
child: materialExpressiveEnabled(context)
|
||||
? MaterialExpressiveScope(
|
||||
child: FittedBox(
|
||||
child: M3ELoadingIndicator(
|
||||
size: 48,
|
||||
color: color,
|
||||
semanticLabel: semanticLabel,
|
||||
),
|
||||
),
|
||||
)
|
||||
: CircularProgressIndicator(
|
||||
color: color,
|
||||
strokeWidth: 3,
|
||||
semanticsLabel: semanticLabel,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart' show CupertinoColors;
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:liquid_glass_easy/liquid_glass_easy.dart';
|
||||
import 'package:material_3_expressive/material_3_expressive.dart';
|
||||
import 'package:spotiflac_android/providers/runtime_profile_provider.dart';
|
||||
import 'package:spotiflac_android/theme/material_expressive.dart';
|
||||
import 'package:spotiflac_android/theme/mornye_theme.dart';
|
||||
import 'package:spotiflac_android/widgets/native_glass_metrics.dart';
|
||||
|
||||
@@ -24,6 +26,15 @@ class AppSwitch extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (materialExpressiveEnabled(context)) {
|
||||
return MaterialExpressiveScope(
|
||||
child: M3ESwitch(
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
semanticLabel: semanticLabel,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!context.isMornye) {
|
||||
return adaptive
|
||||
? Switch.adaptive(value: value, onChanged: onChanged)
|
||||
@@ -132,7 +143,7 @@ class AppSwitchListTile extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!context.isMornye) {
|
||||
if (!context.isMornye && !materialExpressiveEnabled(context)) {
|
||||
return adaptive
|
||||
? SwitchListTile.adaptive(
|
||||
value: value,
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_3_expressive/material_3_expressive.dart';
|
||||
import 'package:spotiflac_android/theme/material_expressive.dart';
|
||||
|
||||
/// Expressive actions retain caller colors, disabled states and flexible labels.
|
||||
class ExpressiveButton extends StatelessWidget {
|
||||
const ExpressiveButton({
|
||||
super.key,
|
||||
required this.onPressed,
|
||||
required this.child,
|
||||
this.icon,
|
||||
this.style,
|
||||
this.outlined = false,
|
||||
this.tonal = false,
|
||||
});
|
||||
|
||||
final VoidCallback? onPressed;
|
||||
final Widget child;
|
||||
final Widget? icon;
|
||||
final ButtonStyle? style;
|
||||
final bool outlined;
|
||||
final bool tonal;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final content = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (icon != null) ...[icon!, const SizedBox(width: 8)],
|
||||
Flexible(child: child),
|
||||
],
|
||||
);
|
||||
if (!materialExpressiveEnabled(context)) {
|
||||
if (outlined) {
|
||||
return OutlinedButton(
|
||||
onPressed: onPressed,
|
||||
style: style,
|
||||
child: content,
|
||||
);
|
||||
}
|
||||
if (tonal) {
|
||||
return FilledButton.tonal(
|
||||
onPressed: onPressed,
|
||||
style: style,
|
||||
child: content,
|
||||
);
|
||||
}
|
||||
return FilledButton(onPressed: onPressed, style: style, child: content);
|
||||
}
|
||||
final states = <WidgetState>{if (onPressed == null) WidgetState.disabled};
|
||||
return MaterialExpressiveScope(
|
||||
child: M3EButton(
|
||||
onPressed: onPressed,
|
||||
style: outlined
|
||||
? M3EButtonStyle.outlined
|
||||
: tonal
|
||||
? M3EButtonStyle.tonal
|
||||
: M3EButtonStyle.filled,
|
||||
decoration: M3EButtonDecoration(
|
||||
backgroundColor: style?.backgroundColor,
|
||||
foregroundColor: style?.foregroundColor,
|
||||
overlayColor: style?.overlayColor,
|
||||
side: style?.side,
|
||||
minimumSize:
|
||||
style?.minimumSize?.resolve(states) ?? const Size(48, 48),
|
||||
maximumSize: style?.maximumSize?.resolve(states),
|
||||
fixedSize: style?.fixedSize?.resolve(states),
|
||||
padding: style?.padding?.resolve(states),
|
||||
textStyle: style?.textStyle?.resolve(states),
|
||||
iconSize: style?.iconSize?.resolve(states),
|
||||
),
|
||||
child: content,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_3_expressive/material_3_expressive.dart';
|
||||
import 'package:spotiflac_android/theme/material_expressive.dart';
|
||||
|
||||
/// Compact expressive transport/header actions with the same accessible target
|
||||
/// and tooltip as the standard Material fallback.
|
||||
class ExpressiveIconButton extends StatelessWidget {
|
||||
const ExpressiveIconButton({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.tooltip,
|
||||
required this.onPressed,
|
||||
this.selected,
|
||||
this.size = 48,
|
||||
this.iconSize = 24,
|
||||
this.foregroundColor,
|
||||
this.backgroundColor,
|
||||
});
|
||||
|
||||
final Widget icon;
|
||||
final String tooltip;
|
||||
final VoidCallback? onPressed;
|
||||
final bool? selected;
|
||||
final double size;
|
||||
final double iconSize;
|
||||
final Color? foregroundColor;
|
||||
final Color? backgroundColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final foreground = WidgetStateProperty.resolveWith<Color?>((states) {
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return scheme.onSurface.withValues(alpha: 0.38);
|
||||
}
|
||||
return foregroundColor;
|
||||
});
|
||||
final background = WidgetStateProperty.resolveWith<Color?>((states) {
|
||||
if (backgroundColor == null) return Colors.transparent;
|
||||
return states.contains(WidgetState.disabled)
|
||||
? scheme.onSurface.withValues(alpha: 0.12)
|
||||
: backgroundColor;
|
||||
});
|
||||
if (!materialExpressiveEnabled(context)) {
|
||||
return IconButton(
|
||||
icon: icon,
|
||||
iconSize: iconSize,
|
||||
tooltip: tooltip,
|
||||
onPressed: onPressed,
|
||||
isSelected: selected,
|
||||
style: ButtonStyle(
|
||||
minimumSize: WidgetStatePropertyAll(Size.square(size)),
|
||||
foregroundColor: foreground,
|
||||
backgroundColor: background,
|
||||
),
|
||||
);
|
||||
}
|
||||
return Tooltip(
|
||||
message: tooltip,
|
||||
child: MaterialExpressiveScope(
|
||||
child: M3EIconButton(
|
||||
icon: IconTheme.merge(
|
||||
data: IconThemeData(size: iconSize),
|
||||
child: icon,
|
||||
),
|
||||
semanticLabel: tooltip,
|
||||
onPressed: onPressed,
|
||||
isSelected: selected,
|
||||
visualSize: Size.square(size),
|
||||
variant: backgroundColor == null
|
||||
? M3EIconButtonVariant.standard
|
||||
: M3EIconButtonVariant.filled,
|
||||
decoration: M3EIconButtonDecoration(
|
||||
foregroundColor: foreground,
|
||||
backgroundColor: background,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_3_expressive/material_3_expressive.dart';
|
||||
import 'package:spotiflac_android/theme/material_expressive.dart';
|
||||
|
||||
class ExpressiveNavigationBar extends StatelessWidget {
|
||||
const ExpressiveNavigationBar({
|
||||
super.key,
|
||||
required this.destinations,
|
||||
required this.selectedIndex,
|
||||
required this.onDestinationSelected,
|
||||
required this.backgroundColor,
|
||||
this.isTablet = false,
|
||||
});
|
||||
|
||||
final List<NavigationDestination> destinations;
|
||||
final int selectedIndex;
|
||||
final ValueChanged<int> onDestinationSelected;
|
||||
final Color backgroundColor;
|
||||
final bool isTablet;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!materialExpressiveEnabled(context)) {
|
||||
return NavigationBar(
|
||||
selectedIndex: selectedIndex,
|
||||
onDestinationSelected: onDestinationSelected,
|
||||
animationDuration: Duration.zero,
|
||||
elevation: 0,
|
||||
height: isTablet ? 72 : 64,
|
||||
backgroundColor: backgroundColor,
|
||||
destinations: destinations,
|
||||
);
|
||||
}
|
||||
return MaterialExpressiveScope(
|
||||
child: ColoredBox(
|
||||
color: backgroundColor,
|
||||
// Use the app's scaled MediaQuery insets, rather than raw view metrics.
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: M3ENavigationBar(
|
||||
selectedIndex: selectedIndex,
|
||||
onDestinationSelected: onDestinationSelected,
|
||||
size: isTablet ? M3ENavBarSize.medium : M3ENavBarSize.small,
|
||||
autoLayout: false,
|
||||
safeArea: false,
|
||||
padding: EdgeInsets.zero,
|
||||
backgroundColor: backgroundColor,
|
||||
destinations: [
|
||||
for (final destination in destinations)
|
||||
_NavigationDestination(
|
||||
icon: destination.icon,
|
||||
selectedIcon: destination.selectedIcon,
|
||||
label: destination.label,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NavigationDestination extends M3ENavigationBarDestination {
|
||||
const _NavigationDestination({
|
||||
required super.icon,
|
||||
required super.selectedIcon,
|
||||
required super.label,
|
||||
});
|
||||
|
||||
// The package also merges its visible label into the button semantics.
|
||||
// Let that text (and any badge) provide the name exactly once.
|
||||
@override
|
||||
String get resolvedSemanticLabel => '';
|
||||
}
|
||||
@@ -2,9 +2,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:spotiflac_android/theme/app_tokens.dart';
|
||||
import 'package:spotiflac_android/theme/mornye_theme.dart';
|
||||
import 'package:spotiflac_android/widgets/app_action_button.dart';
|
||||
import 'package:spotiflac_android/widgets/expressive_button.dart';
|
||||
|
||||
/// Icon+label pill used in selection-mode bottom bars. Disabled state dims
|
||||
/// both the fill and the content to 50% alpha.
|
||||
/// Icon+label pill used in selection-mode bottom bars, with the active theme's
|
||||
/// disabled tones and room for a two-line label.
|
||||
class SelectionActionButton extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
@@ -31,58 +32,24 @@ class SelectionActionButton extends StatelessWidget {
|
||||
fontSize: 15,
|
||||
);
|
||||
}
|
||||
final isDisabled = onPressed == null;
|
||||
return Semantics(
|
||||
button: true,
|
||||
enabled: !isDisabled,
|
||||
enabled: onPressed != null,
|
||||
label: label,
|
||||
onTap: onPressed,
|
||||
excludeSemantics: true,
|
||||
child: Material(
|
||||
color: isDisabled
|
||||
? colorScheme.surfaceContainerHighest.withValues(alpha: 0.5)
|
||||
: colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
child: ExpressiveButton(
|
||||
tonal: true,
|
||||
onPressed: onPressed,
|
||||
icon: Icon(icon, size: 18),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: colorScheme.secondaryContainer,
|
||||
foregroundColor: colorScheme.onSecondaryContainer,
|
||||
minimumSize: Size(48, context.tokens.minTouchTarget),
|
||||
padding: const EdgeInsets.all(12),
|
||||
textStyle: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600),
|
||||
),
|
||||
child: Text(label, maxLines: 2, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart' show CupertinoIcons;
|
||||
import 'package:material_3_expressive/material_3_expressive.dart';
|
||||
import 'package:spotiflac_android/theme/material_expressive.dart';
|
||||
import 'package:spotiflac_android/widgets/app_switch.dart';
|
||||
import 'package:spotiflac_android/theme/app_tokens.dart';
|
||||
import 'package:spotiflac_android/theme/mornye_theme.dart';
|
||||
@@ -236,6 +238,32 @@ class SettingsGroup extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
if (materialExpressiveEnabled(context)) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) => Padding(
|
||||
padding:
|
||||
margin ??
|
||||
EdgeInsets.symmetric(
|
||||
horizontal:
|
||||
16 +
|
||||
(constraints.hasBoundedWidth
|
||||
? wideInsetForWidth(constraints.maxWidth)
|
||||
: 0),
|
||||
vertical: 4,
|
||||
),
|
||||
child: MaterialExpressiveScope(
|
||||
child: M3ECard(
|
||||
variant: M3ECardVariant.filled,
|
||||
color: cardColor,
|
||||
padding: EdgeInsets.zero,
|
||||
borderRadius: context.tokens.borderRadiusCard,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Explicit caller margin wins as-is. Otherwise center on wide surfaces
|
||||
// using the incoming constraint (not screen width) so groups nested in an
|
||||
// already clamped box, e.g. a bottom sheet, are not over-inset.
|
||||
|
||||
Reference in new issue
Block a user