112 lines
2.8 KiB
Dart
112 lines
2.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
/// Constantes UI partagées (layout responsive, thème 42).
|
|
abstract final class SwiftyUi {
|
|
static const Color brand = Color(0xFF00BABC);
|
|
|
|
/// Largeur max du contenu sur tablette / paysage.
|
|
static const double contentMaxWidth = 960;
|
|
|
|
/// Largeur max vue recherche (formulaire centré).
|
|
static const double searchMaxWidth = 480;
|
|
|
|
static const double wideBreakpoint = 720;
|
|
|
|
static EdgeInsets pagePadding(BoxConstraints constraints) {
|
|
final horizontal = constraints.maxWidth >= 600 ? 32.0 : 16.0;
|
|
return EdgeInsets.fromLTRB(horizontal, 24, horizontal, 24);
|
|
}
|
|
|
|
static PreferredSizeWidget appBar({
|
|
required String title,
|
|
List<Widget>? actions,
|
|
bool showBack = false,
|
|
}) {
|
|
return AppBar(
|
|
title: Text(title),
|
|
backgroundColor: brand,
|
|
foregroundColor: Colors.black,
|
|
automaticallyImplyLeading: showBack,
|
|
actions: actions,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Corps scrollable centré avec largeur max (clavier + petits écrans).
|
|
class ResponsiveScrollBody extends StatelessWidget {
|
|
const ResponsiveScrollBody({
|
|
super.key,
|
|
required this.child,
|
|
this.maxWidth = SwiftyUi.contentMaxWidth,
|
|
this.center = true,
|
|
});
|
|
|
|
final Widget child;
|
|
final double maxWidth;
|
|
final bool center;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final content = ConstrainedBox(
|
|
constraints: BoxConstraints(maxWidth: maxWidth),
|
|
child: child,
|
|
);
|
|
|
|
return SingleChildScrollView(
|
|
padding: SwiftyUi.pagePadding(constraints),
|
|
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
|
child: center
|
|
? Align(alignment: Alignment.topCenter, child: content)
|
|
: content,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Message d'erreur centré (recherche ou profil).
|
|
class ErrorMessage extends StatelessWidget {
|
|
const ErrorMessage({super.key, required this.message});
|
|
|
|
final String message;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SelectableText(
|
|
message,
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Bouton principal avec indicateur de chargement.
|
|
class LoadingFilledButton extends StatelessWidget {
|
|
const LoadingFilledButton({
|
|
super.key,
|
|
required this.label,
|
|
required this.loading,
|
|
required this.onPressed,
|
|
});
|
|
|
|
final String label;
|
|
final bool loading;
|
|
final VoidCallback? onPressed;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return FilledButton(
|
|
onPressed: loading ? null : onPressed,
|
|
child: loading
|
|
? const SizedBox(
|
|
height: 20,
|
|
width: 20,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
)
|
|
: Text(label),
|
|
);
|
|
}
|
|
}
|