Copie du code de la maquette #2

This commit is contained in:
2025-07-31 10:55:12 +02:00
parent 3d8510c58b
commit 2a9883a149
120 changed files with 11343 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
+33
View File
@@ -0,0 +1,33 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "ea121f8859e4b13e47a8f845e4586164519588bc"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
- platform: web
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
- platform: windows
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
+16
View File
@@ -0,0 +1,16 @@
# petitspas
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
+28
View File
@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
@@ -0,0 +1,32 @@
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Suppression de l'activité pour image_cropper -->
<!--
<activity
android:name="com.yalantis.ucrop.UCropActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
-->
<!-- Don't delete the meta-data below. -->
<meta-data
@@ -0,0 +1,44 @@
package io.flutter.plugins;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import io.flutter.Log;
import io.flutter.embedding.engine.FlutterEngine;
/**
* Generated file. Do not edit.
* This file is generated by the Flutter tool based on the
* plugins that support the Android platform.
*/
@Keep
public final class GeneratedPluginRegistrant {
private static final String TAG = "GeneratedPluginRegistrant";
public static void registerWith(@NonNull FlutterEngine flutterEngine) {
try {
flutterEngine.getPlugins().add(new io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin());
} catch (Exception e) {
Log.e(TAG, "Error registering plugin flutter_plugin_android_lifecycle, io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin", e);
}
try {
flutterEngine.getPlugins().add(new io.flutter.plugins.imagepicker.ImagePickerPlugin());
} catch (Exception e) {
Log.e(TAG, "Error registering plugin image_picker_android, io.flutter.plugins.imagepicker.ImagePickerPlugin", e);
}
try {
flutterEngine.getPlugins().add(new io.flutter.plugins.pathprovider.PathProviderPlugin());
} catch (Exception e) {
Log.e(TAG, "Error registering plugin path_provider_android, io.flutter.plugins.pathprovider.PathProviderPlugin", e);
}
try {
flutterEngine.getPlugins().add(new io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin());
} catch (Exception e) {
Log.e(TAG, "Error registering plugin shared_preferences_android, io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin", e);
}
try {
flutterEngine.getPlugins().add(new io.flutter.plugins.urllauncher.UrlLauncherPlugin());
} catch (Exception e) {
Log.e(TAG, "Error registering plugin url_launcher_android, io.flutter.plugins.urllauncher.UrlLauncherPlugin", e);
}
}
}
+1
View File
@@ -0,0 +1 @@
flutter.sdk=C:\\Users\\marti\\dev\\flutter
Binary file not shown.

After

Width:  |  Height:  |  Size: 656 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 754 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 784 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 767 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 851 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 850 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 908 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 909 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 890 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

+43
View File
@@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; // Import pour la localisation
// import 'package:provider/provider.dart'; // Supprimer Provider
import 'navigation/app_router.dart';
// import 'theme/app_theme.dart'; // Supprimer AppTheme
// import 'theme/theme_provider.dart'; // Supprimer ThemeProvider
void main() {
runApp(const MyApp()); // Exécution simple
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
// Pas besoin de Provider.of ici
return MaterialApp(
title: 'P\'titsPas',
theme: ThemeData.light().copyWith( // Utiliser un thème simple par défaut
textTheme: GoogleFonts.meriendaTextTheme(
ThemeData.light().textTheme,
),
// TODO: Définir les couleurs principales si besoin
),
localizationsDelegates: const [ // Configuration pour la localisation
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [ // Langues supportées
Locale('fr', 'FR'), // Français
// Locale('en', 'US'), // Anglais, si besoin
],
locale: const Locale('fr', 'FR'), // Forcer la locale française par défaut
initialRoute: AppRouter.login,
onGenerateRoute: AppRouter.generateRoute,
debugShowCheckedModeBanner: false,
);
}
}
+25
View File
@@ -0,0 +1,25 @@
enum CardColorVertical {
red('assets/cards/card_red.png'),
pink('assets/cards/card_pink.png'),
peach('assets/cards/card_peach.png'),
lime('assets/cards/card_lime.png'),
lavender('assets/cards/card_lavender.png'),
green('assets/cards/card_green.png'),
blue('assets/cards/card_blue.png');
final String path;
const CardColorVertical(this.path);
}
enum CardColorHorizontal {
red('assets/cards/card_red_h.png'),
pink('assets/cards/card_pink_h.png'),
peach('assets/cards/card_peach_h.png'),
lime('assets/cards/card_lime_h.png'),
lavender('assets/cards/card_lavender_h.png'),
green('assets/cards/card_green_h.png'),
blue('assets/cards/card_blue_h.png');
final String path;
const CardColorHorizontal(this.path);
}
+35
View File
@@ -0,0 +1,35 @@
class AppUser {
final String id;
final String email;
final String role;
final DateTime createdAt;
final DateTime updatedAt;
AppUser({
required this.id,
required this.email,
required this.role,
required this.createdAt,
required this.updatedAt,
});
factory AppUser.fromJson(Map<String, dynamic> json) {
return AppUser(
id: json['id'] as String,
email: json['email'] as String,
role: json['role'] as String,
createdAt: DateTime.parse(json['createdAt'] as String),
updatedAt: DateTime.parse(json['updatedAt'] as String),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'email': email,
'role': role,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(),
};
}
}
@@ -0,0 +1,97 @@
import 'dart:io'; // Pour File
import '../models/card_assets.dart'; // Import de l'enum CardColorVertical
class ParentData {
String firstName;
String lastName;
String address; // Rue et numéro
String postalCode; // Ajout
String city; // Ajout
String phone;
String email;
String password; // Peut-être pas nécessaire pour le récap, mais pour la création initiale si
File? profilePicture; // Chemin ou objet File
ParentData({
this.firstName = '',
this.lastName = '',
this.address = '', // Rue
this.postalCode = '', // Ajout
this.city = '', // Ajout
this.phone = '',
this.email = '',
this.password = '',
this.profilePicture,
});
}
class ChildData {
String firstName;
String lastName;
String dob; // Date de naissance ou prévisionnelle
bool photoConsent;
bool multipleBirth;
bool isUnbornChild;
File? imageFile;
CardColorVertical cardColor; // Nouveau champ pour la couleur de la carte
ChildData({
this.firstName = '',
this.lastName = '',
this.dob = '',
this.photoConsent = false,
this.multipleBirth = false,
this.isUnbornChild = false,
this.imageFile,
required this.cardColor, // Rendre requis dans le constructeur
});
}
class UserRegistrationData {
ParentData parent1;
ParentData? parent2; // Optionnel
List<ChildData> children;
String motivationText;
bool cguAccepted;
UserRegistrationData({
ParentData? parent1Data,
this.parent2,
List<ChildData>? childrenData,
this.motivationText = '',
this.cguAccepted = false,
}) : parent1 = parent1Data ?? ParentData(),
children = childrenData ?? [];
// Méthode pour ajouter/mettre à jour le parent 1
void updateParent1(ParentData data) {
parent1 = data;
}
// Méthode pour ajouter/mettre à jour le parent 2
void updateParent2(ParentData? data) {
parent2 = data;
}
// Méthode pour ajouter un enfant
void addChild(ChildData child) {
children.add(child);
}
// Méthode pour mettre à jour un enfant (si nécessaire plus tard)
void updateChild(int index, ChildData child) {
if (index >= 0 && index < children.length) {
children[index] = child;
}
}
// Mettre à jour la motivation
void updateMotivation(String text) {
motivationText = text;
}
// Accepter les CGU
void acceptCGU() {
cguAccepted = true;
}
}
+105
View File
@@ -0,0 +1,105 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../screens/auth/login_screen.dart';
import '../screens/auth/register_choice_screen.dart';
import '../screens/auth/parent_register_step1_screen.dart';
import '../screens/auth/parent_register_step2_screen.dart';
import '../screens/auth/parent_register_step3_screen.dart';
import '../screens/auth/parent_register_step4_screen.dart';
import '../screens/auth/parent_register_step5_screen.dart';
import '../screens/home/home_screen.dart';
import '../models/user_registration_data.dart';
class AppRouter {
static const String login = '/login';
static const String registerChoice = '/register-choice';
static const String parentRegisterStep1 = '/parent-register/step1';
static const String parentRegisterStep2 = '/parent-register/step2';
static const String parentRegisterStep3 = '/parent-register/step3';
static const String parentRegisterStep4 = '/parent-register/step4';
static const String parentRegisterStep5 = '/parent-register/step5';
static const String home = '/home';
static Route<dynamic> generateRoute(RouteSettings settings) {
Widget screen;
bool slideTransition = false;
Object? args = settings.arguments;
Widget buildErrorScreen(String step) {
print("Erreur: Données UserRegistrationData manquantes ou de mauvais type pour l'étape $step");
return const ParentRegisterStep1Screen();
}
switch (settings.name) {
case login:
screen = const LoginPage();
break;
case registerChoice:
screen = const RegisterChoiceScreen();
slideTransition = true;
break;
case parentRegisterStep1:
screen = const ParentRegisterStep1Screen();
slideTransition = true;
break;
case parentRegisterStep2:
if (args is UserRegistrationData) {
screen = ParentRegisterStep2Screen(registrationData: args);
} else {
screen = buildErrorScreen('2');
}
slideTransition = true;
break;
case parentRegisterStep3:
if (args is UserRegistrationData) {
screen = ParentRegisterStep3Screen(registrationData: args);
} else {
screen = buildErrorScreen('3');
}
slideTransition = true;
break;
case parentRegisterStep4:
if (args is UserRegistrationData) {
screen = ParentRegisterStep4Screen(registrationData: args);
} else {
screen = buildErrorScreen('4');
}
slideTransition = true;
break;
case parentRegisterStep5:
if (args is UserRegistrationData) {
screen = ParentRegisterStep5Screen(registrationData: args);
} else {
screen = buildErrorScreen('5');
}
slideTransition = true;
break;
case home:
screen = const HomeScreen();
break;
default:
screen = Scaffold(
body: Center(
child: Text('Route non définie : ${settings.name}'),
),
);
}
if (slideTransition) {
return PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => screen,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
const begin = Offset(1.0, 0.0);
const end = Offset.zero;
const curve = Curves.easeInOut;
var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
var offsetAnimation = animation.drive(tween);
return SlideTransition(position: offsetAnimation, child: child);
},
transitionDuration: const Duration(milliseconds: 400),
);
} else {
return MaterialPageRoute(builder: (_) => screen);
}
}
}
+399
View File
@@ -0,0 +1,399 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:url_launcher/url_launcher.dart';
import 'package:p_tits_pas/services/bug_report_service.dart';
import 'package:go_router/go_router.dart';
import '../../widgets/image_button.dart';
import '../../widgets/custom_app_text_field.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
String? _validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre email';
}
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
return 'Veuillez entrer un email valide';
}
return null;
}
String? _validatePassword(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre mot de passe';
}
if (value.length < 6) {
return 'Le mot de passe doit contenir au moins 6 caractères';
}
return null;
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.transparent,
body: LayoutBuilder(
builder: (context, constraints) {
// Version desktop (web)
if (kIsWeb) {
final w = constraints.maxWidth;
final h = constraints.maxHeight;
return FutureBuilder(
future: _getImageDimensions(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final imageDimensions = snapshot.data!;
final imageHeight = h;
final imageWidth = imageHeight * (imageDimensions.width / imageDimensions.height);
final remainingWidth = w - imageWidth;
final leftMargin = remainingWidth / 4;
return Stack(
children: [
// Fond en papier
Positioned.fill(
child: Image.asset(
'assets/images/paper2.png',
fit: BoxFit.cover,
repeat: ImageRepeat.repeat,
),
),
// Image principale
Positioned(
left: leftMargin,
top: 0,
height: imageHeight,
width: imageWidth,
child: Image.asset(
'assets/images/river_logo_desktop.png',
fit: BoxFit.contain,
),
),
// Formulaire dans le cadran en bas à droite
Positioned(
right: 0,
bottom: 0,
width: w * 0.6, // 60% de la largeur de l'écran
height: h * 0.5, // 50% de la hauteur de l'écran
child: Padding(
padding: EdgeInsets.all(w * 0.02), // 2% de padding
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Champs côte à côte
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: CustomAppTextField(
controller: _emailController,
labelText: 'Email',
hintText: 'Votre adresse email',
validator: _validateEmail,
style: CustomAppTextFieldStyle.lavande,
fieldHeight: 53,
fieldWidth: double.infinity,
),
),
const SizedBox(width: 20),
Expanded(
child: CustomAppTextField(
controller: _passwordController,
labelText: 'Mot de passe',
hintText: 'Votre mot de passe',
obscureText: true,
validator: _validatePassword,
style: CustomAppTextFieldStyle.jaune,
fieldHeight: 53,
fieldWidth: double.infinity,
),
),
],
),
const SizedBox(height: 20),
// Bouton centré
Center(
child: ImageButton(
bg: 'assets/images/btn_green.png',
width: 300,
height: 40,
text: 'Se connecter',
textColor: const Color(0xFF2D6A4F),
onPressed: () {
if (_formKey.currentState?.validate() ?? false) {
// TODO: Implémenter la logique de connexion
}
},
),
),
const SizedBox(height: 10),
// Lien mot de passe oublié
Center(
child: TextButton(
onPressed: () {
// TODO: Implémenter la logique de récupération de mot de passe
},
child: Text(
'Mot de passe oublié ?',
style: GoogleFonts.merienda(
fontSize: 14,
color: const Color(0xFF2D6A4F),
decoration: TextDecoration.underline,
),
),
),
),
const SizedBox(height: 10),
// Lien de création de compte
Center(
child: TextButton(
onPressed: () {
Navigator.pushNamed(context, '/register-choice');
},
child: Text(
'Créer un compte',
style: GoogleFonts.merienda(
fontSize: 16,
color: const Color(0xFF2D6A4F),
decoration: TextDecoration.underline,
),
),
),
),
const SizedBox(height: 20), // Réduit l'espacement en bas
],
),
),
),
),
// Pied de page
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8.0),
decoration: BoxDecoration(
color: Colors.transparent,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_FooterLink(
text: 'Contact support',
onTap: () async {
final Uri emailLaunchUri = Uri(
scheme: 'mailto',
path: 'support@supernounou.local',
);
if (await canLaunchUrl(emailLaunchUri)) {
await launchUrl(emailLaunchUri);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Impossible d\'ouvrir le client mail',
style: GoogleFonts.merienda(),
),
),
);
}
},
),
_FooterLink(
text: 'Signaler un bug',
onTap: () {
_showBugReportDialog(context);
},
),
_FooterLink(
text: 'Mentions légales',
onTap: () {
Navigator.pushNamed(context, '/legal');
},
),
_FooterLink(
text: 'Politique de confidentialité',
onTap: () {
Navigator.pushNamed(context, '/privacy');
},
),
],
),
),
),
],
);
},
);
}
// Version mobile (à implémenter)
return const Center(
child: Text('Version mobile à implémenter'),
);
},
),
);
}
void _showBugReportDialog(BuildContext context) {
final TextEditingController controller = TextEditingController();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(
'Signaler un bug',
style: GoogleFonts.merienda(),
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: controller,
maxLines: 5,
decoration: InputDecoration(
hintText: 'Décrivez le problème rencontré...',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'Annuler',
style: GoogleFonts.merienda(),
),
),
TextButton(
onPressed: () async {
if (controller.text.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Veuillez décrire le problème',
style: GoogleFonts.merienda(),
),
),
);
return;
}
try {
await BugReportService.sendReport(controller.text);
if (context.mounted) {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Rapport envoyé avec succès',
style: GoogleFonts.merienda(),
),
),
);
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Erreur lors de l\'envoi du rapport',
style: GoogleFonts.merienda(),
),
),
);
}
}
},
child: Text(
'Envoyer',
style: GoogleFonts.merienda(),
),
),
],
),
);
}
Future<ImageDimensions> _getImageDimensions() async {
final image = Image.asset('assets/images/river_logo_desktop.png');
final completer = Completer<ImageDimensions>();
image.image.resolve(const ImageConfiguration()).addListener(
ImageStreamListener((info, _) {
completer.complete(ImageDimensions(
width: info.image.width.toDouble(),
height: info.image.height.toDouble(),
));
}),
);
return completer.future;
}
}
class ImageDimensions {
final double width;
final double height;
ImageDimensions({required this.width, required this.height});
}
// ───────────────────────────────────────────────────────────────
// Lien du pied de page
// ───────────────────────────────────────────────────────────────
class _FooterLink extends StatelessWidget {
final String text;
final VoidCallback onTap;
const _FooterLink({
required this.text,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
text,
style: GoogleFonts.merienda(
fontSize: 14,
color: Colors.black87,
decoration: TextDecoration.underline,
),
),
),
);
}
}
@@ -0,0 +1,226 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'dart:math' as math; // Pour la rotation du chevron
import '../../models/user_registration_data.dart'; // Import du modèle de données
import '../../utils/data_generator.dart'; // Import du générateur de données
import '../../widgets/custom_app_text_field.dart'; // Import du widget CustomAppTextField
import '../../models/card_assets.dart'; // Import des enums de cartes
class ParentRegisterStep1Screen extends StatefulWidget {
const ParentRegisterStep1Screen({super.key});
@override
State<ParentRegisterStep1Screen> createState() => _ParentRegisterStep1ScreenState();
}
class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
final _formKey = GlobalKey<FormState>();
late UserRegistrationData _registrationData;
// Contrôleurs pour les champs (restauration CP et Ville)
final _lastNameController = TextEditingController();
final _firstNameController = TextEditingController();
final _phoneController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
final _addressController = TextEditingController(); // Rue seule
final _postalCodeController = TextEditingController(); // Restauré
final _cityController = TextEditingController(); // Restauré
@override
void initState() {
super.initState();
_registrationData = UserRegistrationData();
_generateAndFillData();
}
void _generateAndFillData() {
final String genFirstName = DataGenerator.firstName();
final String genLastName = DataGenerator.lastName();
// Utilisation des méthodes publiques de DataGenerator
_addressController.text = DataGenerator.address();
_postalCodeController.text = DataGenerator.postalCode();
_cityController.text = DataGenerator.city();
_firstNameController.text = genFirstName;
_lastNameController.text = genLastName;
_phoneController.text = DataGenerator.phone();
_emailController.text = DataGenerator.email(genFirstName, genLastName);
_passwordController.text = DataGenerator.password();
_confirmPasswordController.text = _passwordController.text;
}
@override
void dispose() {
_lastNameController.dispose();
_firstNameController.dispose();
_phoneController.dispose();
_emailController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
_addressController.dispose();
_postalCodeController.dispose();
_cityController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
return Scaffold(
body: Stack(
children: [
// Fond papier
Positioned.fill(
child: Image.asset(
'assets/images/paper2.png',
fit: BoxFit.cover,
repeat: ImageRepeat.repeat,
),
),
// Contenu centré
Center(
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Indicateur d'étape (à rendre dynamique)
Text(
'Étape 1/5',
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54),
),
const SizedBox(height: 10),
// Texte d'instruction
Text(
'Informations du Parent Principal',
style: GoogleFonts.merienda(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
// Carte jaune contenant le formulaire
Container(
width: screenSize.width * 0.6,
padding: const EdgeInsets.symmetric(vertical: 50, horizontal: 50),
constraints: const BoxConstraints(minHeight: 570),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(CardColorHorizontal.peach.path),
fit: BoxFit.fill,
),
),
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Expanded(flex: 12, child: CustomAppTextField(controller: _lastNameController, labelText: 'Nom', hintText: 'Votre nom de famille', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
Expanded(flex: 12, child: CustomAppTextField(controller: _firstNameController, labelText: 'Prénom', hintText: 'Votre prénom', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
],
),
const SizedBox(height: 20),
Row(
children: [
Expanded(flex: 12, child: CustomAppTextField(controller: _phoneController, labelText: 'Téléphone', keyboardType: TextInputType.phone, hintText: 'Votre numéro de téléphone', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
Expanded(flex: 12, child: CustomAppTextField(controller: _emailController, labelText: 'Email', keyboardType: TextInputType.emailAddress, hintText: 'Votre adresse e-mail', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
],
),
const SizedBox(height: 20),
Row(
children: [
Expanded(flex: 12, child: CustomAppTextField(controller: _passwordController, labelText: 'Mot de passe', obscureText: true, hintText: 'Créez votre mot de passe', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, validator: (value) {
if (value == null || value.isEmpty) return 'Mot de passe requis';
if (value.length < 6) return '6 caractères minimum';
return null;
})),
Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
Expanded(flex: 12, child: CustomAppTextField(controller: _confirmPasswordController, labelText: 'Confirmation', obscureText: true, hintText: 'Confirmez le mot de passe', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, validator: (value) {
if (value == null || value.isEmpty) return 'Confirmation requise';
if (value != _passwordController.text) return 'Ne correspond pas';
return null;
})),
],
),
const SizedBox(height: 20),
CustomAppTextField(
controller: _addressController,
labelText: 'Adresse (N° et Rue)',
hintText: 'Numéro et nom de votre rue',
style: CustomAppTextFieldStyle.beige,
fieldWidth: double.infinity,
),
const SizedBox(height: 20),
Row(
children: [
Expanded(flex: 1, child: CustomAppTextField(controller: _postalCodeController, labelText: 'Code Postal', keyboardType: TextInputType.number, hintText: 'Code postal', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
const SizedBox(width: 20),
Expanded(flex: 4, child: CustomAppTextField(controller: _cityController, labelText: 'Ville', hintText: 'Votre ville', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
],
),
],
),
),
),
],
),
),
),
// Chevron de navigation gauche (Retour)
Positioned(
top: screenSize.height / 2 - 20, // Centré verticalement
left: 40,
child: IconButton(
icon: Transform(
alignment: Alignment.center,
transform: Matrix4.rotationY(math.pi), // Inverse horizontalement
child: Image.asset('assets/images/chevron_right.png', height: 40),
),
onPressed: () => Navigator.pop(context), // Retour à l'écran de choix
tooltip: 'Retour',
),
),
// Chevron de navigation droit (Suivant)
Positioned(
top: screenSize.height / 2 - 20, // Centré verticalement
right: 40,
child: IconButton(
icon: Image.asset('assets/images/chevron_right.png', height: 40),
onPressed: () {
if (_formKey.currentState?.validate() ?? false) {
_registrationData.updateParent1(
ParentData(
firstName: _firstNameController.text,
lastName: _lastNameController.text,
address: _addressController.text, // Rue
postalCode: _postalCodeController.text, // Ajout
city: _cityController.text, // Ajout
phone: _phoneController.text,
email: _emailController.text,
password: _passwordController.text,
)
);
Navigator.pushNamed(context, '/parent-register/step2', arguments: _registrationData);
}
},
tooltip: 'Suivant',
),
),
],
),
);
}
}
@@ -0,0 +1,255 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'dart:math' as math; // Pour la rotation du chevron
import '../../models/user_registration_data.dart'; // Import du modèle
import '../../utils/data_generator.dart'; // Import du générateur
import '../../widgets/custom_app_text_field.dart'; // Import du widget
import '../../models/card_assets.dart'; // Import des enums de cartes
class ParentRegisterStep2Screen extends StatefulWidget {
final UserRegistrationData registrationData; // Accepte les données de l'étape 1
const ParentRegisterStep2Screen({super.key, required this.registrationData});
@override
State<ParentRegisterStep2Screen> createState() => _ParentRegisterStep2ScreenState();
}
class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
final _formKey = GlobalKey<FormState>();
late UserRegistrationData _registrationData; // Copie locale pour modification
bool _addParent2 = true; // Pour le test, on ajoute toujours le parent 2
bool _sameAddressAsParent1 = false; // Peut être généré aléatoirement aussi
// Contrôleurs pour les champs du parent 2 (restauration CP et Ville)
final _lastNameController = TextEditingController();
final _firstNameController = TextEditingController();
final _phoneController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
final _addressController = TextEditingController(); // Rue seule
final _postalCodeController = TextEditingController(); // Restauré
final _cityController = TextEditingController(); // Restauré
@override
void initState() {
super.initState();
_registrationData = widget.registrationData; // Récupère les données de l'étape 1
if (_addParent2) {
_generateAndFillParent2Data();
}
}
void _generateAndFillParent2Data() {
final String genFirstName = DataGenerator.firstName();
final String genLastName = DataGenerator.lastName();
_firstNameController.text = genFirstName;
_lastNameController.text = genLastName;
_phoneController.text = DataGenerator.phone();
_emailController.text = DataGenerator.email(genFirstName, genLastName);
_passwordController.text = DataGenerator.password();
_confirmPasswordController.text = _passwordController.text;
_sameAddressAsParent1 = DataGenerator.boolean();
if (!_sameAddressAsParent1) {
// Générer adresse, CP, Ville séparément
_addressController.text = DataGenerator.address();
_postalCodeController.text = DataGenerator.postalCode();
_cityController.text = DataGenerator.city();
} else {
// Vider les champs si même adresse (seront désactivés)
_addressController.clear();
_postalCodeController.clear();
_cityController.clear();
}
}
@override
void dispose() {
_lastNameController.dispose();
_firstNameController.dispose();
_phoneController.dispose();
_emailController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
_addressController.dispose();
_postalCodeController.dispose();
_cityController.dispose();
super.dispose();
}
bool get _parent2FieldsEnabled => _addParent2;
bool get _addressFieldsEnabled => _addParent2 && !_sameAddressAsParent1;
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
return Scaffold(
body: Stack(
children: [
Positioned.fill(
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat),
),
Center(
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Étape 2/5', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
const SizedBox(height: 10),
Text(
'Informations du Deuxième Parent (Optionnel)',
style: GoogleFonts.merienda(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black87),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
Container(
width: screenSize.width * 0.6,
padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 50),
decoration: BoxDecoration(
image: DecorationImage(image: AssetImage(CardColorHorizontal.blue.path), fit: BoxFit.fill),
),
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
flex: 12,
child: Row(children: [
const Icon(Icons.person_add_alt_1, size: 20), const SizedBox(width: 8),
Flexible(child: Text('Ajouter Parent 2 ?', style: GoogleFonts.merienda(fontWeight: FontWeight.bold), overflow: TextOverflow.ellipsis)),
const Spacer(),
Switch(value: _addParent2, onChanged: (val) => setState(() {
_addParent2 = val ?? false;
if (_addParent2) _generateAndFillParent2Data(); else _clearParent2Fields();
}), activeColor: Theme.of(context).primaryColor),
]),
),
Expanded(flex: 1, child: const SizedBox()),
Expanded(
flex: 12,
child: Row(children: [
Icon(Icons.home_work_outlined, size: 20, color: _addParent2 ? null : Colors.grey),
const SizedBox(width: 8),
Flexible(child: Text('Même Adresse ?', style: GoogleFonts.merienda(color: _addParent2 ? null : Colors.grey), overflow: TextOverflow.ellipsis)),
const Spacer(),
Switch(value: _sameAddressAsParent1, onChanged: _addParent2 ? (val) => setState(() {
_sameAddressAsParent1 = val ?? false;
if (_sameAddressAsParent1) {
_addressController.text = _registrationData.parent1.address;
_postalCodeController.text = _registrationData.parent1.postalCode;
_cityController.text = _registrationData.parent1.city;
} else {
_addressController.text = DataGenerator.address();
_postalCodeController.text = DataGenerator.postalCode();
_cityController.text = DataGenerator.city();
}
}) : null, activeColor: Theme.of(context).primaryColor),
]),
),
]),
const SizedBox(height: 25),
Row(
children: [
Expanded(flex: 12, child: CustomAppTextField(controller: _lastNameController, labelText: 'Nom', hintText: 'Nom du parent 2', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
Expanded(flex: 12, child: CustomAppTextField(controller: _firstNameController, labelText: 'Prénom', hintText: 'Prénom du parent 2', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
],
),
const SizedBox(height: 20),
Row(
children: [
Expanded(flex: 12, child: CustomAppTextField(controller: _phoneController, labelText: 'Téléphone', keyboardType: TextInputType.phone, hintText: 'Son téléphone', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
Expanded(flex: 12, child: CustomAppTextField(controller: _emailController, labelText: 'Email', keyboardType: TextInputType.emailAddress, hintText: 'Son email', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
],
),
const SizedBox(height: 20),
Row(
children: [
Expanded(flex: 12, child: CustomAppTextField(controller: _passwordController, labelText: 'Mot de passe', obscureText: true, hintText: 'Son mot de passe', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, validator: _addParent2 ? (v) => (v == null || v.isEmpty ? 'Requis' : (v.length < 6 ? '6 car. min' : null)) : null)),
Expanded(flex: 1, child: const SizedBox()), // Espace de 4%
Expanded(flex: 12, child: CustomAppTextField(controller: _confirmPasswordController, labelText: 'Confirmation', obscureText: true, hintText: 'Confirmer mot de passe', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, validator: _addParent2 ? (v) => (v == null || v.isEmpty ? 'Requis' : (v != _passwordController.text ? 'Différent' : null)) : null)),
],
),
const SizedBox(height: 20),
CustomAppTextField(controller: _addressController, labelText: 'Adresse (N° et Rue)', hintText: 'Son numéro et nom de rue', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity),
const SizedBox(height: 20),
Row(
children: [
Expanded(flex: 1, child: CustomAppTextField(controller: _postalCodeController, labelText: 'Code Postal', keyboardType: TextInputType.number, hintText: 'Son code postal', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
const SizedBox(width: 20),
Expanded(flex: 4, child: CustomAppTextField(controller: _cityController, labelText: 'Ville', hintText: 'Sa ville', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)),
],
),
],
),
),
),
),
],
),
),
),
Positioned(
top: screenSize.height / 2 - 20,
left: 40,
child: IconButton(
icon: Transform(alignment: Alignment.center, transform: Matrix4.rotationY(math.pi), child: Image.asset('assets/images/chevron_right.png', height: 40)),
onPressed: () => Navigator.pop(context),
tooltip: 'Retour',
),
),
Positioned(
top: screenSize.height / 2 - 20,
right: 40,
child: IconButton(
icon: Image.asset('assets/images/chevron_right.png', height: 40),
onPressed: () {
if (!_addParent2 || (_formKey.currentState?.validate() ?? false)) {
if (_addParent2) {
_registrationData.updateParent2(
ParentData(
firstName: _firstNameController.text,
lastName: _lastNameController.text,
address: _sameAddressAsParent1 ? _registrationData.parent1.address : _addressController.text,
postalCode: _sameAddressAsParent1 ? _registrationData.parent1.postalCode : _postalCodeController.text,
city: _sameAddressAsParent1 ? _registrationData.parent1.city : _cityController.text,
phone: _phoneController.text,
email: _emailController.text,
password: _passwordController.text,
)
);
} else {
_registrationData.updateParent2(null);
}
Navigator.pushNamed(context, '/parent-register/step3', arguments: _registrationData);
}
},
tooltip: 'Suivant',
),
),
],
),
);
}
void _clearParent2Fields() {
_formKey.currentState?.reset();
_lastNameController.clear(); _firstNameController.clear(); _phoneController.clear();
_emailController.clear(); _passwordController.clear(); _confirmPasswordController.clear();
_addressController.clear();
_postalCodeController.clear();
_cityController.clear();
_sameAddressAsParent1 = false;
setState(() {});
}
}
@@ -0,0 +1,487 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'dart:math' as math; // Pour la rotation du chevron
import 'package:flutter/gestures.dart'; // Pour PointerDeviceKind
import '../../widgets/hover_relief_widget.dart'; // Import du nouveau widget
import 'package:image_picker/image_picker.dart';
// import 'package:image_cropper/image_cropper.dart'; // Supprimé
import 'dart:io' show File, Platform; // Ajout de Platform
import 'package:flutter/foundation.dart' show kIsWeb; // Import pour kIsWeb
import '../../widgets/custom_app_text_field.dart'; // Import du nouveau widget TextField
import '../../widgets/app_custom_checkbox.dart'; // Import du nouveau widget Checkbox
import '../../models/user_registration_data.dart'; // Import du modèle de données
import '../../utils/data_generator.dart'; // Import du générateur
import '../../models/card_assets.dart'; // Import des enums de cartes
// La classe _ChildFormData est supprimée car on utilise ChildData du modèle
class ParentRegisterStep3Screen extends StatefulWidget {
final UserRegistrationData registrationData; // Accepte les données
const ParentRegisterStep3Screen({super.key, required this.registrationData});
@override
State<ParentRegisterStep3Screen> createState() => _ParentRegisterStep3ScreenState();
}
class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
late UserRegistrationData _registrationData; // Stocke l'état complet
final ScrollController _scrollController = ScrollController(); // Pour le défilement horizontal
bool _isScrollable = false;
bool _showLeftFade = false;
bool _showRightFade = false;
static const double _fadeExtent = 0.05; // Pourcentage de fondu
// Liste ordonnée des couleurs de cartes pour les enfants
static const List<CardColorVertical> _childCardColors = [
CardColorVertical.lavender, // Premier enfant toujours lavande
CardColorVertical.pink,
CardColorVertical.peach,
CardColorVertical.lime,
CardColorVertical.red,
CardColorVertical.green,
CardColorVertical.blue,
];
// Garder une trace des couleurs déjà utilisées
final Set<CardColorVertical> _usedColors = {};
// Utilisation de GlobalKey pour les cartes enfants si validation complexe future
// Map<int, GlobalKey<FormState>> _childFormKeys = {};
@override
void initState() {
super.initState();
_registrationData = widget.registrationData;
// Initialiser les couleurs utilisées avec les enfants existants
for (var child in _registrationData.children) {
_usedColors.add(child.cardColor);
}
// S'il n'y a pas d'enfant, en ajouter un automatiquement avec des données générées
if (_registrationData.children.isEmpty) {
_addChild();
}
_scrollController.addListener(_scrollListener);
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollListener());
}
@override
void dispose() {
_scrollController.removeListener(_scrollListener);
_scrollController.dispose();
super.dispose();
}
void _scrollListener() {
if (!_scrollController.hasClients) return;
final position = _scrollController.position;
final newIsScrollable = position.maxScrollExtent > 0.0;
final newShowLeftFade = newIsScrollable && position.pixels > (position.viewportDimension * _fadeExtent / 2);
final newShowRightFade = newIsScrollable && position.pixels < (position.maxScrollExtent - (position.viewportDimension * _fadeExtent / 2));
if (newIsScrollable != _isScrollable || newShowLeftFade != _showLeftFade || newShowRightFade != _showRightFade) {
setState(() {
_isScrollable = newIsScrollable;
_showLeftFade = newShowLeftFade;
_showRightFade = newShowRightFade;
});
}
}
void _addChild() {
setState(() {
bool isUnborn = DataGenerator.boolean();
// Trouver la première couleur non utilisée
CardColorVertical cardColor = _childCardColors.firstWhere(
(color) => !_usedColors.contains(color),
orElse: () => _childCardColors[0], // Fallback sur la première couleur si toutes sont utilisées
);
final newChild = ChildData(
lastName: _registrationData.parent1.lastName,
firstName: DataGenerator.firstName(),
dob: DataGenerator.dob(isUnborn: isUnborn),
isUnbornChild: isUnborn,
photoConsent: DataGenerator.boolean(),
multipleBirth: DataGenerator.boolean(),
cardColor: cardColor,
);
_registrationData.addChild(newChild);
_usedColors.add(cardColor);
});
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollListener();
if (_scrollController.hasClients && _scrollController.position.maxScrollExtent > 0.0) {
_scrollController.animateTo(_scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut);
}
});
}
void _removeChild(int index) {
if (_registrationData.children.length > 1 && index >= 0 && index < _registrationData.children.length) {
setState(() {
// Ne pas retirer la couleur de _usedColors pour éviter sa réutilisation
_registrationData.children.removeAt(index);
});
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollListener());
}
}
Future<void> _pickImage(int childIndex) async {
final ImagePicker picker = ImagePicker();
try {
final XFile? pickedFile = await picker.pickImage(
source: ImageSource.gallery, imageQuality: 70, maxWidth: 1024, maxHeight: 1024);
if (pickedFile != null) {
setState(() {
if (childIndex < _registrationData.children.length) {
_registrationData.children[childIndex].imageFile = File(pickedFile.path);
}
});
}
} catch (e) { print("Erreur image: $e"); }
}
Future<void> _selectDate(BuildContext context, int childIndex) async {
final ChildData currentChild = _registrationData.children[childIndex];
final DateTime now = DateTime.now();
DateTime initialDatePickerDate = now;
DateTime firstDatePickerDate = DateTime(1980); DateTime lastDatePickerDate = now;
if (currentChild.isUnbornChild) {
firstDatePickerDate = now; lastDatePickerDate = now.add(const Duration(days: 300));
if (currentChild.dob.isNotEmpty) {
try {
List<String> parts = currentChild.dob.split('/');
DateTime? parsedDate = DateTime.tryParse("${parts[2]}-${parts[1].padLeft(2, '0')}-${parts[0].padLeft(2, '0')}");
if (parsedDate != null && !parsedDate.isBefore(firstDatePickerDate) && !parsedDate.isAfter(lastDatePickerDate)) {
initialDatePickerDate = parsedDate;
}
} catch (e) {}
}
} else {
if (currentChild.dob.isNotEmpty) {
try {
List<String> parts = currentChild.dob.split('/');
DateTime? parsedDate = DateTime.tryParse("${parts[2]}-${parts[1].padLeft(2, '0')}-${parts[0].padLeft(2, '0')}");
if (parsedDate != null && !parsedDate.isBefore(firstDatePickerDate) && !parsedDate.isAfter(lastDatePickerDate)) {
initialDatePickerDate = parsedDate;
}
} catch (e) {}
}
}
final DateTime? picked = await showDatePicker(
context: context, initialDate: initialDatePickerDate, firstDate: firstDatePickerDate,
lastDate: lastDatePickerDate, locale: const Locale('fr', 'FR'),
);
if (picked != null) {
setState(() {
currentChild.dob = "${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}";
});
}
}
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
return Scaffold(
body: Stack(
children: [
Positioned.fill(
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat),
),
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Étape 3/5', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
const SizedBox(height: 10),
Text(
'Informations Enfants',
style: GoogleFonts.merienda(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black87),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 150.0),
child: SizedBox(
height: 684.0,
child: ShaderMask(
shaderCallback: (Rect bounds) {
final Color leftFade = (_isScrollable && _showLeftFade) ? Colors.transparent : Colors.black;
final Color rightFade = (_isScrollable && _showRightFade) ? Colors.transparent : Colors.black;
if (!_isScrollable) { return LinearGradient(colors: const <Color>[Colors.black, Colors.black, Colors.black, Colors.black], stops: const [0.0, _fadeExtent, 1.0 - _fadeExtent, 1.0],).createShader(bounds); }
return LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, colors: <Color>[ leftFade, Colors.black, Colors.black, rightFade ], stops: const [0.0, _fadeExtent, 1.0 - _fadeExtent, 1.0], ).createShader(bounds);
},
blendMode: BlendMode.dstIn,
child: Scrollbar(
controller: _scrollController,
thumbVisibility: true,
child: ListView.builder(
controller: _scrollController,
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 20.0),
itemCount: _registrationData.children.length + 1,
itemBuilder: (context, index) {
if (index < _registrationData.children.length) {
// Carte Enfant
return Padding(
padding: const EdgeInsets.only(right: 20.0),
child: _ChildCardWidget(
key: ValueKey(_registrationData.children[index].hashCode), // Utiliser une clé basée sur les données
childData: _registrationData.children[index],
childIndex: index,
onPickImage: () => _pickImage(index),
onDateSelect: () => _selectDate(context, index),
onFirstNameChanged: (value) => setState(() => _registrationData.children[index].firstName = value),
onLastNameChanged: (value) => setState(() => _registrationData.children[index].lastName = value),
onTogglePhotoConsent: (newValue) => setState(() => _registrationData.children[index].photoConsent = newValue),
onToggleMultipleBirth: (newValue) => setState(() => _registrationData.children[index].multipleBirth = newValue),
onToggleIsUnborn: (newValue) => setState(() {
_registrationData.children[index].isUnbornChild = newValue;
// Générer une nouvelle date si on change le statut
_registrationData.children[index].dob = DataGenerator.dob(isUnborn: newValue);
}),
onRemove: () => _removeChild(index),
canBeRemoved: _registrationData.children.length > 1,
),
);
} else {
// Bouton Ajouter
return Center(
child: HoverReliefWidget(
onPressed: _addChild,
borderRadius: BorderRadius.circular(15),
child: Image.asset('assets/images/plus.png', height: 80, width: 80),
),
);
}
},
),
),
),
),
),
const SizedBox(height: 20),
],
),
),
// Chevrons de navigation
Positioned(
top: screenSize.height / 2 - 20,
left: 40,
child: IconButton(
icon: Transform(alignment: Alignment.center, transform: Matrix4.rotationY(math.pi), child: Image.asset('assets/images/chevron_right.png', height: 40)),
onPressed: () => Navigator.pop(context),
tooltip: 'Retour',
),
),
Positioned(
top: screenSize.height / 2 - 20,
right: 40,
child: IconButton(
icon: Image.asset('assets/images/chevron_right.png', height: 40),
onPressed: () {
// TODO: Validation (si nécessaire)
Navigator.pushNamed(context, '/parent-register/step4', arguments: _registrationData);
},
tooltip: 'Suivant',
),
),
],
),
);
}
}
// Widget pour la carte enfant (adapté pour prendre ChildData et des callbacks)
class _ChildCardWidget extends StatefulWidget { // Transformé en StatefulWidget pour gérer les contrôleurs internes
final ChildData childData;
final int childIndex;
final VoidCallback onPickImage;
final VoidCallback onDateSelect;
final ValueChanged<String> onFirstNameChanged;
final ValueChanged<String> onLastNameChanged;
final ValueChanged<bool> onTogglePhotoConsent;
final ValueChanged<bool> onToggleMultipleBirth;
final ValueChanged<bool> onToggleIsUnborn;
final VoidCallback onRemove;
final bool canBeRemoved;
const _ChildCardWidget({
required Key key,
required this.childData,
required this.childIndex,
required this.onPickImage,
required this.onDateSelect,
required this.onFirstNameChanged,
required this.onLastNameChanged,
required this.onTogglePhotoConsent,
required this.onToggleMultipleBirth,
required this.onToggleIsUnborn,
required this.onRemove,
required this.canBeRemoved,
}) : super(key: key);
@override
State<_ChildCardWidget> createState() => _ChildCardWidgetState();
}
class _ChildCardWidgetState extends State<_ChildCardWidget> {
late TextEditingController _firstNameController;
late TextEditingController _lastNameController;
late TextEditingController _dobController;
@override
void initState() {
super.initState();
// Initialiser les contrôleurs avec les données du widget
_firstNameController = TextEditingController(text: widget.childData.firstName);
_lastNameController = TextEditingController(text: widget.childData.lastName);
_dobController = TextEditingController(text: widget.childData.dob);
// Ajouter des listeners pour mettre à jour les données sources via les callbacks
_firstNameController.addListener(() => widget.onFirstNameChanged(_firstNameController.text));
_lastNameController.addListener(() => widget.onLastNameChanged(_lastNameController.text));
// Pour dob, la mise à jour se fait via _selectDate, pas besoin de listener ici
}
@override
void didUpdateWidget(covariant _ChildCardWidget oldWidget) {
super.didUpdateWidget(oldWidget);
// Mettre à jour les contrôleurs si les données externes changent
// (peut arriver si on recharge l'état global)
if (widget.childData.firstName != _firstNameController.text) {
_firstNameController.text = widget.childData.firstName;
}
if (widget.childData.lastName != _lastNameController.text) {
_lastNameController.text = widget.childData.lastName;
}
if (widget.childData.dob != _dobController.text) {
_dobController.text = widget.childData.dob;
}
}
@override
void dispose() {
_firstNameController.dispose();
_lastNameController.dispose();
_dobController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final File? currentChildImage = widget.childData.imageFile;
// Utiliser la couleur de la carte de childData pour l'ombre si besoin, ou directement pour le fond
final Color baseCardColorForShadow = widget.childData.cardColor == CardColorVertical.lavender
? Colors.purple.shade200
: (widget.childData.cardColor == CardColorVertical.pink ? Colors.pink.shade200 : Colors.grey.shade200); // Placeholder pour autres couleurs
final Color initialPhotoShadow = baseCardColorForShadow.withAlpha(90);
final Color hoverPhotoShadow = baseCardColorForShadow.withAlpha(130);
return Container(
width: 345.0 * 1.1, // 379.5
height: 570.0 * 1.2, // 684.0
padding: const EdgeInsets.all(22.0 * 1.1), // 24.2
decoration: BoxDecoration(
image: DecorationImage(image: AssetImage(widget.childData.cardColor.path), fit: BoxFit.cover),
borderRadius: BorderRadius.circular(20 * 1.1), // 22
),
child: Stack(
children: [
Column(
mainAxisSize: MainAxisSize.min,
children: [
HoverReliefWidget(
onPressed: widget.onPickImage,
borderRadius: BorderRadius.circular(10),
initialShadowColor: initialPhotoShadow,
hoverShadowColor: hoverPhotoShadow,
child: SizedBox(
height: 200.0,
width: 200.0,
child: Center(
child: Padding(
padding: const EdgeInsets.all(5.0 * 1.1), // 5.5
child: currentChildImage != null
? ClipRRect(borderRadius: BorderRadius.circular(10 * 1.1), child: kIsWeb ? Image.network(currentChildImage.path, fit: BoxFit.cover) : Image.file(currentChildImage, fit: BoxFit.cover))
: Image.asset('assets/images/photo.png', fit: BoxFit.contain),
),
),
),
),
const SizedBox(height: 12.0 * 1.1), // Augmenté pour plus d'espace après la photo
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Enfant à naître ?', style: GoogleFonts.merienda(fontSize: 16 * 1.1, fontWeight: FontWeight.w600)),
Switch(value: widget.childData.isUnbornChild, onChanged: widget.onToggleIsUnborn, activeColor: Theme.of(context).primaryColor),
],
),
const SizedBox(height: 9.0 * 1.1), // 9.9
CustomAppTextField(
controller: _firstNameController,
labelText: 'Prénom',
hintText: 'Facultatif si à naître',
isRequired: !widget.childData.isUnbornChild,
fieldHeight: 55.0 * 1.1, // 60.5
),
const SizedBox(height: 6.0 * 1.1), // 6.6
CustomAppTextField(
controller: _lastNameController,
labelText: 'Nom',
hintText: 'Nom de l\'enfant',
enabled: true,
fieldHeight: 55.0 * 1.1, // 60.5
),
const SizedBox(height: 9.0 * 1.1), // 9.9
CustomAppTextField(
controller: _dobController,
labelText: widget.childData.isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance',
hintText: 'JJ/MM/AAAA',
readOnly: true,
onTap: widget.onDateSelect,
suffixIcon: Icons.calendar_today,
fieldHeight: 55.0 * 1.1, // 60.5
),
const SizedBox(height: 11.0 * 1.1), // 12.1
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AppCustomCheckbox(
label: 'Consentement photo',
value: widget.childData.photoConsent,
onChanged: widget.onTogglePhotoConsent,
checkboxSize: 22.0 * 1.1, // 24.2
),
const SizedBox(height: 6.0 * 1.1), // 6.6
AppCustomCheckbox(
label: 'Naissance multiple',
value: widget.childData.multipleBirth,
onChanged: widget.onToggleMultipleBirth,
checkboxSize: 22.0 * 1.1, // 24.2
),
],
),
],
),
if (widget.canBeRemoved)
Positioned(
top: -5, right: -5,
child: InkWell(
onTap: widget.onRemove,
customBorder: const CircleBorder(),
child: Image.asset(
'images/red_cross2.png',
width: 36,
height: 36,
fit: BoxFit.contain,
),
),
),
],
),
);
}
}
@@ -0,0 +1,217 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:p_tits_pas/widgets/custom_decorated_text_field.dart'; // Import du nouveau widget
import 'dart:math' as math; // Pour la rotation du chevron
import 'package:p_tits_pas/widgets/app_custom_checkbox.dart'; // Import de la checkbox personnalisée
// import 'package:p_tits_pas/models/placeholder_registration_data.dart'; // Remplacé
import '../../models/user_registration_data.dart'; // Import du vrai modèle
import '../../utils/data_generator.dart'; // Import du générateur
import '../../models/card_assets.dart'; // Import des enums de cartes
class ParentRegisterStep4Screen extends StatefulWidget {
final UserRegistrationData registrationData; // Accepte les données
const ParentRegisterStep4Screen({super.key, required this.registrationData});
@override
State<ParentRegisterStep4Screen> createState() => _ParentRegisterStep4ScreenState();
}
class _ParentRegisterStep4ScreenState extends State<ParentRegisterStep4Screen> {
late UserRegistrationData _registrationData; // État local
final _motivationController = TextEditingController();
bool _cguAccepted = true; // Pour le test, CGU acceptées par défaut
@override
void initState() {
super.initState();
_registrationData = widget.registrationData;
_motivationController.text = DataGenerator.motivation(); // Générer la motivation
}
@override
void dispose() {
_motivationController.dispose();
super.dispose();
}
void _showCGUModal() {
// Un long texte Lorem Ipsum pour simuler les CGU
const String loremIpsumText = '''
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut in risus volutpat libero pharetra tempor. Cras vestibulum bibendum augue. Praesent egestas leo in pede. Praesent blandit odio eu enim. Pellentesque sed dui ut augue blandit sodales. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Aliquam nibh. Mauris ac mauris sed pede pellentesque fermentum. Maecenas adipiscing ante non diam sodales hendrerit.
Ut velit mauris, egestas sed, gravida nec, ornare ut, mi. Aenean ut orci vel massa suscipit pulvinar. Nulla sollicitudin. Fusce varius, ligula non tempus aliquam, nunc turpis ullamcorper nibh, in tempus sapien eros vitae ligula. Pellentesque rhoncus nunc et augue. Integer id felis. Curabitur aliquet pellentesque diam. Integer quis metus vitae elit lobortis egestas. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Morbi vel erat non mauris convallis vehicula. Nulla et sapien. Integer tortor tellus, aliquam faucibus, convallis id, congue eu, quam. Mauris ullamcorper felis vitae erat. Proin feugiat, augue non elementum posuere, metus purus iaculis lectus, et tristique ligula justo vitae magna.
Aliquam convallis sollicitudin purus. Praesent aliquam, enim at fermentum mollis, ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum augue. Nulla tincidunt tincidunt mi. Curabitur iaculis, lorem vel rhoncus faucibus, felis magna fermentum augue, et ultricies lacus lorem varius purus. Curabitur eu amet.
Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut in risus volutpat libero pharetra tempor. Cras vestibulum bibendum augue. Praesent egestas leo in pede. Praesent blandit odio eu enim. Pellentesque sed dui ut augue blandit sodales. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Aliquam nibh. Mauris ac mauris sed pede pellentesque fermentum. Maecenas adipiscing ante non diam sodales hendrerit.
Ut velit mauris, egestas sed, gravida nec, ornare ut, mi. Aenean ut orci vel massa suscipit pulvinar. Nulla sollicitudin. Fusce varius, ligula non tempus aliquam, nunc turpis ullamcorper nibh, in tempus sapien eros vitae ligula. Pellentesque rhoncus nunc et augue. Integer id felis. Curabitur aliquet pellentesque diam. Integer quis metus vitae elit lobortis egestas. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Morbi vel erat non mauris convallis vehicula. Nulla et sapien. Integer tortor tellus, aliquam faucibus, convallis id, congue eu, quam. Mauris ullamcorper felis vitae erat. Proin feugiat, augue non elementum posuere, metus purus iaculis lectus, et tristique ligula justo vitae magna. Etiam et felis dolor.
Praesent aliquam, enim at fermentum mollis, ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum augue. Nulla tincidunt tincidunt mi. Curabitur iaculis, lorem vel rhoncus faucibus, felis magna fermentum augue, et ultricies lacus lorem varius purus. Curabitur eu amet. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.
Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.
''';
showDialog<void>(
context: context,
barrierDismissible: false, // L'utilisateur doit utiliser le bouton
builder: (BuildContext dialogContext) {
return AlertDialog(
title: Text(
'Conditions Générales d\'Utilisation',
style: GoogleFonts.merienda(fontWeight: FontWeight.bold),
),
content: SizedBox(
width: MediaQuery.of(dialogContext).size.width * 0.7, // 70% de la largeur de l'écran
height: MediaQuery.of(dialogContext).size.height * 0.6, // 60% de la hauteur de l'écran
child: SingleChildScrollView(
child: Text(
loremIpsumText,
style: GoogleFonts.merienda(fontSize: 13),
textAlign: TextAlign.justify,
),
),
),
actionsPadding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0),
actionsAlignment: MainAxisAlignment.center,
actions: <Widget>[
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(dialogContext).primaryColor,
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15),
),
child: Text(
'Valider et Accepter',
style: GoogleFonts.merienda(fontSize: 15, color: Colors.white, fontWeight: FontWeight.bold),
),
onPressed: () {
Navigator.of(dialogContext).pop(); // Ferme la modale
setState(() {
_cguAccepted = true; // Met à jour l'état
});
},
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
final cardWidth = screenSize.width * 0.6; // Largeur de la carte (60% de l'écran)
final double imageAspectRatio = 2.0; // Ratio corrigé (1024/512 = 2.0)
final cardHeight = cardWidth / imageAspectRatio;
return Scaffold(
body: Stack(
children: [
Positioned.fill(
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat),
),
Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 50.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'Étape 4/5',
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54),
),
const SizedBox(height: 20),
Text(
'Motivation de votre demande',
style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
Container(
width: cardWidth,
height: cardHeight,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(CardColorHorizontal.green.path),
fit: BoxFit.fill,
),
),
child: Padding(
padding: const EdgeInsets.all(40.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: CustomDecoratedTextField(
controller: _motivationController,
hintText: 'Écrivez ici pour motiver votre demande...',
fieldHeight: cardHeight * 0.6,
maxLines: 10,
expandDynamically: true,
fontSize: 18.0,
),
),
const SizedBox(height: 20),
GestureDetector(
onTap: () {
if (!_cguAccepted) {
_showCGUModal();
}
},
child: AppCustomCheckbox(
label: 'J\'accepte les conditions générales d\'utilisation',
value: _cguAccepted,
onChanged: (newValue) {
if (!_cguAccepted) {
_showCGUModal();
} else {
setState(() => _cguAccepted = false);
}
},
),
),
],
),
),
),
],
),
),
),
// Chevrons de navigation
Positioned(
top: screenSize.height / 2 - 20,
left: 40,
child: IconButton(
icon: Transform(alignment: Alignment.center, transform: Matrix4.rotationY(math.pi), child: Image.asset('assets/images/chevron_right.png', height: 40)),
onPressed: () => Navigator.pop(context),
tooltip: 'Retour',
),
),
Positioned(
top: screenSize.height / 2 - 20,
right: 40,
child: IconButton(
icon: Image.asset('assets/images/chevron_right.png', height: 40),
onPressed: _cguAccepted
? () {
_registrationData.updateMotivation(_motivationController.text);
_registrationData.acceptCGU();
Navigator.pushNamed(
context,
'/parent-register/step5',
arguments: _registrationData
);
}
: null,
tooltip: 'Suivant',
),
),
],
),
);
}
}
@@ -0,0 +1,464 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../models/user_registration_data.dart'; // Utilisation du vrai modèle
import '../../widgets/image_button.dart'; // Import du ImageButton
import '../../models/card_assets.dart'; // Import des enums de cartes
import 'package:flutter/foundation.dart' show kIsWeb;
import '../../widgets/custom_decorated_text_field.dart'; // Import du CustomDecoratedTextField
// Nouvelle méthode helper pour afficher un champ de type "lecture seule" stylisé
Widget _buildDisplayFieldValue(BuildContext context, String label, String value, {bool multiLine = false, double fieldHeight = 50.0, double labelFontSize = 18.0}) {
const FontWeight labelFontWeight = FontWeight.w600;
// Ne pas afficher le label si labelFontSize est 0 ou si label est vide
bool showLabel = label.isNotEmpty && labelFontSize > 0;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (showLabel)
Text(label, style: GoogleFonts.merienda(fontSize: labelFontSize, fontWeight: labelFontWeight)),
if (showLabel)
const SizedBox(height: 4),
// Utiliser Expanded si multiLine et pas de hauteur fixe, sinon Container
multiLine && fieldHeight == null
? Expanded(
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0),
decoration: BoxDecoration(
image: const DecorationImage(
image: AssetImage('assets/images/input_field_bg.png'),
fit: BoxFit.fill,
),
),
child: SingleChildScrollView( // Pour le défilement si le texte dépasse
child: Text(
value.isNotEmpty ? value : '-',
style: GoogleFonts.merienda(fontSize: labelFontSize > 0 ? labelFontSize : 18.0), // Garder une taille de texte par défaut si label caché
maxLines: null, // Permettre un nombre illimité de lignes
),
),
),
)
: Container(
width: double.infinity,
height: multiLine ? null : fieldHeight,
constraints: multiLine ? BoxConstraints(minHeight: fieldHeight) : null,
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0),
decoration: BoxDecoration(
image: const DecorationImage(
image: AssetImage('assets/images/input_field_bg.png'),
fit: BoxFit.fill,
),
),
child: Text(
value.isNotEmpty ? value : '-',
style: GoogleFonts.merienda(fontSize: labelFontSize > 0 ? labelFontSize : 18.0),
maxLines: multiLine ? null : 1,
overflow: multiLine ? TextOverflow.visible : TextOverflow.ellipsis,
),
),
],
);
}
class ParentRegisterStep5Screen extends StatelessWidget {
final UserRegistrationData registrationData;
const ParentRegisterStep5Screen({super.key, required this.registrationData});
// Méthode pour construire la carte Parent 1
Widget _buildParent1Card(BuildContext context, ParentData data) {
const double verticalSpacing = 28.0; // Espacement vertical augmenté
const double labelFontSize = 22.0; // Taille de label augmentée
List<Widget> details = [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: _buildDisplayFieldValue(context, "Nom:", data.lastName, labelFontSize: labelFontSize)),
const SizedBox(width: 20),
Expanded(child: _buildDisplayFieldValue(context, "Prénom:", data.firstName, labelFontSize: labelFontSize)),
],
),
const SizedBox(height: verticalSpacing),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: _buildDisplayFieldValue(context, "Téléphone:", data.phone, labelFontSize: labelFontSize)),
const SizedBox(width: 20),
Expanded(child: _buildDisplayFieldValue(context, "Email:", data.email, multiLine: true, labelFontSize: labelFontSize)),
],
),
const SizedBox(height: verticalSpacing),
_buildDisplayFieldValue(context, "Adresse:", "${data.address}\n${data.postalCode} ${data.city}".trim(), multiLine: true, fieldHeight: 80, labelFontSize: labelFontSize),
];
return _SummaryCard(
backgroundImagePath: CardColorHorizontal.peach.path,
title: 'Parent Principal',
content: details,
onEdit: () => Navigator.of(context).pushNamed('/parent-register/step1', arguments: registrationData),
);
}
// Méthode pour construire la carte Parent 2
Widget _buildParent2Card(BuildContext context, ParentData data) {
const double verticalSpacing = 28.0;
const double labelFontSize = 22.0;
List<Widget> details = [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: _buildDisplayFieldValue(context, "Nom:", data.lastName, labelFontSize: labelFontSize)),
const SizedBox(width: 20),
Expanded(child: _buildDisplayFieldValue(context, "Prénom:", data.firstName, labelFontSize: labelFontSize)),
],
),
const SizedBox(height: verticalSpacing),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: _buildDisplayFieldValue(context, "Téléphone:", data.phone, labelFontSize: labelFontSize)),
const SizedBox(width: 20),
Expanded(child: _buildDisplayFieldValue(context, "Email:", data.email, multiLine: true, labelFontSize: labelFontSize)),
],
),
const SizedBox(height: verticalSpacing),
_buildDisplayFieldValue(context, "Adresse:", "${data.address}\n${data.postalCode} ${data.city}".trim(), multiLine: true, fieldHeight: 80, labelFontSize: labelFontSize),
];
return _SummaryCard(
backgroundImagePath: CardColorHorizontal.blue.path,
title: 'Deuxième Parent',
content: details,
onEdit: () => Navigator.of(context).pushNamed('/parent-register/step2', arguments: registrationData),
);
}
// Méthode pour construire les cartes Enfants
List<Widget> _buildChildrenCards(BuildContext context, List<ChildData> children) {
return children.asMap().entries.map((entry) {
int index = entry.key;
ChildData child = entry.value;
CardColorHorizontal cardColorHorizontal = CardColorHorizontal.values.firstWhere(
(e) => e.name == child.cardColor.name,
orElse: () => CardColorHorizontal.lavender,
);
return Padding(
padding: const EdgeInsets.only(bottom: 20.0),
child: Stack(
children: [
AspectRatio(
aspectRatio: 2.0,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 25.0),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(cardColorHorizontal.path),
fit: BoxFit.cover,
),
borderRadius: BorderRadius.circular(15),
),
child: Column(
children: [
// Titre centré dans la carte
Row(
children: [
Expanded(
child: Text(
'Enfant ${index + 1}' + (child.isUnbornChild ? ' (à naître)' : ''),
style: GoogleFonts.merienda(fontSize: 28, fontWeight: FontWeight.w600),
textAlign: TextAlign.center,
),
),
IconButton(
icon: const Icon(Icons.edit, color: Colors.black54, size: 28),
onPressed: () {
Navigator.of(context).pushNamed(
'/parent-register/step3',
arguments: registrationData,
);
},
tooltip: 'Modifier',
),
],
),
const SizedBox(height: 18),
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// IMAGE SANS CADRE BLANC, PREND LA HAUTEUR
Expanded(
flex: 1,
child: Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(18),
child: AspectRatio(
aspectRatio: 1,
child: (child.imageFile != null)
? (kIsWeb
? Image.network(child.imageFile!.path, fit: BoxFit.cover)
: Image.file(child.imageFile!, fit: BoxFit.cover))
: Image.asset('assets/images/photo.png', fit: BoxFit.contain),
),
),
),
),
const SizedBox(width: 32),
// INFOS À DROITE (2/3)
Expanded(
flex: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildDisplayFieldValue(context, 'Prénom :', child.firstName, labelFontSize: 22.0),
const SizedBox(height: 12),
_buildDisplayFieldValue(context, 'Nom :', child.lastName, labelFontSize: 22.0),
const SizedBox(height: 12),
_buildDisplayFieldValue(context, child.isUnbornChild ? 'Date de naissance :' : 'Date de naissance :', child.dob, labelFontSize: 22.0),
],
),
),
],
),
),
const SizedBox(height: 18),
// Ligne des consentements
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
children: [
Checkbox(
value: child.photoConsent,
onChanged: null,
),
Text('Consentement photo', style: GoogleFonts.merienda(fontSize: 16)),
],
),
const SizedBox(width: 32),
Row(
children: [
Checkbox(
value: child.multipleBirth,
onChanged: null,
),
Text('Naissance multiple', style: GoogleFonts.merienda(fontSize: 16)),
],
),
],
),
],
),
),
),
],
),
);
}).toList();
}
// Méthode pour construire la carte Motivation
Widget _buildMotivationCard(BuildContext context, String motivation) {
return _SummaryCard(
backgroundImagePath: CardColorHorizontal.green.path,
title: 'Votre Motivation',
content: [
Expanded(
child: CustomDecoratedTextField(
controller: TextEditingController(text: motivation),
hintText: 'Aucune motivation renseignée.',
fieldHeight: 200,
maxLines: 10,
expandDynamically: true,
readOnly: true,
fontSize: 18.0,
),
),
],
onEdit: () => Navigator.of(context).pushNamed('/parent-register/step4', arguments: registrationData),
);
}
// Helper pour afficher une ligne de détail (police et agencement amélioré)
Widget _buildDetailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"$label: ",
style: GoogleFonts.merienda(fontSize: 18, fontWeight: FontWeight.w600),
),
Expanded(
child: Text(
value.isNotEmpty ? value : '-',
style: GoogleFonts.merienda(fontSize: 18),
softWrap: true,
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
final cardWidth = screenSize.width / 2.0; // Largeur de la carte (50% de l'écran)
final double imageAspectRatio = 2.0; // Ratio corrigé (1024/512 = 2.0)
final cardHeight = cardWidth / imageAspectRatio;
return Scaffold(
body: Stack(
children: [
Positioned.fill(
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeatY),
),
Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 40.0), // Padding horizontal supprimé ici
child: Padding( // Ajout du Padding horizontal externe
padding: EdgeInsets.symmetric(horizontal: screenSize.width / 4.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text('Étape 5/5', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
const SizedBox(height: 20),
Text('Récapitulatif de votre demande', style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), textAlign: TextAlign.center),
const SizedBox(height: 30),
_buildParent1Card(context, registrationData.parent1),
const SizedBox(height: 20),
if (registrationData.parent2 != null) ...[
_buildParent2Card(context, registrationData.parent2!),
const SizedBox(height: 20),
],
..._buildChildrenCards(context, registrationData.children),
_buildMotivationCard(context, registrationData.motivationText),
const SizedBox(height: 40),
ImageButton(
bg: 'assets/images/btn_green.png',
text: 'Soumettre ma demande',
textColor: const Color(0xFF2D6A4F),
width: 350,
height: 50,
fontSize: 18,
onPressed: () {
print("Données finales: ${registrationData.parent1.firstName}, Enfant(s): ${registrationData.children.length}");
_showConfirmationModal(context);
},
),
],
),
),
),
),
Positioned(
top: screenSize.height / 2 - 20,
left: 40,
child: IconButton(
icon: Transform.flip(flipX: true, child: Image.asset('assets/images/chevron_right.png', height: 40)),
onPressed: () => Navigator.pop(context), // Retour à l'étape 4
tooltip: 'Retour',
),
),
],
),
);
}
void _showConfirmationModal(BuildContext context) {
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: Text(
'Demande enregistrée',
style: GoogleFonts.merienda(fontWeight: FontWeight.bold),
),
content: Text(
'Votre dossier a bien été pris en compte. Un gestionnaire le validera bientôt.',
style: GoogleFonts.merienda(fontSize: 14),
),
actions: <Widget>[
TextButton(
child: Text('OK', style: GoogleFonts.merienda(fontWeight: FontWeight.bold)),
onPressed: () {
Navigator.of(dialogContext).pop(); // Ferme la modale
// TODO: Naviguer vers l'écran de connexion ou tableau de bord
Navigator.of(context).pushNamedAndRemoveUntil('/login', (Route<dynamic> route) => false);
},
),
],
);
},
);
}
}
// Widget générique _SummaryCard (ajusté)
class _SummaryCard extends StatelessWidget {
final String backgroundImagePath;
final String title;
final List<Widget> content;
final VoidCallback onEdit;
const _SummaryCard({
super.key,
required this.backgroundImagePath,
required this.title,
required this.content,
required this.onEdit,
});
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 2.0,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 25.0),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(backgroundImagePath),
fit: BoxFit.cover,
),
borderRadius: BorderRadius.circular(15),
),
child: Column(
children: [
Row(
children: [
Expanded(
child: Text(
title,
style: GoogleFonts.merienda(fontSize: 28, fontWeight: FontWeight.w600),
textAlign: TextAlign.center,
),
),
IconButton(
icon: const Icon(Icons.edit, color: Colors.black54, size: 28),
onPressed: onEdit,
tooltip: 'Modifier',
),
],
),
const SizedBox(height: 18),
Expanded(
child: Column(
children: content,
),
),
],
),
),
);
}
}
@@ -0,0 +1,162 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'dart:math' as math; // Pour la rotation du chevron
import '../../widgets/hover_relief_widget.dart'; // Import du widget générique
import '../../models/card_assets.dart'; // Import des enums de cartes
class RegisterChoiceScreen extends StatelessWidget {
const RegisterChoiceScreen({super.key});
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
return Scaffold(
body: Stack(
children: [
// Fond papier
Positioned.fill(
child: Image.asset(
'assets/images/paper2.png',
fit: BoxFit.cover,
repeat: ImageRepeat.repeat,
),
),
// Bouton Retour (chevron gauche)
Positioned(
top: 40,
left: 40,
child: IconButton(
icon: Transform(
alignment: Alignment.center,
transform: Matrix4.rotationY(math.pi),
child: Image.asset('assets/images/chevron_right.png', height: 40),
),
onPressed: () => Navigator.pop(context),
tooltip: 'Retour',
),
),
// Contenu principal en Row (Gauche / Droite)
Padding(
padding: EdgeInsets.symmetric(horizontal: screenSize.width * 0.05),
child: Row(
children: [
// Partie Gauche: Texte d'instruction centré
Expanded(
flex: 1,
child: Center(
child: Text(
'Veuillez choisir votre\ntype de compte :',
style: GoogleFonts.merienda(
fontSize: 36,
fontWeight: FontWeight.bold,
color: Colors.black87,
height: 1.5,
),
textAlign: TextAlign.center,
),
),
),
// Espace entre les deux parties
SizedBox(width: screenSize.width * 0.05),
// Partie Droite: Carte rose avec les boutons
Expanded(
flex: 1,
child: Center(
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: screenSize.height * 0.78, // Augmenté pour éviter l'overflow
),
child: AspectRatio(
aspectRatio: 2 / 3,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(CardColorVertical.pink.path),
fit: BoxFit.fill,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// Bouton "Parents" avec HoverReliefWidget appliqué uniquement à l'image
_buildChoiceButton(
context: context,
iconPath: 'assets/images/icon_parents.png',
label: 'Parents',
onPressed: () {
Navigator.pushNamed(context, '/parent-register/step1');
},
),
// Bouton "Assistante Maternelle" avec HoverReliefWidget appliqué uniquement à l'image
_buildChoiceButton(
context: context,
iconPath: 'assets/images/icon_assmat.png',
label: 'Assistante Maternelle',
onPressed: () {
// TODO: Naviguer vers l'écran d'inscription assmat
print('Choix: Assistante Maternelle');
},
),
],
),
),
),
),
),
),
],
),
),
],
),
);
}
}
// Nouvelle méthode helper pour construire les boutons de choix
Widget _buildChoiceButton({
required BuildContext context,
required String iconPath,
required String label,
required VoidCallback onPressed,
}) {
// TODO: Déterminer la couleur de base de card_rose.png et ajuster ces couleurs d'ombre
final Color baseRoseColor = Colors.pink.shade300; // Placeholder
final Color initialShadow = baseRoseColor.withAlpha(90); // Rose plus foncé et transparent pour l'ombre initiale
final Color hoverShadow = baseRoseColor.withAlpha(130); // Rose encore plus foncé pour l'ombre au survol
return Column(
mainAxisSize: MainAxisSize.min,
children: [
HoverReliefWidget(
onPressed: onPressed,
borderRadius: BorderRadius.circular(15.0),
initialShadowColor: initialShadow, // Ombre rose initiale
hoverShadowColor: hoverShadow, // Ombre rose au survol
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Image.asset(iconPath, height: 140),
),
),
const SizedBox(height: 15),
Text(
label,
style: GoogleFonts.merienda(
fontSize: 26,
fontWeight: FontWeight.w600,
color: Colors.black.withOpacity(0.85),
),
textAlign: TextAlign.center,
),
],
);
}
// --- La classe HoverChoiceButton peut maintenant être supprimée si elle n'est plus utilisée ailleurs ---
// class HoverChoiceButton extends StatefulWidget { ... }
// class _HoverChoiceButtonState extends State<HoverChoiceButton> { ... }
@@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Accueil'),
),
body: const Center(
child: Text('Bienvenue sur P\'titsPas !'),
),
);
}
}
@@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class LegalPage extends StatelessWidget {
const LegalPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
'Mentions légales',
style: GoogleFonts.merienda(),
),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Éditeur',
style: GoogleFonts.merienda(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Text(
'P\'titsPas est une application développée pour les collectivités locales.',
style: GoogleFonts.merienda(),
),
const SizedBox(height: 32),
Text(
'Hébergeur',
style: GoogleFonts.merienda(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Text(
'Les données sont hébergées sur des serveurs sécurisés en France.',
style: GoogleFonts.merienda(),
),
const SizedBox(height: 32),
Text(
'Responsable du traitement',
style: GoogleFonts.merienda(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Text(
'Le responsable du traitement des données est la collectivité locale utilisatrice de l\'application.',
style: GoogleFonts.merienda(),
),
],
),
),
);
}
}
@@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class PrivacyPage extends StatelessWidget {
const PrivacyPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
'Politique de confidentialité',
style: GoogleFonts.merienda(),
),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Protection des données personnelles',
style: GoogleFonts.merienda(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Text(
'P\'titsPas s\'engage à protéger vos données personnelles conformément au RGPD.',
style: GoogleFonts.merienda(),
),
const SizedBox(height: 32),
Text(
'Données collectées',
style: GoogleFonts.merienda(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Text(
'Les données collectées sont nécessaires au bon fonctionnement de l\'application et à la gestion des contrats de garde d\'enfants.',
style: GoogleFonts.merienda(),
),
const SizedBox(height: 32),
Text(
'Vos droits',
style: GoogleFonts.merienda(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Text(
'Vous disposez d\'un droit d\'accès, de rectification, d\'effacement et de portabilité de vos données.',
style: GoogleFonts.merienda(),
),
],
),
),
);
}
}
+42
View File
@@ -0,0 +1,42 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/user.dart';
class AuthService {
static const String _usersKey = 'users';
static const String _parentsKey = 'parents';
static const String _childrenKey = 'children';
// Méthode pour se connecter (mode démonstration)
static Future<AppUser> login(String email, String password) async {
await Future.delayed(const Duration(seconds: 1)); // Simule un délai de traitement
throw Exception('Mode démonstration - Connexion désactivée');
}
// Méthode pour s'inscrire (mode démonstration)
static Future<AppUser> register({
required String email,
required String password,
required String firstName,
required String lastName,
required String role,
}) async {
await Future.delayed(const Duration(seconds: 1)); // Simule un délai de traitement
throw Exception('Mode démonstration - Inscription désactivée');
}
// Méthode pour se déconnecter (mode démonstration)
static Future<void> logout() async {
// Ne fait rien en mode démonstration
}
// Méthode pour vérifier si l'utilisateur est connecté (mode démonstration)
static Future<bool> isLoggedIn() async {
return false; // Toujours non connecté en mode démonstration
}
// Méthode pour récupérer l'utilisateur connecté (mode démonstration)
static Future<AppUser?> getCurrentUser() async {
return null; // Aucun utilisateur en mode démonstration
}
}
@@ -0,0 +1,28 @@
import 'package:http/http.dart' as http;
import 'dart:convert';
class BugReportService {
static const String _apiUrl = 'https://api.supernounou.local/bug-reports';
static Future<void> sendReport(String description) async {
try {
final response = await http.post(
Uri.parse(_apiUrl),
headers: {
'Content-Type': 'application/json',
},
body: jsonEncode({
'description': description,
'timestamp': DateTime.now().toIso8601String(),
'platform': 'web', // TODO: Ajouter la détection de la plateforme
}),
);
if (response.statusCode != 200) {
throw Exception('Erreur lors de l\'envoi du rapport');
}
} catch (e) {
rethrow;
}
}
}
+65
View File
@@ -0,0 +1,65 @@
import 'dart:math';
class DataGenerator {
static final Random _random = Random();
static final List<String> _firstNames = [
'Alice', 'Bob', 'Charlie', 'David', 'Eva', 'Félix', 'Gabrielle', 'Hugo', 'Inès', 'Jules',
'Léa', 'Manon', 'Nathan', 'Oscar', 'Pauline', 'Quentin', 'Raphaël', 'Sophie', 'Théo', 'Victoire'
];
static final List<String> _lastNames = [
'Martin', 'Bernard', 'Dubois', 'Thomas', 'Robert', 'Richard', 'Petit', 'Durand', 'Leroy', 'Moreau',
'Simon', 'Laurent', 'Lefebvre', 'Michel', 'Garcia', 'David', 'Bertrand', 'Roux', 'Vincent', 'Fournier'
];
static final List<String> _addressSuffixes = [
'Rue de la Paix', 'Boulevard des Rêves', 'Avenue du Soleil', 'Place des Étoiles', 'Chemin des Champs'
];
static final List<String> _motivationSnippets = [
'Nous cherchons une personne de confiance.',
'Nos horaires sont atypiques.',
'Notre enfant est plein de vie.',
'Nous souhaitons une garde à temps plein.',
'Une adaptation en douceur est primordiale pour nous.',
'Nous avons hâte de vous rencontrer.',
'La pédagogie Montessori nous intéresse.'
];
static String firstName() => _firstNames[_random.nextInt(_firstNames.length)];
static String lastName() => _lastNames[_random.nextInt(_lastNames.length)];
static String address() => "${_random.nextInt(100) + 1} ${_addressSuffixes[_random.nextInt(_addressSuffixes.length)]}";
static String postalCode() => "750${_random.nextInt(10)}${_random.nextInt(10)}";
static String city() => "Paris";
static String phone() => "06${_random.nextInt(10)}${_random.nextInt(10)}${_random.nextInt(10)}${_random.nextInt(10)}${_random.nextInt(10)}${_random.nextInt(10)}${_random.nextInt(10)}${_random.nextInt(10)}";
static String email(String firstName, String lastName) => "${firstName.toLowerCase()}.${lastName.toLowerCase()}@example.com";
static String password() => "password123"; // Simple pour le test
static String dob({bool isUnborn = false}) {
final now = DateTime.now();
if (isUnborn) {
final provisionalDate = now.add(Duration(days: _random.nextInt(180) + 30)); // Entre 1 et 7 mois dans le futur
return "${provisionalDate.day.toString().padLeft(2, '0')}/${provisionalDate.month.toString().padLeft(2, '0')}/${provisionalDate.year}";
} else {
final birthYear = now.year - _random.nextInt(3); // Enfants de 0 à 2 ans
final birthMonth = _random.nextInt(12) + 1;
final birthDay = _random.nextInt(28) + 1; // Simple, évite les pbs de jours/mois
return "${birthDay.toString().padLeft(2, '0')}/${birthMonth.toString().padLeft(2, '0')}/${birthYear}";
}
}
static bool boolean() => _random.nextBool();
static String motivation() {
int count = _random.nextInt(3) + 2; // 2 à 4 phrases
List<String> chosenSnippets = [];
while(chosenSnippets.length < count) {
String snippet = _motivationSnippets[_random.nextInt(_motivationSnippets.length)];
if (!chosenSnippets.contains(snippet)) {
chosenSnippets.add(snippet);
}
}
return chosenSnippets.join(' ');
}
}
@@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class AppCustomCheckbox extends StatelessWidget {
final String label;
final bool value;
final ValueChanged<bool> onChanged;
final double checkboxSize;
final double checkmarkSizeFactor;
const AppCustomCheckbox({
super.key,
required this.label,
required this.value,
required this.onChanged,
this.checkboxSize = 20.0,
this.checkmarkSizeFactor = 1.4,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => onChanged(!value), // Inverse la valeur au clic
behavior: HitTestBehavior.opaque, // Pour s'assurer que toute la zone du Row est cliquable
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: checkboxSize,
height: checkboxSize,
child: Stack(
alignment: Alignment.center,
clipBehavior: Clip.none,
children: [
Image.asset(
'assets/images/square.png',
height: checkboxSize,
width: checkboxSize,
),
if (value)
Image.asset(
'assets/images/coche.png',
height: checkboxSize * checkmarkSizeFactor,
width: checkboxSize * checkmarkSizeFactor,
),
],
),
),
const SizedBox(width: 10),
// Utiliser Flexible pour que le texte ne cause pas d'overflow si trop long
Flexible(
child: Text(
label,
style: GoogleFonts.merienda(fontSize: 16),
overflow: TextOverflow.ellipsis, // Gérer le texte long
),
),
],
),
);
}
}
@@ -0,0 +1,140 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
// Définition de l'enum pour les styles de couleur/fond
enum CustomAppTextFieldStyle {
beige,
lavande,
jaune,
}
class CustomAppTextField extends StatefulWidget {
final TextEditingController controller;
final String labelText;
final String hintText;
final double fieldWidth;
final double fieldHeight;
final bool obscureText;
final TextInputType keyboardType;
final String? Function(String?)? validator;
final CustomAppTextFieldStyle style;
final bool isRequired;
final bool enabled;
final bool readOnly;
final VoidCallback? onTap;
final IconData? suffixIcon;
final double labelFontSize;
final double inputFontSize;
const CustomAppTextField({
super.key,
required this.controller,
required this.labelText,
this.hintText = '',
this.fieldWidth = 300.0,
this.fieldHeight = 53.0,
this.obscureText = false,
this.keyboardType = TextInputType.text,
this.validator,
this.style = CustomAppTextFieldStyle.beige,
this.isRequired = false,
this.enabled = true,
this.readOnly = false,
this.onTap,
this.suffixIcon,
this.labelFontSize = 18.0,
this.inputFontSize = 18.0,
});
@override
State<CustomAppTextField> createState() => _CustomAppTextFieldState();
}
class _CustomAppTextFieldState extends State<CustomAppTextField> {
String getBackgroundImagePath() {
switch (widget.style) {
case CustomAppTextFieldStyle.lavande:
return 'assets/images/input_field_lavande.png';
case CustomAppTextFieldStyle.jaune:
return 'assets/images/input_field_jaune.png';
case CustomAppTextFieldStyle.beige:
default:
return 'assets/images/input_field_bg.png';
}
}
@override
Widget build(BuildContext context) {
const double fontHeightMultiplier = 1.2;
const double internalVerticalPadding = 16.0;
final double dynamicFieldHeight = widget.fieldHeight;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
widget.labelText,
style: GoogleFonts.merienda(
fontSize: widget.labelFontSize,
color: Colors.black87,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 6),
SizedBox(
width: widget.fieldWidth,
height: dynamicFieldHeight,
child: Stack(
alignment: Alignment.centerLeft,
children: [
Positioned.fill(
child: Image.asset(
getBackgroundImagePath(),
fit: BoxFit.fill,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 8.0),
child: TextFormField(
controller: widget.controller,
obscureText: widget.obscureText,
keyboardType: widget.keyboardType,
enabled: widget.enabled,
readOnly: widget.readOnly,
onTap: widget.onTap,
style: GoogleFonts.merienda(
fontSize: widget.inputFontSize,
color: widget.enabled ? Colors.black87 : Colors.grey
),
validator: widget.validator ??
(value) {
if (!widget.enabled || widget.readOnly) return null;
if (widget.isRequired && (value == null || value.isEmpty)) {
return 'Ce champ est obligatoire';
}
return null;
},
decoration: InputDecoration(
hintText: widget.hintText,
hintStyle: GoogleFonts.merienda(fontSize: widget.inputFontSize, color: Colors.black54.withOpacity(0.7)),
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
suffixIcon: widget.suffixIcon != null
? Padding(
padding: const EdgeInsets.only(right: 0.0),
child: Icon(widget.suffixIcon, color: Colors.black54, size: widget.inputFontSize * 1.1),
)
: null,
isDense: true,
),
textAlignVertical: TextAlignVertical.center,
),
),
],
),
),
],
);
}
}
@@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class CustomDecoratedTextField extends StatelessWidget {
final TextEditingController controller;
final String hintText;
final int maxLines;
final double? fieldHeight; // Hauteur optionnelle pour le champ
final bool expandDynamically; // Nouvelle propriété
final bool readOnly;
final double fontSize;
const CustomDecoratedTextField({
super.key,
required this.controller,
this.hintText = 'Écrire votre texte ici...',
this.maxLines = 10, // Un nombre raisonnable de lignes par défaut si non dynamique
this.fieldHeight, // Si non fourni, la hauteur sera intrinsèque ou définie par l'image
this.expandDynamically = false, // Par défaut, non dynamique
this.readOnly = false,
this.fontSize = 15.0,
});
@override
Widget build(BuildContext context) {
return SizedBox(
height: fieldHeight, // Permet de forcer une hauteur si besoin
child: Stack(
alignment: Alignment.topLeft,
children: [
Image.asset(
'assets/images/square.png', // L'image de fond
fit: BoxFit.fill, // Pour remplir l'espace du Stack/SizedBox
width: double.infinity, // S'assurer qu'elle prend toute la largeur disponible
height: fieldHeight != null ? double.infinity : null, // Et toute la hauteur si fieldHeight est spécifié
),
Padding(
// Ajouter un padding interne pour que le texte ne colle pas aux bords de l'image
padding: const EdgeInsets.only(top: 25.0, bottom: 15.0, left: 20.0, right: 20.0), // Augmentation de la marge supérieure
child: TextFormField(
controller: controller,
keyboardType: TextInputType.multiline,
maxLines: expandDynamically ? null : maxLines, // S'étend dynamiquement si expandDynamically est true
style: GoogleFonts.merienda(fontSize: fontSize, color: Colors.black87),
textAlignVertical: TextAlignVertical.top,
readOnly: readOnly,
decoration: InputDecoration(
hintText: hintText,
hintStyle: GoogleFonts.merienda(fontSize: fontSize, color: Colors.black54.withOpacity(0.7)),
border: InputBorder.none, // Pas de bordure pour le TextFormField lui-même
contentPadding: EdgeInsets.zero, // Le padding est géré par le widget Padding externe
// Pour aligner le hintText en haut à gauche
alignLabelWithHint: true,
),
),
),
],
),
);
}
}
@@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
class HoverReliefWidget extends StatefulWidget {
final Widget child;
final VoidCallback? onPressed;
final BorderRadius borderRadius;
final double initialElevation;
final double hoverElevation;
final double scaleFactor;
final bool enableHoverEffect; // Pour activer/désactiver l'effet de survol
final Color initialShadowColor; // Nouveau paramètre
final Color hoverShadowColor; // Nouveau paramètre
const HoverReliefWidget({
required this.child,
this.onPressed,
this.borderRadius = const BorderRadius.all(Radius.circular(15.0)),
this.initialElevation = 4.0,
this.hoverElevation = 8.0,
this.scaleFactor = 1.03, // Légèrement réduit par rapport à l'exemple précédent
this.enableHoverEffect = true, // Par défaut, l'effet est activé
this.initialShadowColor = const Color(0x26000000), // Default: Colors.black.withOpacity(0.15)
this.hoverShadowColor = const Color(0x4D000000), // Default: Colors.black.withOpacity(0.3)
super.key,
});
@override
State<HoverReliefWidget> createState() => _HoverReliefWidgetState();
}
class _HoverReliefWidgetState extends State<HoverReliefWidget> {
bool _isHovering = false;
@override
Widget build(BuildContext context) {
final bool canHover = widget.enableHoverEffect && widget.onPressed != null;
final hoverTransform = Matrix4.identity()..scale(widget.scaleFactor);
final transform = _isHovering && canHover ? hoverTransform : Matrix4.identity();
final shadowColor = _isHovering && canHover ? widget.hoverShadowColor : widget.initialShadowColor;
final elevation = _isHovering && canHover ? widget.hoverElevation : widget.initialElevation;
Widget content = AnimatedContainer(
duration: const Duration(milliseconds: 200),
transform: transform,
transformAlignment: Alignment.center,
child: Material(
color: Colors.transparent,
elevation: elevation,
shadowColor: shadowColor,
borderRadius: widget.borderRadius,
clipBehavior: Clip.antiAlias,
child: widget.child,
),
);
if (widget.onPressed == null) {
// Si non cliquable, on retourne juste le contenu avec l'élévation initiale (pas de survol)
// Ajustement: pour toujours avoir un Material de base même si non cliquable et sans hover.
return Material(
color: Colors.transparent,
elevation: widget.initialElevation, // Utilise l'élévation initiale
shadowColor: widget.initialShadowColor, // Appliqué ici pour l'état non cliquable
borderRadius: widget.borderRadius,
clipBehavior: Clip.antiAlias,
child: widget.child,
);
}
return MouseRegion(
onEnter: (_) {
if (widget.enableHoverEffect) setState(() => _isHovering = true);
},
onExit: (_) {
if (widget.enableHoverEffect) setState(() => _isHovering = false);
},
cursor: SystemMouseCursors.click,
child: InkWell(
onTap: widget.onPressed,
borderRadius: widget.borderRadius,
hoverColor: Colors.transparent,
splashColor: Colors.grey.withOpacity(0.2),
highlightColor: Colors.grey.withOpacity(0.1),
child: content,
),
);
}
}
+53
View File
@@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class ImageButton extends StatelessWidget {
final String bg;
final double width;
final double height;
final String text;
final Color textColor;
final VoidCallback onPressed;
final double fontSize; // Ajout pour la flexibilité
const ImageButton({
super.key,
required this.bg,
required this.width,
required this.height,
required this.text,
required this.textColor,
required this.onPressed,
this.fontSize = 16, // Valeur par défaut
});
@override
Widget build(BuildContext context) {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: onPressed,
child: Container(
width: width,
height: height,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(bg),
fit: BoxFit.fill,
),
),
child: Center(
child: Text(
text,
style: GoogleFonts.merienda(
color: textColor,
fontSize: fontSize, // Utilisation du paramètre
fontWeight: FontWeight.bold,
),
),
),
),
),
);
}
}
+40
View File
@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>P'titsPas - Connexion</title>
<link rel="stylesheet" href="styles.css">
<link href="https://fonts.googleapis.com/css2?family=Merienda:wght@400;600&display=swap" rel="stylesheet">
</head>
<body>
<img src="assets/river.png" alt="" class="river" aria-hidden="true">
<main class="login-container">
<img src="assets/logo.png" alt="P'titsPas" class="logo">
<h1 class="slogan">Grandir pas à pas, sereinement</h1>
<form class="login-form">
<div class="form-group">
<label for="email">Adresse e-mail</label>
<input type="email" id="email" name="email"
placeholder="Votre adresse e-mail"
aria-label="Saisissez votre adresse e-mail"
required>
</div>
<div class="form-group">
<label for="password">Mot de passe</label>
<input type="password" id="password" name="password"
placeholder="Votre mot de passe"
aria-label="Saisissez votre mot de passe"
required>
</div>
<button type="submit" class="login-button">
Se connecter
</button>
</form>
</main>
</body>
</html>
+133
View File
@@ -0,0 +1,133 @@
/* Reset et styles de base */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Merienda', cursive;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background-color: #ffffff;
}
/* Décor de fond */
.river {
position: fixed;
left: 0;
top: 0;
width: 60vw;
opacity: 0.15;
pointer-events: none;
z-index: -1;
}
/* Container principal */
.login-container {
width: 100%;
max-width: 480px;
padding: 2rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 2rem;
}
/* Logo */
.logo {
max-width: 220px;
height: auto;
}
/* Slogan */
.slogan {
font-family: 'Merienda', cursive;
text-align: center;
color: #3a3a3a;
font-size: 1.5rem;
margin-bottom: 2rem;
}
/* Formulaire */
.login-form {
width: 100%;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
/* Labels */
label {
color: #3a3a3a;
font-weight: 600;
font-size: 1rem;
}
/* Champs de saisie */
input {
height: 80px;
border: none;
border-radius: 30px;
padding: 0 1.2rem;
font-size: 1rem;
font-family: inherit;
background-size: cover;
}
input[type="email"] {
background-image: url('assets/field_email.png');
color: #fff;
}
input[type="password"] {
background-image: url('assets/field_password.png');
color: #000;
}
/* Bouton de connexion */
.login-button {
height: 80px;
background-image: url('assets/btn_green.png');
background-size: cover;
border: none;
border-radius: 40px;
color: #ffffff;
font-family: "Merienda", cursive;
font-size: 1.1rem;
cursor: pointer;
transition: filter 0.2s ease;
}
.login-button:hover {
filter: brightness(1.08);
}
/* Media queries pour mobile */
@media (max-width: 480px) {
.river {
width: 40vw;
opacity: 0.10;
clip-path: inset(0 0 20% 0);
}
.logo {
max-width: 120px;
}
.login-container {
padding: 1rem;
}
.slogan {
font-size: 1.2rem;
}
}
+639
View File
@@ -0,0 +1,639 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
url: "https://pub.dev"
source: hosted
version: "2.12.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
source: hosted
version: "1.4.0"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
cross_file:
dependency: transitive
description:
name: cross_file
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
url: "https://pub.dev"
source: hosted
version: "0.3.4+2"
crypto:
dependency: transitive
description:
name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "https://pub.dev"
source: hosted
version: "3.0.6"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
url: "https://pub.dev"
source: hosted
version: "1.3.2"
ffi:
dependency: transitive
description:
name: ffi
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
file_selector_linux:
dependency: transitive
description:
name: file_selector_linux
sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33"
url: "https://pub.dev"
source: hosted
version: "0.9.3+2"
file_selector_macos:
dependency: transitive
description:
name: file_selector_macos
sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc"
url: "https://pub.dev"
source: hosted
version: "0.9.4+2"
file_selector_platform_interface:
dependency: transitive
description:
name: file_selector_platform_interface
sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b
url: "https://pub.dev"
source: hosted
version: "2.6.2"
file_selector_windows:
dependency: transitive
description:
name: file_selector_windows
sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b"
url: "https://pub.dev"
source: hosted
version: "0.9.3+4"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
url: "https://pub.dev"
source: hosted
version: "2.0.3"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e
url: "https://pub.dev"
source: hosted
version: "2.0.28"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
go_router:
dependency: "direct main"
description:
name: go_router
sha256: b465e99ce64ba75e61c8c0ce3d87b66d8ac07f0b35d0a7e0263fcfc10f99e836
url: "https://pub.dev"
source: hosted
version: "13.2.5"
google_fonts:
dependency: "direct main"
description:
name: google_fonts
sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82
url: "https://pub.dev"
source: hosted
version: "6.2.1"
http:
dependency: "direct main"
description:
name: http
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
url: "https://pub.dev"
source: hosted
version: "1.3.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
image_picker:
dependency: "direct main"
description:
name: image_picker
sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a"
url: "https://pub.dev"
source: hosted
version: "1.1.2"
image_picker_android:
dependency: transitive
description:
name: image_picker_android
sha256: "317a5d961cec5b34e777b9252393f2afbd23084aa6e60fcf601dcf6341b9ebeb"
url: "https://pub.dev"
source: hosted
version: "0.8.12+23"
image_picker_for_web:
dependency: transitive
description:
name: image_picker_for_web
sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83"
url: "https://pub.dev"
source: hosted
version: "3.0.6"
image_picker_ios:
dependency: transitive
description:
name: image_picker_ios
sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100"
url: "https://pub.dev"
source: hosted
version: "0.8.12+2"
image_picker_linux:
dependency: transitive
description:
name: image_picker_linux
sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9"
url: "https://pub.dev"
source: hosted
version: "0.2.1+2"
image_picker_macos:
dependency: transitive
description:
name: image_picker_macos
sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1"
url: "https://pub.dev"
source: hosted
version: "0.2.1+2"
image_picker_platform_interface:
dependency: transitive
description:
name: image_picker_platform_interface
sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0"
url: "https://pub.dev"
source: hosted
version: "2.10.1"
image_picker_windows:
dependency: transitive
description:
name: image_picker_windows
sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb"
url: "https://pub.dev"
source: hosted
version: "0.2.1+1"
intl:
dependency: transitive
description:
name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.dev"
source: hosted
version: "0.19.0"
js:
dependency: "direct main"
description:
name: js
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev"
source: hosted
version: "0.6.7"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
url: "https://pub.dev"
source: hosted
version: "10.0.8"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
url: "https://pub.dev"
source: hosted
version: "3.0.9"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
lints:
dependency: transitive
description:
name: lints
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
version: "0.12.17"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
version: "0.11.1"
meta:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev"
source: hosted
version: "1.16.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider:
dependency: transitive
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9
url: "https://pub.dev"
source: hosted
version: "2.2.17"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
provider:
dependency: "direct main"
description:
name: provider
sha256: "489024f942069c2920c844ee18bb3d467c69e48955a4f32d1677f71be103e310"
url: "https://pub.dev"
source: hosted
version: "6.1.4"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
url: "https://pub.dev"
source: hosted
version: "2.5.3"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac"
url: "https://pub.dev"
source: hosted
version: "2.4.10"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.dev"
source: hosted
version: "2.4.3"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
url: "https://pub.dev"
source: hosted
version: "1.10.1"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
url: "https://pub.dev"
source: hosted
version: "0.7.4"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
url: "https://pub.dev"
source: hosted
version: "6.3.1"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79"
url: "https://pub.dev"
source: hosted
version: "6.3.16"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb"
url: "https://pub.dev"
source: hosted
version: "6.3.3"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
url: "https://pub.dev"
source: hosted
version: "3.2.2"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
url: "https://pub.dev"
source: hosted
version: "3.1.4"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
url: "https://pub.dev"
source: hosted
version: "14.3.1"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
sdks:
dart: ">=3.7.0 <4.0.0"
flutter: ">=3.27.0"
+39
View File
@@ -0,0 +1,39 @@
name: p_tits_pas
description: Application de gestion de la garde d'enfants pour les collectivités locales.
publish_to: 'none'
version: 0.1.0
environment:
sdk: '>=3.2.6 <4.0.0'
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
provider: ^6.1.1
go_router: ^13.2.5
google_fonts: ^6.1.0
shared_preferences: ^2.2.2
image_picker: ^1.0.7
js: ^0.6.7
url_launcher: ^6.2.4
http: ^1.2.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
flutter:
uses-material-design: true
assets:
- assets/images/ # Déclarer le dossier entier
- assets/cards/ # Nouveau dossier de cartes
fonts:
- family: Merienda
fonts:
- asset: assets/fonts/Merienda-VariableFont_wght.ttf
style: normal
+30
View File
@@ -0,0 +1,30 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:petitspas/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

+66
View File
@@ -0,0 +1,66 @@
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="Application de gestion de la garde d'enfants pour les collectivités locales.">
<!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="P'titsPas">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>P'titsPas</title>
<link rel="manifest" href="manifest.json">
<!-- Suppression des dépendances image_cropper web -->
<!--
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.12/cropper.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.12/cropper.min.css">
-->
<script>
// The value below is injected by flutter build, do not touch.
const serviceWorkerVersion = "{{flutter_service_worker_version}}";
</script>
<!-- This script adds the flutter initialization JS code -->
<script src="flutter.js" defer></script>
</head>
<body>
<script>
window.addEventListener('load', function(ev) {
// Download main.dart.js
_flutter.loader.loadEntrypoint({
serviceWorker: {
serviceWorkerVersion: serviceWorkerVersion,
},
onEntrypointLoaded: function(engineInitializer) {
engineInitializer.initializeEngine().then(function(appRunner) {
appRunner.runApp();
});
}
});
});
</script>
</body>
</html>
+35
View File
@@ -0,0 +1,35 @@
{
"name": "P'titsPas",
"short_name": "P'titsPas",
"start_url": ".",
"display": "standalone",
"background_color": "#FFFEF9",
"theme_color": "#8AD0C8",
"description": "P'titsPas - Grandir pas à pas, sereinement",
"orientation": "portrait-primary",
"prefer_related_applications": false,
"icons": [
{
"src": "assets/images/icon.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "assets/images/icon.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "icons/Icon-maskable-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "icons/Icon-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}
+17
View File
@@ -0,0 +1,17 @@
flutter/ephemeral/
# Visual Studio user-specific files.
*.suo
*.user
*.userosscache
*.sln.docstates
# Visual Studio build-related files.
x64/
x86/
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
+108
View File
@@ -0,0 +1,108 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.14)
project(petitspas LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "petitspas")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(VERSION 3.14...3.25)
# Define build configuration option.
get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
if(IS_MULTICONFIG)
set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
CACHE STRING "" FORCE)
else()
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
endif()
# Define settings for the Profile build mode.
set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")
set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}")
set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
# Use Unicode for all projects.
add_definitions(-DUNICODE -D_UNICODE)
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_17)
target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100")
target_compile_options(${TARGET} PRIVATE /EHsc)
target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
target_compile_definitions(${TARGET} PRIVATE "$<$<CONFIG:Debug>:_DEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# Application build; see runner/CMakeLists.txt.
add_subdirectory("runner")
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# Support files are copied into place next to the executable, so that it can
# run in place. This is done instead of making a separate bundle (as on Linux)
# so that building and running from within Visual Studio will work.
set(BUILD_BUNDLE_DIR "$<TARGET_FILE_DIR:${BINARY_NAME}>")
# Make the "install" step default, as it's required to run.
set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
if(PLUGIN_BUNDLED_LIBRARIES)
install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
CONFIGURATIONS Profile;Release
COMPONENT Runtime)
+109
View File
@@ -0,0 +1,109 @@
# This file controls Flutter-level build steps. It should not be edited.
cmake_minimum_required(VERSION 3.14)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
# Set fallback configurations for older versions of the flutter tool.
if (NOT DEFINED FLUTTER_TARGET_PLATFORM)
set(FLUTTER_TARGET_PLATFORM "windows-x64")
endif()
# === Flutter Library ===
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"flutter_export.h"
"flutter_windows.h"
"flutter_messenger.h"
"flutter_plugin_registrar.h"
"flutter_texture_registrar.h"
)
list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib")
add_dependencies(flutter flutter_assemble)
# === Wrapper ===
list(APPEND CPP_WRAPPER_SOURCES_CORE
"core_implementations.cc"
"standard_codec.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
"plugin_registrar.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
list(APPEND CPP_WRAPPER_SOURCES_APP
"flutter_engine.cc"
"flutter_view_controller.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/")
# Wrapper sources needed for a plugin.
add_library(flutter_wrapper_plugin STATIC
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_PLUGIN}
)
apply_standard_settings(flutter_wrapper_plugin)
set_target_properties(flutter_wrapper_plugin PROPERTIES
POSITION_INDEPENDENT_CODE ON)
set_target_properties(flutter_wrapper_plugin PROPERTIES
CXX_VISIBILITY_PRESET hidden)
target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
target_include_directories(flutter_wrapper_plugin PUBLIC
"${WRAPPER_ROOT}/include"
)
add_dependencies(flutter_wrapper_plugin flutter_assemble)
# Wrapper sources needed for the runner.
add_library(flutter_wrapper_app STATIC
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_APP}
)
apply_standard_settings(flutter_wrapper_app)
target_link_libraries(flutter_wrapper_app PUBLIC flutter)
target_include_directories(flutter_wrapper_app PUBLIC
"${WRAPPER_ROOT}/include"
)
add_dependencies(flutter_wrapper_app flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")
set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE)
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
${CPP_WRAPPER_SOURCES_APP}
${PHONY_OUTPUT}
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
${FLUTTER_TARGET_PLATFORM} $<CONFIG>
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_PLUGIN}
${CPP_WRAPPER_SOURCES_APP}
)
@@ -0,0 +1,17 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <file_selector_windows/file_selector_windows.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}
@@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter/plugin_registry.h>
// Registers Flutter plugins.
void RegisterPlugins(flutter::PluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_
@@ -0,0 +1,25 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows
url_launcher_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)
+40
View File
@@ -0,0 +1,40 @@
cmake_minimum_required(VERSION 3.14)
project(runner LANGUAGES CXX)
# Define the application target. To change its name, change BINARY_NAME in the
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
# work.
#
# Any new source files that you add to the application should be added here.
add_executable(${BINARY_NAME} WIN32
"flutter_window.cpp"
"main.cpp"
"utils.cpp"
"win32_window.cpp"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
"Runner.rc"
"runner.exe.manifest"
)
# Apply the standard set of build settings. This can be removed for applications
# that need different build settings.
apply_standard_settings(${BINARY_NAME})
# Add preprocessor definitions for the build version.
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
# Disable Windows macros that collide with C++ standard library functions.
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
# Add dependency libraries and include directories. Add any application-specific
# dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)
+121
View File
@@ -0,0 +1,121 @@
// Microsoft Visual C++ generated resource script.
//
#pragma code_page(65001)
#include "resource.h"
#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "winres.h"
/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
// English (United States) resources
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//
1 TEXTINCLUDE
BEGIN
"resource.h\0"
END
2 TEXTINCLUDE
BEGIN
"#include ""winres.h""\r\n"
"\0"
END
3 TEXTINCLUDE
BEGIN
"\r\n"
"\0"
END
#endif // APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Icon
//
// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
IDI_APP_ICON ICON "resources\\app_icon.ico"
/////////////////////////////////////////////////////////////////////////////
//
// Version
//
#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
#else
#define VERSION_AS_NUMBER 1,0,0,0
#endif
#if defined(FLUTTER_VERSION)
#define VERSION_AS_STRING FLUTTER_VERSION
#else
#define VERSION_AS_STRING "1.0.0"
#endif
VS_VERSION_INFO VERSIONINFO
FILEVERSION VERSION_AS_NUMBER
PRODUCTVERSION VERSION_AS_NUMBER
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
#ifdef _DEBUG
FILEFLAGS VS_FF_DEBUG
#else
FILEFLAGS 0x0L
#endif
FILEOS VOS__WINDOWS32
FILETYPE VFT_APP
FILESUBTYPE 0x0L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904e4"
BEGIN
VALUE "CompanyName", "com.example" "\0"
VALUE "FileDescription", "petitspas" "\0"
VALUE "FileVersion", VERSION_AS_STRING "\0"
VALUE "InternalName", "petitspas" "\0"
VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0"
VALUE "OriginalFilename", "petitspas.exe" "\0"
VALUE "ProductName", "petitspas" "\0"
VALUE "ProductVersion", VERSION_AS_STRING "\0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1252
END
END
#endif // English (United States) resources
/////////////////////////////////////////////////////////////////////////////
#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//
/////////////////////////////////////////////////////////////////////////////
#endif // not APSTUDIO_INVOKED
@@ -0,0 +1,71 @@
#include "flutter_window.h"
#include <optional>
#include "flutter/generated_plugin_registrant.h"
FlutterWindow::FlutterWindow(const flutter::DartProject& project)
: project_(project) {}
FlutterWindow::~FlutterWindow() {}
bool FlutterWindow::OnCreate() {
if (!Win32Window::OnCreate()) {
return false;
}
RECT frame = GetClientArea();
// The size here must match the window dimensions to avoid unnecessary surface
// creation / destruction in the startup path.
flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
frame.right - frame.left, frame.bottom - frame.top, project_);
// Ensure that basic setup of the controller was successful.
if (!flutter_controller_->engine() || !flutter_controller_->view()) {
return false;
}
RegisterPlugins(flutter_controller_->engine());
SetChildContent(flutter_controller_->view()->GetNativeWindow());
flutter_controller_->engine()->SetNextFrameCallback([&]() {
this->Show();
});
// Flutter can complete the first frame before the "show window" callback is
// registered. The following call ensures a frame is pending to ensure the
// window is shown. It is a no-op if the first frame hasn't completed yet.
flutter_controller_->ForceRedraw();
return true;
}
void FlutterWindow::OnDestroy() {
if (flutter_controller_) {
flutter_controller_ = nullptr;
}
Win32Window::OnDestroy();
}
LRESULT
FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept {
// Give Flutter, including plugins, an opportunity to handle window messages.
if (flutter_controller_) {
std::optional<LRESULT> result =
flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
lparam);
if (result) {
return *result;
}
}
switch (message) {
case WM_FONTCHANGE:
flutter_controller_->engine()->ReloadSystemFonts();
break;
}
return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
}
+33
View File
@@ -0,0 +1,33 @@
#ifndef RUNNER_FLUTTER_WINDOW_H_
#define RUNNER_FLUTTER_WINDOW_H_
#include <flutter/dart_project.h>
#include <flutter/flutter_view_controller.h>
#include <memory>
#include "win32_window.h"
// A window that does nothing but host a Flutter view.
class FlutterWindow : public Win32Window {
public:
// Creates a new FlutterWindow hosting a Flutter view running |project|.
explicit FlutterWindow(const flutter::DartProject& project);
virtual ~FlutterWindow();
protected:
// Win32Window:
bool OnCreate() override;
void OnDestroy() override;
LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,
LPARAM const lparam) noexcept override;
private:
// The project to run.
flutter::DartProject project_;
// The Flutter instance hosted by this window.
std::unique_ptr<flutter::FlutterViewController> flutter_controller_;
};
#endif // RUNNER_FLUTTER_WINDOW_H_
+43
View File
@@ -0,0 +1,43 @@
#include <flutter/dart_project.h>
#include <flutter/flutter_view_controller.h>
#include <windows.h>
#include "flutter_window.h"
#include "utils.h"
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
_In_ wchar_t *command_line, _In_ int show_command) {
// Attach to console when present (e.g., 'flutter run') or create a
// new console when running with a debugger.
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
CreateAndAttachConsole();
}
// Initialize COM, so that it is available for use in the library and/or
// plugins.
::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
flutter::DartProject project(L"data");
std::vector<std::string> command_line_arguments =
GetCommandLineArguments();
project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
FlutterWindow window(project);
Win32Window::Point origin(10, 10);
Win32Window::Size size(1280, 720);
if (!window.Create(L"petitspas", origin, size)) {
return EXIT_FAILURE;
}
window.SetQuitOnClose(true);
::MSG msg;
while (::GetMessage(&msg, nullptr, 0, 0)) {
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
::CoUninitialize();
return EXIT_SUCCESS;
}
+16
View File
@@ -0,0 +1,16 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by Runner.rc
//
#define IDI_APP_ICON 101
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 102
#define _APS_NEXT_COMMAND_VALUE 40001
#define _APS_NEXT_CONTROL_VALUE 1001
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 and Windows 11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
</assembly>
+65
View File
@@ -0,0 +1,65 @@
#include "utils.h"
#include <flutter_windows.h>
#include <io.h>
#include <stdio.h>
#include <windows.h>
#include <iostream>
void CreateAndAttachConsole() {
if (::AllocConsole()) {
FILE *unused;
if (freopen_s(&unused, "CONOUT$", "w", stdout)) {
_dup2(_fileno(stdout), 1);
}
if (freopen_s(&unused, "CONOUT$", "w", stderr)) {
_dup2(_fileno(stdout), 2);
}
std::ios::sync_with_stdio();
FlutterDesktopResyncOutputStreams();
}
}
std::vector<std::string> GetCommandLineArguments() {
// Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.
int argc;
wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
if (argv == nullptr) {
return std::vector<std::string>();
}
std::vector<std::string> command_line_arguments;
// Skip the first argument as it's the binary name.
for (int i = 1; i < argc; i++) {
command_line_arguments.push_back(Utf8FromUtf16(argv[i]));
}
::LocalFree(argv);
return command_line_arguments;
}
std::string Utf8FromUtf16(const wchar_t* utf16_string) {
if (utf16_string == nullptr) {
return std::string();
}
unsigned int target_length = ::WideCharToMultiByte(
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
-1, nullptr, 0, nullptr, nullptr)
-1; // remove the trailing null character
int input_length = (int)wcslen(utf16_string);
std::string utf8_string;
if (target_length == 0 || target_length > utf8_string.max_size()) {
return utf8_string;
}
utf8_string.resize(target_length);
int converted_length = ::WideCharToMultiByte(
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
input_length, utf8_string.data(), target_length, nullptr, nullptr);
if (converted_length == 0) {
return std::string();
}
return utf8_string;
}
+19
View File
@@ -0,0 +1,19 @@
#ifndef RUNNER_UTILS_H_
#define RUNNER_UTILS_H_
#include <string>
#include <vector>
// Creates a console for the process, and redirects stdout and stderr to
// it for both the runner and the Flutter library.
void CreateAndAttachConsole();
// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string
// encoded in UTF-8. Returns an empty std::string on failure.
std::string Utf8FromUtf16(const wchar_t* utf16_string);
// Gets the command line arguments passed in as a std::vector<std::string>,
// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.
std::vector<std::string> GetCommandLineArguments();
#endif // RUNNER_UTILS_H_
+288
View File
@@ -0,0 +1,288 @@
#include "win32_window.h"
#include <dwmapi.h>
#include <flutter_windows.h>
#include "resource.h"
namespace {
/// Window attribute that enables dark mode window decorations.
///
/// Redefined in case the developer's machine has a Windows SDK older than
/// version 10.0.22000.0.
/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
#endif
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
/// Registry key for app theme preference.
///
/// A value of 0 indicates apps should use dark mode. A non-zero or missing
/// value indicates apps should use light mode.
constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";
// The number of Win32Window objects that currently exist.
static int g_active_window_count = 0;
using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
// Scale helper to convert logical scaler values to physical using passed in
// scale factor
int Scale(int source, double scale_factor) {
return static_cast<int>(source * scale_factor);
}
// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
// This API is only needed for PerMonitor V1 awareness mode.
void EnableFullDpiSupportIfAvailable(HWND hwnd) {
HMODULE user32_module = LoadLibraryA("User32.dll");
if (!user32_module) {
return;
}
auto enable_non_client_dpi_scaling =
reinterpret_cast<EnableNonClientDpiScaling*>(
GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
if (enable_non_client_dpi_scaling != nullptr) {
enable_non_client_dpi_scaling(hwnd);
}
FreeLibrary(user32_module);
}
} // namespace
// Manages the Win32Window's window class registration.
class WindowClassRegistrar {
public:
~WindowClassRegistrar() = default;
// Returns the singleton registrar instance.
static WindowClassRegistrar* GetInstance() {
if (!instance_) {
instance_ = new WindowClassRegistrar();
}
return instance_;
}
// Returns the name of the window class, registering the class if it hasn't
// previously been registered.
const wchar_t* GetWindowClass();
// Unregisters the window class. Should only be called if there are no
// instances of the window.
void UnregisterWindowClass();
private:
WindowClassRegistrar() = default;
static WindowClassRegistrar* instance_;
bool class_registered_ = false;
};
WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
const wchar_t* WindowClassRegistrar::GetWindowClass() {
if (!class_registered_) {
WNDCLASS window_class{};
window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
window_class.lpszClassName = kWindowClassName;
window_class.style = CS_HREDRAW | CS_VREDRAW;
window_class.cbClsExtra = 0;
window_class.cbWndExtra = 0;
window_class.hInstance = GetModuleHandle(nullptr);
window_class.hIcon =
LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
window_class.hbrBackground = 0;
window_class.lpszMenuName = nullptr;
window_class.lpfnWndProc = Win32Window::WndProc;
RegisterClass(&window_class);
class_registered_ = true;
}
return kWindowClassName;
}
void WindowClassRegistrar::UnregisterWindowClass() {
UnregisterClass(kWindowClassName, nullptr);
class_registered_ = false;
}
Win32Window::Win32Window() {
++g_active_window_count;
}
Win32Window::~Win32Window() {
--g_active_window_count;
Destroy();
}
bool Win32Window::Create(const std::wstring& title,
const Point& origin,
const Size& size) {
Destroy();
const wchar_t* window_class =
WindowClassRegistrar::GetInstance()->GetWindowClass();
const POINT target_point = {static_cast<LONG>(origin.x),
static_cast<LONG>(origin.y)};
HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
double scale_factor = dpi / 96.0;
HWND window = CreateWindow(
window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
Scale(size.width, scale_factor), Scale(size.height, scale_factor),
nullptr, nullptr, GetModuleHandle(nullptr), this);
if (!window) {
return false;
}
UpdateTheme(window);
return OnCreate();
}
bool Win32Window::Show() {
return ShowWindow(window_handle_, SW_SHOWNORMAL);
}
// static
LRESULT CALLBACK Win32Window::WndProc(HWND const window,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept {
if (message == WM_NCCREATE) {
auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);
SetWindowLongPtr(window, GWLP_USERDATA,
reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));
auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);
EnableFullDpiSupportIfAvailable(window);
that->window_handle_ = window;
} else if (Win32Window* that = GetThisFromHandle(window)) {
return that->MessageHandler(window, message, wparam, lparam);
}
return DefWindowProc(window, message, wparam, lparam);
}
LRESULT
Win32Window::MessageHandler(HWND hwnd,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept {
switch (message) {
case WM_DESTROY:
window_handle_ = nullptr;
Destroy();
if (quit_on_close_) {
PostQuitMessage(0);
}
return 0;
case WM_DPICHANGED: {
auto newRectSize = reinterpret_cast<RECT*>(lparam);
LONG newWidth = newRectSize->right - newRectSize->left;
LONG newHeight = newRectSize->bottom - newRectSize->top;
SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
return 0;
}
case WM_SIZE: {
RECT rect = GetClientArea();
if (child_content_ != nullptr) {
// Size and position the child window.
MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
rect.bottom - rect.top, TRUE);
}
return 0;
}
case WM_ACTIVATE:
if (child_content_ != nullptr) {
SetFocus(child_content_);
}
return 0;
case WM_DWMCOLORIZATIONCOLORCHANGED:
UpdateTheme(hwnd);
return 0;
}
return DefWindowProc(window_handle_, message, wparam, lparam);
}
void Win32Window::Destroy() {
OnDestroy();
if (window_handle_) {
DestroyWindow(window_handle_);
window_handle_ = nullptr;
}
if (g_active_window_count == 0) {
WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
}
}
Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
return reinterpret_cast<Win32Window*>(
GetWindowLongPtr(window, GWLP_USERDATA));
}
void Win32Window::SetChildContent(HWND content) {
child_content_ = content;
SetParent(content, window_handle_);
RECT frame = GetClientArea();
MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
frame.bottom - frame.top, true);
SetFocus(child_content_);
}
RECT Win32Window::GetClientArea() {
RECT frame;
GetClientRect(window_handle_, &frame);
return frame;
}
HWND Win32Window::GetHandle() {
return window_handle_;
}
void Win32Window::SetQuitOnClose(bool quit_on_close) {
quit_on_close_ = quit_on_close;
}
bool Win32Window::OnCreate() {
// No-op; provided for subclasses.
return true;
}
void Win32Window::OnDestroy() {
// No-op; provided for subclasses.
}
void Win32Window::UpdateTheme(HWND const window) {
DWORD light_mode;
DWORD light_mode_size = sizeof(light_mode);
LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,
kGetPreferredBrightnessRegValue,
RRF_RT_REG_DWORD, nullptr, &light_mode,
&light_mode_size);
if (result == ERROR_SUCCESS) {
BOOL enable_dark_mode = light_mode == 0;
DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,
&enable_dark_mode, sizeof(enable_dark_mode));
}
}
+102
View File
@@ -0,0 +1,102 @@
#ifndef RUNNER_WIN32_WINDOW_H_
#define RUNNER_WIN32_WINDOW_H_
#include <windows.h>
#include <functional>
#include <memory>
#include <string>
// A class abstraction for a high DPI-aware Win32 Window. Intended to be
// inherited from by classes that wish to specialize with custom
// rendering and input handling
class Win32Window {
public:
struct Point {
unsigned int x;
unsigned int y;
Point(unsigned int x, unsigned int y) : x(x), y(y) {}
};
struct Size {
unsigned int width;
unsigned int height;
Size(unsigned int width, unsigned int height)
: width(width), height(height) {}
};
Win32Window();
virtual ~Win32Window();
// Creates a win32 window with |title| that is positioned and sized using
// |origin| and |size|. New windows are created on the default monitor. Window
// sizes are specified to the OS in physical pixels, hence to ensure a
// consistent size this function will scale the inputted width and height as
// as appropriate for the default monitor. The window is invisible until
// |Show| is called. Returns true if the window was created successfully.
bool Create(const std::wstring& title, const Point& origin, const Size& size);
// Show the current window. Returns true if the window was successfully shown.
bool Show();
// Release OS resources associated with window.
void Destroy();
// Inserts |content| into the window tree.
void SetChildContent(HWND content);
// Returns the backing Window handle to enable clients to set icon and other
// window properties. Returns nullptr if the window has been destroyed.
HWND GetHandle();
// If true, closing this window will quit the application.
void SetQuitOnClose(bool quit_on_close);
// Return a RECT representing the bounds of the current client area.
RECT GetClientArea();
protected:
// Processes and route salient window messages for mouse handling,
// size change and DPI. Delegates handling of these to member overloads that
// inheriting classes can handle.
virtual LRESULT MessageHandler(HWND window,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept;
// Called when CreateAndShow is called, allowing subclass window-related
// setup. Subclasses should return false if setup fails.
virtual bool OnCreate();
// Called when Destroy is called.
virtual void OnDestroy();
private:
friend class WindowClassRegistrar;
// OS callback called by message pump. Handles the WM_NCCREATE message which
// is passed when the non-client area is being created and enables automatic
// non-client DPI scaling so that the non-client area automatically
// responds to changes in DPI. All other messages are handled by
// MessageHandler.
static LRESULT CALLBACK WndProc(HWND const window,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept;
// Retrieves a class instance pointer for |window|
static Win32Window* GetThisFromHandle(HWND const window) noexcept;
// Update the window frame's theme to match the system theme.
static void UpdateTheme(HWND const window);
bool quit_on_close_ = false;
// window handle for top level window.
HWND window_handle_ = nullptr;
// window handle for hosted content.
HWND child_content_ = nullptr;
};
#endif // RUNNER_WIN32_WINDOW_H_