mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-29 21:02:09 +02:00
Centralize existing Material and Mornye layout metrics in AppTokens and share adaptive icon selection. Enlarge the Home profile avatar to 44 dp with a 56 dp touch target.
293 lines
9.1 KiB
Dart
293 lines
9.1 KiB
Dart
import 'package:flutter/cupertino.dart';
|
|
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/mornye_chrome.dart';
|
|
|
|
/// Dialog presentation follows the selected design, including on Android.
|
|
Future<T?> showAppDialog<T>({
|
|
required BuildContext context,
|
|
required WidgetBuilder builder,
|
|
bool barrierDismissible = true,
|
|
bool useRootNavigator = true,
|
|
}) {
|
|
if (context.isMornye) {
|
|
final navigator = Navigator.of(context, rootNavigator: useRootNavigator);
|
|
final themes = InheritedTheme.capture(from: context, to: navigator.context);
|
|
return navigator.push<T>(
|
|
_MornyeDialogRoute<T>(
|
|
context: context,
|
|
builder: (context) => themes.wrap(Builder(builder: builder)),
|
|
barrierDismissible: barrierDismissible,
|
|
),
|
|
);
|
|
}
|
|
return showDialog<T>(
|
|
context: context,
|
|
builder: builder,
|
|
barrierDismissible: barrierDismissible,
|
|
useRootNavigator: useRootNavigator,
|
|
);
|
|
}
|
|
|
|
/// The confirmation glass also hosts larger dialogs such as release notes.
|
|
class AppDialogSurface extends StatelessWidget {
|
|
const AppDialogSurface({
|
|
super.key,
|
|
required this.child,
|
|
this.maxWidth = 480,
|
|
this.backgroundColor,
|
|
this.shape,
|
|
});
|
|
|
|
final Widget child;
|
|
final double maxWidth;
|
|
final Color? backgroundColor;
|
|
final ShapeBorder? shape;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final mornye = context.isMornye;
|
|
return Dialog(
|
|
backgroundColor: mornye ? Colors.transparent : backgroundColor,
|
|
surfaceTintColor: mornye ? Colors.transparent : null,
|
|
elevation: mornye ? 0 : null,
|
|
shape: shape,
|
|
insetPadding: EdgeInsets.symmetric(
|
|
horizontal: context.tokens.dialogInsetH,
|
|
vertical: 24,
|
|
),
|
|
child: ConstrainedBox(
|
|
constraints: BoxConstraints(maxWidth: maxWidth),
|
|
child: mornye
|
|
? MornyeGlassPanel.overlay(
|
|
child: SingleChildScrollView(child: child),
|
|
)
|
|
: child,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _MornyeDialogRoute<T> extends RawDialogRoute<T> {
|
|
_MornyeDialogRoute({
|
|
required BuildContext context,
|
|
required WidgetBuilder builder,
|
|
required super.barrierDismissible,
|
|
}) : super(
|
|
pageBuilder: (context, animation, secondaryAnimation) =>
|
|
builder(context),
|
|
barrierColor: CupertinoDynamicColor.resolve(
|
|
kCupertinoModalBarrierColor,
|
|
context,
|
|
),
|
|
barrierLabel: CupertinoLocalizations.of(
|
|
context,
|
|
).modalBarrierDismissLabel,
|
|
transitionDuration: MediaQuery.disableAnimationsOf(context)
|
|
? Duration.zero
|
|
: const Duration(milliseconds: 220),
|
|
);
|
|
|
|
// Use a timed exit instead of CupertinoDialogRoute's settling spring.
|
|
@override
|
|
Duration get reverseTransitionDuration => transitionDuration == Duration.zero
|
|
? Duration.zero
|
|
: const Duration(milliseconds: 160);
|
|
|
|
CurvedAnimation? _scaleAnimation;
|
|
|
|
@override
|
|
Widget buildTransitions(
|
|
BuildContext context,
|
|
Animation<double> animation,
|
|
Animation<double> secondaryAnimation,
|
|
Widget child,
|
|
) {
|
|
if (MediaQuery.disableAnimationsOf(context)) return child;
|
|
// Keep the backdrop in the page's compositing context. A fading dialog
|
|
// creates an opacity layer that hides the page from its BackdropFilter.
|
|
final scaleAnimation = _scaleAnimation ??= CurvedAnimation(
|
|
parent: animation,
|
|
curve: Curves.easeOutCubic,
|
|
reverseCurve: Curves.easeInCubic,
|
|
);
|
|
return ScaleTransition(
|
|
scale: scaleAnimation.drive(Tween<double>(begin: 0.96, end: 1)),
|
|
child: child,
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_scaleAnimation?.dispose();
|
|
super.dispose();
|
|
}
|
|
}
|
|
|
|
class AppAlertDialog extends StatelessWidget {
|
|
const AppAlertDialog({
|
|
super.key,
|
|
this.icon,
|
|
this.title,
|
|
this.content,
|
|
this.actions = const [],
|
|
this.backgroundColor,
|
|
this.shape,
|
|
});
|
|
|
|
final Widget? icon;
|
|
final Widget? title;
|
|
final Widget? content;
|
|
final List<Widget> actions;
|
|
final Color? backgroundColor;
|
|
final ShapeBorder? shape;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (!context.isMornye) {
|
|
return AlertDialog(
|
|
backgroundColor: backgroundColor,
|
|
shape: shape,
|
|
icon: icon,
|
|
title: title,
|
|
content: content,
|
|
actions: actions,
|
|
);
|
|
}
|
|
final theme = Theme.of(context);
|
|
final primaryActions = actions.where(
|
|
(action) =>
|
|
action is AppDialogAction && (action.isDestructive || action.filled),
|
|
);
|
|
final secondaryActions = actions.where(
|
|
(action) =>
|
|
action is! AppDialogAction ||
|
|
(!action.isDestructive && !action.filled),
|
|
);
|
|
final orderedActions = [...primaryActions, ...secondaryActions];
|
|
return Dialog(
|
|
backgroundColor: Colors.transparent,
|
|
surfaceTintColor: Colors.transparent,
|
|
elevation: 0,
|
|
insetPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24),
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 320),
|
|
child: MornyeGlassPanel.overlay(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
if (icon != null || title != null || content != null)
|
|
Flexible(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.fromLTRB(12, 8, 12, 20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
if (icon != null) ...[
|
|
Align(
|
|
alignment: AlignmentDirectional.centerStart,
|
|
child: icon!,
|
|
),
|
|
const SizedBox(height: 12),
|
|
],
|
|
if (title != null)
|
|
DefaultTextStyle(
|
|
style: theme.textTheme.titleLarge!.copyWith(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.w600,
|
|
color: theme.colorScheme.onSurface,
|
|
),
|
|
child: title!,
|
|
),
|
|
if (title != null && content != null)
|
|
const SizedBox(height: 10),
|
|
if (content != null)
|
|
DefaultTextStyle(
|
|
style: theme.textTheme.bodyLarge!.copyWith(
|
|
fontSize: 17,
|
|
height: 1.35,
|
|
color: theme.colorScheme.onSurface,
|
|
),
|
|
child: content!,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
for (var i = 0; i < orderedActions.length; i++) ...[
|
|
if (i > 0) const SizedBox(height: 8),
|
|
orderedActions[i],
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class AppDialogAction extends StatelessWidget {
|
|
const AppDialogAction({
|
|
super.key,
|
|
required this.onPressed,
|
|
required this.child,
|
|
this.isDestructive = false,
|
|
this.isDefault = false,
|
|
this.filled = false,
|
|
this.outlined = false,
|
|
this.style,
|
|
});
|
|
|
|
final VoidCallback? onPressed;
|
|
final Widget child;
|
|
final bool isDestructive;
|
|
final bool isDefault;
|
|
final bool filled;
|
|
final bool outlined;
|
|
final ButtonStyle? style;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (context.isMornye) {
|
|
final theme = Theme.of(context);
|
|
final scheme = theme.colorScheme;
|
|
final color = onPressed == null
|
|
? scheme.onSurfaceVariant
|
|
: isDestructive
|
|
? scheme.primary
|
|
: scheme.onSurface;
|
|
return CupertinoButton(
|
|
onPressed: onPressed,
|
|
color: MornyeTheme.controlFill(context),
|
|
disabledColor: MornyeTheme.controlFill(context, enabled: false),
|
|
borderRadius: BorderRadius.circular(28),
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
|
child: DefaultTextStyle(
|
|
textAlign: TextAlign.center,
|
|
style: theme.textTheme.bodyLarge!.copyWith(
|
|
color: color,
|
|
fontSize: 17,
|
|
fontWeight: isDestructive || isDefault || filled
|
|
? FontWeight.w600
|
|
: FontWeight.w500,
|
|
),
|
|
child: IconTheme(
|
|
data: IconThemeData(color: color, size: 20),
|
|
child: child,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
return filled
|
|
? FilledButton(onPressed: onPressed, style: style, child: child)
|
|
: outlined
|
|
? OutlinedButton(onPressed: onPressed, style: style, child: child)
|
|
: TextButton(onPressed: onPressed, style: style, child: child);
|
|
}
|
|
}
|