Initialise l'app Flutter mobile : BDD Drift, onboarding sans compte et tableau de bord.
Phase 1 du MVP : schéma SQLite porté depuis le prototype web, seed des règles/récompenses, flux d'onboarding en 4 étapes et écran d'accueil minimal. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
a82dffe959
commit
aac3a729a2
45
mobile/.gitignore
vendored
Normal file
45
mobile/.gitignore
vendored
Normal 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-dependencies
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
/coverage/
|
||||
|
||||
# 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
|
||||
30
mobile/.metadata
Normal file
30
mobile/.metadata
Normal file
@ -0,0 +1,30 @@
|
||||
# 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: "c9a6c484230f8b5e408ec57be1ef71dee1e77020"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
|
||||
base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
|
||||
- platform: android
|
||||
create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
|
||||
base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
|
||||
|
||||
# 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'
|
||||
@ -1,7 +1,54 @@
|
||||
# Application mobile Android
|
||||
# Bons Points — Application Android (Flutter)
|
||||
|
||||
Emplacement prévu pour l'app **Flutter** (Bons Points — produit commercial Play Store).
|
||||
App familiale de bons points, **100 % locale** (SQLite, pas de compte cloud).
|
||||
|
||||
Voir le cahier des charges : [../docs/CAHIER_DES_CHARGES_APP_ANDROID.md](../docs/CAHIER_DES_CHARGES_APP_ANDROID.md).
|
||||
## Stack (choix par défaut — voir CDC §14)
|
||||
|
||||
**Statut :** non initialisé — à développer dans un workspace Cursor dédié.
|
||||
| Couche | Choix |
|
||||
|--------|--------|
|
||||
| UI | Flutter + Material 3 |
|
||||
| État | **Riverpod** |
|
||||
| BDD | **Drift** (SQLite) |
|
||||
| PIN | bcrypt |
|
||||
|
||||
## Démarrage
|
||||
|
||||
```bash
|
||||
cd mobile
|
||||
flutter pub get
|
||||
dart run build_runner build # après modification des tables Drift
|
||||
flutter run
|
||||
```
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
lib/
|
||||
├── main.dart / app.dart
|
||||
├── core/theme/ # charte visuelle (prototype web)
|
||||
├── data/
|
||||
│ ├── database/ # schéma SQLite (port de web/src/db.js)
|
||||
│ ├── seed/ # règles & récompenses par défaut
|
||||
│ └── repositories/
|
||||
├── providers/
|
||||
└── features/
|
||||
├── onboarding/ # bienvenue → enfant → PIN → règles
|
||||
└── dashboard/ # tableau de bord MVP
|
||||
```
|
||||
|
||||
## État actuel (phase 1)
|
||||
|
||||
- [x] Projet Flutter (`fr.ptitspas.bonpoint`, Android seul)
|
||||
- [x] Schéma SQLite + seed règles/récompenses
|
||||
- [x] Logique score (plancher 0, delta réel)
|
||||
- [x] Onboarding sans compte (4 étapes)
|
||||
- [x] Tableau de bord minimal
|
||||
- [ ] Espace parent (PIN, grille règles)
|
||||
- [ ] Fiche enfant + boutique
|
||||
- [ ] Export/import, IAP, AdMob
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
flutter test
|
||||
```
|
||||
|
||||
28
mobile/analysis_options.yaml
Normal file
28
mobile/analysis_options.yaml
Normal 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
|
||||
14
mobile/android/.gitignore
vendored
Normal file
14
mobile/android/.gitignore
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
gradle-wrapper.jar
|
||||
/.gradle
|
||||
/captures/
|
||||
/gradlew
|
||||
/gradlew.bat
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
.cxx/
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/to/reference-keystore
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
45
mobile/android/app/build.gradle.kts
Normal file
45
mobile/android/app/build.gradle.kts
Normal file
@ -0,0 +1,45 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "fr.ptitspas.bonpoint.bonpoint"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "fr.ptitspas.bonpoint.bonpoint"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
7
mobile/android/app/src/debug/AndroidManifest.xml
Normal file
7
mobile/android/app/src/debug/AndroidManifest.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
45
mobile/android/app/src/main/AndroidManifest.xml
Normal file
45
mobile/android/app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,45 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application
|
||||
android:label="bonpoint"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
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>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||
|
||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
@ -0,0 +1,5 @@
|
||||
package fr.ptitspas.bonpoint.bonpoint
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
BIN
mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 544 B |
BIN
mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 442 B |
BIN
mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 721 B |
BIN
mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
BIN
mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
18
mobile/android/app/src/main/res/values-night/styles.xml
Normal file
18
mobile/android/app/src/main/res/values-night/styles.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
18
mobile/android/app/src/main/res/values/styles.xml
Normal file
18
mobile/android/app/src/main/res/values/styles.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
7
mobile/android/app/src/profile/AndroidManifest.xml
Normal file
7
mobile/android/app/src/profile/AndroidManifest.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
24
mobile/android/build.gradle.kts
Normal file
24
mobile/android/build.gradle.kts
Normal file
@ -0,0 +1,24 @@
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
val newBuildDir: Directory =
|
||||
rootProject.layout.buildDirectory
|
||||
.dir("../../build")
|
||||
.get()
|
||||
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||
|
||||
subprojects {
|
||||
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(":app")
|
||||
}
|
||||
|
||||
tasks.register<Delete>("clean") {
|
||||
delete(rootProject.layout.buildDirectory)
|
||||
}
|
||||
6
mobile/android/gradle.properties
Normal file
6
mobile/android/gradle.properties
Normal file
@ -0,0 +1,6 @@
|
||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
# This newDsl flag was added by the Flutter template
|
||||
android.newDsl=false
|
||||
# This builtInKotlin flag was added by the Flutter template
|
||||
android.builtInKotlin=false
|
||||
5
mobile/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
mobile/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip
|
||||
26
mobile/android/settings.gradle.kts
Normal file
26
mobile/android/settings.gradle.kts
Normal file
@ -0,0 +1,26 @@
|
||||
pluginManagement {
|
||||
val flutterSdkPath =
|
||||
run {
|
||||
val properties = java.util.Properties()
|
||||
file("local.properties").inputStream().use { properties.load(it) }
|
||||
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||
flutterSdkPath
|
||||
}
|
||||
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "9.0.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.3.20" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
71
mobile/lib/app.dart
Normal file
71
mobile/lib/app.dart
Normal file
@ -0,0 +1,71 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'core/theme/app_theme.dart';
|
||||
import 'features/dashboard/dashboard_screen.dart';
|
||||
import 'features/onboarding/onboarding_flow.dart';
|
||||
import 'providers/database_provider.dart';
|
||||
|
||||
class BonpointApp extends ConsumerStatefulWidget {
|
||||
const BonpointApp({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<BonpointApp> createState() => _BonpointAppState();
|
||||
}
|
||||
|
||||
class _BonpointAppState extends ConsumerState<BonpointApp> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Future.microtask(() => ref.read(databaseProvider).initialize());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final onboardingAsync = ref.watch(onboardingCompleteProvider);
|
||||
|
||||
return MaterialApp(
|
||||
title: 'Bons Points',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: AppTheme.light(),
|
||||
home: onboardingAsync.when(
|
||||
loading: () => const _SplashScreen(),
|
||||
error: (_, _) => const OnboardingFlow(),
|
||||
data: (complete) =>
|
||||
complete ? const DashboardScreen() : const OnboardingFlow(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SplashScreen extends StatelessWidget {
|
||||
const _SplashScreen();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DecoratedBox(
|
||||
decoration: const BoxDecoration(gradient: AppTheme.primaryGradient),
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('⭐', style: TextStyle(fontSize: 64)),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Bons Points',
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
const CircularProgressIndicator(color: Colors.white),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
73
mobile/lib/core/theme/app_theme.dart
Normal file
73
mobile/lib/core/theme/app_theme.dart
Normal file
@ -0,0 +1,73 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Charte inspirée du prototype web (dégradé violet, cartes blanches).
|
||||
abstract final class AppTheme {
|
||||
static const primaryGradient = LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFF667EEA), Color(0xFF764BA2)],
|
||||
);
|
||||
|
||||
static const childColors = [
|
||||
'#6C63FF',
|
||||
'#E91E8C',
|
||||
'#2196F3',
|
||||
'#FF9800',
|
||||
'#4CAF50',
|
||||
'#9C27B0',
|
||||
'#00BCD4',
|
||||
'#F44336',
|
||||
];
|
||||
|
||||
static Color parseColor(String hex) {
|
||||
final value = hex.replaceFirst('#', '');
|
||||
return Color(int.parse('FF$value', radix: 16));
|
||||
}
|
||||
|
||||
static ThemeData light() {
|
||||
final seed = parseColor('#667EEA');
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: seed,
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
scaffoldBackgroundColor: seed,
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(16)),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.3)),
|
||||
),
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
color: Colors.white,
|
||||
elevation: 4,
|
||||
shadowColor: Colors.black26,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
),
|
||||
filledButtonTheme: FilledButtonThemeData(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: seed,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
textStyle: const TextStyle(fontWeight: FontWeight.w700, fontSize: 16),
|
||||
),
|
||||
),
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.white),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
178
mobile/lib/data/database/app_database.dart
Normal file
178
mobile/lib/data/database/app_database.dart
Normal file
@ -0,0 +1,178 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../seed/default_rewards.dart';
|
||||
import '../seed/default_rules.dart';
|
||||
import 'tables.dart';
|
||||
|
||||
part 'app_database.g.dart';
|
||||
|
||||
@DriftDatabase(tables: [Enfants, Regles, Mouvements, Recompenses, Config])
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
AppDatabase() : super(_openConnection());
|
||||
|
||||
AppDatabase.forTesting(super.executor);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
onCreate: (m) async {
|
||||
await m.createAll();
|
||||
},
|
||||
onUpgrade: (m, from, to) async {},
|
||||
);
|
||||
|
||||
/// Met à jour les packs par défaut si l'utilisateur les a déjà importés.
|
||||
Future<void> initialize() async {
|
||||
if (await getConfig('regles_version') != null) {
|
||||
await _syncDefaultRules();
|
||||
}
|
||||
if (await getConfig('recompenses_version') != null) {
|
||||
await _syncDefaultRewards();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> importDefaultPack() async {
|
||||
await _syncDefaultRules();
|
||||
await _syncDefaultRewards();
|
||||
}
|
||||
|
||||
Future<void> _syncDefaultRules() async {
|
||||
final version = await getConfig('regles_version');
|
||||
if (version == rulesVersion) return;
|
||||
|
||||
await (update(regles)..where((r) => r.actif.equals(true)))
|
||||
.write(const ReglesCompanion(actif: Value(false)));
|
||||
|
||||
for (final rule in defaultRules) {
|
||||
await into(regles).insert(
|
||||
ReglesCompanion.insert(
|
||||
libelle: rule.libelle,
|
||||
points: rule.points,
|
||||
icone: Value(rule.icone),
|
||||
ordre: Value(rule.ordre),
|
||||
famille: Value(rule.famille),
|
||||
familleOrdre: Value(rule.familleOrdre),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await setConfig('regles_version', rulesVersion);
|
||||
}
|
||||
|
||||
Future<void> _syncDefaultRewards() async {
|
||||
final version = await getConfig('recompenses_version');
|
||||
if (version == rewardsVersion) return;
|
||||
|
||||
await (update(recompenses)..where((r) => r.actif.equals(true)))
|
||||
.write(const RecompensesCompanion(actif: Value(false)));
|
||||
|
||||
for (final reward in defaultRewards) {
|
||||
await into(recompenses).insert(
|
||||
RecompensesCompanion.insert(
|
||||
libelle: reward.libelle,
|
||||
coutPoints: reward.coutPoints,
|
||||
icone: Value(reward.icone),
|
||||
ordre: Value(reward.ordre),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await setConfig('recompenses_version', rewardsVersion);
|
||||
}
|
||||
|
||||
Future<String?> getConfig(String key) async {
|
||||
final row = await (select(config)..where((c) => c.cle.equals(key)))
|
||||
.getSingleOrNull();
|
||||
return row?.valeur;
|
||||
}
|
||||
|
||||
Future<void> setConfig(String key, String value) async {
|
||||
await into(config).insertOnConflictUpdate(
|
||||
ConfigCompanion.insert(cle: key, valeur: value),
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> isOnboardingComplete() async {
|
||||
return await getConfig('onboarding_complete') == 'true';
|
||||
}
|
||||
|
||||
Future<void> completeOnboarding() async {
|
||||
await setConfig('onboarding_complete', 'true');
|
||||
}
|
||||
|
||||
Future<List<Enfant>> listChildren() {
|
||||
return (select(enfants)..orderBy([(e) => OrderingTerm.asc(e.ordre)])).get();
|
||||
}
|
||||
|
||||
Future<int> countChildren() async {
|
||||
final count = await enfants.count().getSingle();
|
||||
return count;
|
||||
}
|
||||
|
||||
Future<int> insertChild({
|
||||
required String prenom,
|
||||
required String couleur,
|
||||
String? dateNaissance,
|
||||
}) {
|
||||
return into(enfants).insert(
|
||||
EnfantsCompanion.insert(
|
||||
prenom: prenom,
|
||||
couleur: Value(couleur),
|
||||
dateNaissance: Value(dateNaissance),
|
||||
ordre: const Value(1),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setPinHash(String hash) async {
|
||||
await setConfig('pin_hash', hash);
|
||||
}
|
||||
|
||||
Future<String?> getPinHash() => getConfig('pin_hash');
|
||||
|
||||
/// Applique une règle : plancher score à 0, delta réel enregistré.
|
||||
Future<Enfant?> applyRule(int childId, int ruleId, {String? note}) async {
|
||||
return transaction(() async {
|
||||
final child = await (select(enfants)..where((e) => e.id.equals(childId)))
|
||||
.getSingleOrNull();
|
||||
final rule = await (select(regles)
|
||||
..where((r) => r.id.equals(ruleId) & r.actif.equals(true)))
|
||||
.getSingleOrNull();
|
||||
|
||||
if (child == null || rule == null) return null;
|
||||
|
||||
final newScore = (child.score + rule.points).clamp(0, 1 << 30);
|
||||
final realDelta = newScore - child.score;
|
||||
|
||||
await into(mouvements).insert(
|
||||
MouvementsCompanion.insert(
|
||||
enfantId: childId,
|
||||
regleId: Value(ruleId),
|
||||
delta: realDelta,
|
||||
note: Value(note),
|
||||
),
|
||||
);
|
||||
|
||||
await (update(enfants)..where((e) => e.id.equals(childId)))
|
||||
.write(EnfantsCompanion(score: Value(newScore)));
|
||||
|
||||
return (select(enfants)..where((e) => e.id.equals(childId)))
|
||||
.getSingleOrNull();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
LazyDatabase _openConnection() {
|
||||
return LazyDatabase(() async {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final file = File(p.join(dir.path, 'bonpoint.db'));
|
||||
return NativeDatabase.createInBackground(file);
|
||||
});
|
||||
}
|
||||
3692
mobile/lib/data/database/app_database.g.dart
Normal file
3692
mobile/lib/data/database/app_database.g.dart
Normal file
File diff suppressed because it is too large
Load Diff
54
mobile/lib/data/database/tables.dart
Normal file
54
mobile/lib/data/database/tables.dart
Normal file
@ -0,0 +1,54 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
class Enfants extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get prenom => text()();
|
||||
TextColumn get dateNaissance => text().nullable()();
|
||||
IntColumn get score => integer().withDefault(const Constant(0))();
|
||||
TextColumn get couleur => text().withDefault(const Constant('#6C63FF'))();
|
||||
IntColumn get ordre => integer().withDefault(const Constant(0))();
|
||||
|
||||
@override
|
||||
List<Set<Column<Object>>>? get uniqueKeys => [
|
||||
{prenom},
|
||||
];
|
||||
}
|
||||
|
||||
class Regles extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get libelle => text()();
|
||||
IntColumn get points => integer()();
|
||||
TextColumn get icone => text().withDefault(const Constant('⭐'))();
|
||||
BoolColumn get actif => boolean().withDefault(const Constant(true))();
|
||||
IntColumn get ordre => integer().withDefault(const Constant(0))();
|
||||
TextColumn get famille => text().withDefault(const Constant('📋 Autre'))();
|
||||
IntColumn get familleOrdre => integer().withDefault(const Constant(99))();
|
||||
}
|
||||
|
||||
class Mouvements extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
IntColumn get enfantId => integer().references(Enfants, #id)();
|
||||
IntColumn get regleId => integer().nullable().references(Regles, #id)();
|
||||
IntColumn get recompenseId => integer().nullable().references(Recompenses, #id)();
|
||||
IntColumn get delta => integer()();
|
||||
TextColumn get note => text().nullable()();
|
||||
DateTimeColumn get creeLe => dateTime().withDefault(currentDateAndTime)();
|
||||
BoolColumn get annule => boolean().withDefault(const Constant(false))();
|
||||
}
|
||||
|
||||
class Recompenses extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get libelle => text()();
|
||||
IntColumn get coutPoints => integer()();
|
||||
TextColumn get icone => text().withDefault(const Constant('🎁'))();
|
||||
BoolColumn get actif => boolean().withDefault(const Constant(true))();
|
||||
IntColumn get ordre => integer().withDefault(const Constant(0))();
|
||||
}
|
||||
|
||||
class Config extends Table {
|
||||
TextColumn get cle => text()();
|
||||
TextColumn get valeur => text()();
|
||||
|
||||
@override
|
||||
Set<Column<Object>>? get primaryKey => {cle};
|
||||
}
|
||||
29
mobile/lib/data/repositories/onboarding_repository.dart
Normal file
29
mobile/lib/data/repositories/onboarding_repository.dart
Normal file
@ -0,0 +1,29 @@
|
||||
import 'package:bcrypt/bcrypt.dart';
|
||||
|
||||
import '../database/app_database.dart';
|
||||
|
||||
class OnboardingRepository {
|
||||
OnboardingRepository(this._db);
|
||||
|
||||
final AppDatabase _db;
|
||||
|
||||
Future<bool> isComplete() => _db.isOnboardingComplete();
|
||||
|
||||
Future<void> finish({
|
||||
required String childName,
|
||||
required String color,
|
||||
required String pin,
|
||||
required bool importDefaultRules,
|
||||
}) async {
|
||||
await _db.transaction(() async {
|
||||
await _db.insertChild(prenom: childName.trim(), couleur: color);
|
||||
await _db.setPinHash(BCrypt.hashpw(pin, BCrypt.gensalt()));
|
||||
|
||||
if (importDefaultRules) {
|
||||
await _db.importDefaultPack();
|
||||
}
|
||||
|
||||
await _db.completeOnboarding();
|
||||
});
|
||||
}
|
||||
}
|
||||
31
mobile/lib/data/seed/default_rewards.dart
Normal file
31
mobile/lib/data/seed/default_rewards.dart
Normal file
@ -0,0 +1,31 @@
|
||||
// Récompenses par défaut — portées depuis web/src/db.js (version famille-v2).
|
||||
|
||||
class DefaultRewardSeed {
|
||||
const DefaultRewardSeed({
|
||||
required this.libelle,
|
||||
required this.coutPoints,
|
||||
required this.icone,
|
||||
required this.ordre,
|
||||
});
|
||||
|
||||
final String libelle;
|
||||
final int coutPoints;
|
||||
final String icone;
|
||||
final int ordre;
|
||||
}
|
||||
|
||||
const rewardsVersion = 'famille-v2';
|
||||
|
||||
const defaultRewards = <DefaultRewardSeed>[
|
||||
DefaultRewardSeed(libelle: 'Un bonbon', coutPoints: 1, icone: '🍬', ordre: 1),
|
||||
DefaultRewardSeed(libelle: '10 minutes de télé', coutPoints: 1, icone: '📺', ordre: 2),
|
||||
DefaultRewardSeed(libelle: 'Une sucette', coutPoints: 2, icone: '🍭', ordre: 3),
|
||||
DefaultRewardSeed(libelle: 'Neige', coutPoints: 3, icone: '🍫', ordre: 4),
|
||||
DefaultRewardSeed(libelle: '10 minutes de jeu vidéo', coutPoints: 3, icone: '🎮', ordre: 5),
|
||||
DefaultRewardSeed(libelle: 'Cahier de dessin neuf', coutPoints: 10, icone: '📓', ordre: 6),
|
||||
DefaultRewardSeed(libelle: 'Jouet pas cher (< 10 €)', coutPoints: 50, icone: '🧸', ordre: 7),
|
||||
DefaultRewardSeed(libelle: 'Un biscuit / cookie', coutPoints: 1, icone: '🍪', ordre: 8),
|
||||
DefaultRewardSeed(libelle: 'Choisir la musique dans la voiture', coutPoints: 1, icone: '🎵', ordre: 9),
|
||||
DefaultRewardSeed(libelle: 'Choisir le menu du repas', coutPoints: 5, icone: '🍕', ordre: 10),
|
||||
DefaultRewardSeed(libelle: 'Une glace', coutPoints: 5, icone: '🍦', ordre: 11),
|
||||
];
|
||||
75
mobile/lib/data/seed/default_rules.dart
Normal file
75
mobile/lib/data/seed/default_rules.dart
Normal file
@ -0,0 +1,75 @@
|
||||
/// Règles par défaut — portées depuis web/src/db.js (version famille-v5),
|
||||
/// généralisées pour l'app publique (sans références famille prototype).
|
||||
class DefaultRuleSeed {
|
||||
const DefaultRuleSeed({
|
||||
required this.libelle,
|
||||
required this.points,
|
||||
required this.icone,
|
||||
required this.ordre,
|
||||
required this.famille,
|
||||
required this.familleOrdre,
|
||||
});
|
||||
|
||||
final String libelle;
|
||||
final int points;
|
||||
final String icone;
|
||||
final int ordre;
|
||||
final String famille;
|
||||
final int familleOrdre;
|
||||
}
|
||||
|
||||
const rulesVersion = 'famille-v5';
|
||||
|
||||
class RuleFamilies {
|
||||
static const maison = '🏠 Maison & rangement';
|
||||
static const routine = '⏰ Matin, soir & école';
|
||||
static const fratrie = '👫 Fratrie & entraide';
|
||||
static const respect = '🙏 Respect & écoute';
|
||||
static const ecrans = '📱 Écrans';
|
||||
static const bonus = '🌟 Bonus';
|
||||
}
|
||||
|
||||
const defaultRules = <DefaultRuleSeed>[
|
||||
// Maison & rangement
|
||||
DefaultRuleSeed(libelle: 'Je mets la table', points: 1, icone: '🍽️', ordre: 1, famille: RuleFamilies.maison, familleOrdre: 1),
|
||||
DefaultRuleSeed(libelle: 'Débarrasser la table', points: 1, icone: '🧹', ordre: 2, famille: RuleFamilies.maison, familleOrdre: 1),
|
||||
DefaultRuleSeed(libelle: 'Ranger le lave-vaisselle', points: 1, icone: '🫧', ordre: 3, famille: RuleFamilies.maison, familleOrdre: 1),
|
||||
DefaultRuleSeed(libelle: 'Nettoyer la table du séjour', points: 1, icone: '✨', ordre: 4, famille: RuleFamilies.maison, familleOrdre: 1),
|
||||
DefaultRuleSeed(libelle: 'Ranger la salle de jeu', points: 2, icone: '🎮', ordre: 5, famille: RuleFamilies.maison, familleOrdre: 1),
|
||||
DefaultRuleSeed(libelle: 'Ranger sa chambre', points: 1, icone: '🛏️', ordre: 6, famille: RuleFamilies.maison, familleOrdre: 1),
|
||||
DefaultRuleSeed(libelle: 'Ranger ses habits propres', points: 1, icone: '👕', ordre: 7, famille: RuleFamilies.maison, familleOrdre: 1),
|
||||
DefaultRuleSeed(libelle: 'Ranger la cabane', points: 1, icone: '🏕️', ordre: 8, famille: RuleFamilies.maison, familleOrdre: 1),
|
||||
DefaultRuleSeed(libelle: 'Ranger les vélos (abri jardin)', points: 1, icone: '🚲', ordre: 9, famille: RuleFamilies.maison, familleOrdre: 1),
|
||||
DefaultRuleSeed(libelle: 'Ranger sur ordre des parents', points: 1, icone: '👨👩👧', ordre: 10, famille: RuleFamilies.maison, familleOrdre: 1),
|
||||
DefaultRuleSeed(libelle: 'Piquer dans les placards', points: -2, icone: '🗄️', ordre: 11, famille: RuleFamilies.maison, familleOrdre: 1),
|
||||
// Matin, soir & école
|
||||
DefaultRuleSeed(libelle: "Habits prêts + s'habiller seul (à l'heure)", points: 1, icone: '⏰', ordre: 1, famille: RuleFamilies.routine, familleOrdre: 2),
|
||||
DefaultRuleSeed(libelle: 'Se préparer pour le lit sans bagarre (< 15 min)', points: 1, icone: '🌙', ordre: 2, famille: RuleFamilies.routine, familleOrdre: 2),
|
||||
DefaultRuleSeed(libelle: 'Devoirs faits sans rappel', points: 2, icone: '📚', ordre: 3, famille: RuleFamilies.routine, familleOrdre: 2),
|
||||
DefaultRuleSeed(libelle: 'Activité calme manuelle 1h', points: 2, icone: '🎨', ordre: 4, famille: RuleFamilies.routine, familleOrdre: 2),
|
||||
DefaultRuleSeed(libelle: 'Retard le matin', points: -1, icone: '⏰', ordre: 5, famille: RuleFamilies.routine, familleOrdre: 2),
|
||||
// Fratrie & entraide
|
||||
DefaultRuleSeed(libelle: "Aider son frère/sa sœur (sans qu'on demande)", points: 2, icone: '🤝', ordre: 1, famille: RuleFamilies.fratrie, familleOrdre: 3),
|
||||
DefaultRuleSeed(libelle: 'Partager un jouet sans bagarre', points: 1, icone: '🎁', ordre: 2, famille: RuleFamilies.fratrie, familleOrdre: 3),
|
||||
DefaultRuleSeed(libelle: 'Dire « pardon » tout seul', points: 1, icone: '🗣️', ordre: 3, famille: RuleFamilies.fratrie, familleOrdre: 3),
|
||||
DefaultRuleSeed(libelle: 'Aider le plus jeune (lire, jouer calmement)', points: 1, icone: '👶', ordre: 4, famille: RuleFamilies.fratrie, familleOrdre: 3),
|
||||
DefaultRuleSeed(libelle: 'Taper, frapper', points: -2, icone: '👊', ordre: 5, famille: RuleFamilies.fratrie, familleOrdre: 3),
|
||||
DefaultRuleSeed(libelle: "Ne pas s'excuser auprès de frère/sœur", points: -2, icone: '😤', ordre: 6, famille: RuleFamilies.fratrie, familleOrdre: 3),
|
||||
DefaultRuleSeed(libelle: 'Provocation volontaire de frère/sœur', points: -2, icone: '😈', ordre: 7, famille: RuleFamilies.fratrie, familleOrdre: 3),
|
||||
DefaultRuleSeed(libelle: "Accuser l'autre à tort", points: -2, icone: '🗣️', ordre: 8, famille: RuleFamilies.fratrie, familleOrdre: 3),
|
||||
DefaultRuleSeed(libelle: 'Casser / abîmer un jouet (volontairement)', points: -3, icone: '💔', ordre: 9, famille: RuleFamilies.fratrie, familleOrdre: 3),
|
||||
// Respect & écoute
|
||||
DefaultRuleSeed(libelle: 'Mentir à ses parents', points: -2, icone: '🤥', ordre: 1, famille: RuleFamilies.respect, familleOrdre: 4),
|
||||
DefaultRuleSeed(libelle: 'Dire des gros mots ou insultes', points: -1, icone: '🤬', ordre: 2, famille: RuleFamilies.respect, familleOrdre: 4),
|
||||
DefaultRuleSeed(libelle: 'Répondre mal / faire la tête', points: -1, icone: '🙄', ordre: 3, famille: RuleFamilies.respect, familleOrdre: 4),
|
||||
DefaultRuleSeed(libelle: 'Crier / hurler', points: -1, icone: '🗯️', ordre: 4, famille: RuleFamilies.respect, familleOrdre: 4),
|
||||
DefaultRuleSeed(libelle: 'Faire claquer une porte', points: -1, icone: '🚪', ordre: 5, famille: RuleFamilies.respect, familleOrdre: 4),
|
||||
DefaultRuleSeed(libelle: 'Tirer la langue / geste irrespectueux', points: -1, icone: '👅', ordre: 6, famille: RuleFamilies.respect, familleOrdre: 4),
|
||||
DefaultRuleSeed(libelle: 'Ne pas écouter une consigne (plusieurs reprises)', points: -2, icone: '👂', ordre: 7, famille: RuleFamilies.respect, familleOrdre: 4),
|
||||
DefaultRuleSeed(libelle: 'Partir sans permission (jardin, rue)', points: -3, icone: '🏃', ordre: 8, famille: RuleFamilies.respect, familleOrdre: 4),
|
||||
// Écrans
|
||||
DefaultRuleSeed(libelle: 'Se lever pour la télé sans autorisation (semaine)', points: -3, icone: '📺', ordre: 1, famille: RuleFamilies.ecrans, familleOrdre: 5),
|
||||
DefaultRuleSeed(libelle: 'Utiliser un téléphone sans autorisation', points: -5, icone: '📱', ordre: 2, famille: RuleFamilies.ecrans, familleOrdre: 5),
|
||||
// Bonus
|
||||
DefaultRuleSeed(libelle: 'Bon point bonus — je suis content !', points: 1, icone: '🌟', ordre: 1, famille: RuleFamilies.bonus, familleOrdre: 6),
|
||||
];
|
||||
125
mobile/lib/features/dashboard/dashboard_screen.dart
Normal file
125
mobile/lib/features/dashboard/dashboard_screen.dart
Normal file
@ -0,0 +1,125 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../providers/database_provider.dart';
|
||||
|
||||
class DashboardScreen extends ConsumerWidget {
|
||||
const DashboardScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final childrenAsync = ref.watch(childrenProvider);
|
||||
|
||||
return DecoratedBox(
|
||||
decoration: const BoxDecoration(gradient: AppTheme.primaryGradient),
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
appBar: AppBar(
|
||||
title: const Text('Bons Points'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.lock_outline),
|
||||
tooltip: 'Espace parent',
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Espace parent — bientôt disponible'),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
child: childrenAsync.when(
|
||||
loading: () => const Center(
|
||||
child: CircularProgressIndicator(color: Colors.white),
|
||||
),
|
||||
error: (e, _) => Center(
|
||||
child: Text(
|
||||
'Erreur de chargement',
|
||||
style: TextStyle(color: Colors.white.withValues(alpha: 0.9)),
|
||||
),
|
||||
),
|
||||
data: (children) {
|
||||
if (children.isEmpty) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
'Aucun enfant',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: children.length,
|
||||
itemBuilder: (context, index) {
|
||||
final child = children[index];
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 12,
|
||||
),
|
||||
leading: CircleAvatar(
|
||||
radius: 28,
|
||||
backgroundColor: AppTheme.parseColor(child.couleur),
|
||||
child: Text(
|
||||
child.prenom[0].toUpperCase(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
child.prenom,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'${child.score}',
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.parseColor(child.couleur),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Text('⭐', style: TextStyle(fontSize: 20)),
|
||||
const SizedBox(width: 8),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Fiche ${child.prenom} — bientôt disponible',
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
129
mobile/lib/features/onboarding/onboarding_flow.dart
Normal file
129
mobile/lib/features/onboarding/onboarding_flow.dart
Normal file
@ -0,0 +1,129 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../providers/database_provider.dart';
|
||||
import '../dashboard/dashboard_screen.dart';
|
||||
import 'onboarding_state.dart';
|
||||
import 'steps/child_step.dart';
|
||||
import 'steps/pin_step.dart';
|
||||
import 'steps/rules_step.dart';
|
||||
import 'steps/welcome_step.dart';
|
||||
|
||||
class OnboardingFlow extends ConsumerStatefulWidget {
|
||||
const OnboardingFlow({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<OnboardingFlow> createState() => _OnboardingFlowState();
|
||||
}
|
||||
|
||||
class _OnboardingFlowState extends ConsumerState<OnboardingFlow> {
|
||||
final _pageController = PageController();
|
||||
bool _isSaving = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pageController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _goTo(int page) {
|
||||
_pageController.animateToPage(
|
||||
page,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _finish() async {
|
||||
final draft = ref.read(onboardingDraftProvider);
|
||||
if (draft.childName.trim().isEmpty) {
|
||||
_showError('Indiquez le prénom de l\'enfant.');
|
||||
_goTo(1);
|
||||
return;
|
||||
}
|
||||
if (draft.pin.length < 4) {
|
||||
_showError('Le PIN doit contenir au moins 4 chiffres.');
|
||||
_goTo(2);
|
||||
return;
|
||||
}
|
||||
if (draft.pin != draft.pinConfirm) {
|
||||
_showError('Les deux PIN ne correspondent pas.');
|
||||
_goTo(2);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _isSaving = true);
|
||||
try {
|
||||
final repo = ref.read(onboardingRepositoryProvider);
|
||||
await repo.finish(
|
||||
childName: draft.childName,
|
||||
color: draft.color,
|
||||
pin: draft.pin,
|
||||
importDefaultRules: draft.importDefaultRules,
|
||||
);
|
||||
ref.invalidate(onboardingCompleteProvider);
|
||||
ref.invalidate(childrenProvider);
|
||||
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute<void>(builder: (_) => const DashboardScreen()),
|
||||
);
|
||||
} catch (e) {
|
||||
_showError('Erreur lors de l\'enregistrement. Réessayez.');
|
||||
} finally {
|
||||
if (mounted) setState(() => _isSaving = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _showError(String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(message), backgroundColor: Colors.red.shade700),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PageView(
|
||||
controller: _pageController,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
onPageChanged: (_) {},
|
||||
children: [
|
||||
WelcomeStep(onNext: () => _goTo(1)),
|
||||
ChildStep(
|
||||
step: 2,
|
||||
onBack: () => _goTo(0),
|
||||
onNext: () {
|
||||
final name = ref.read(onboardingDraftProvider).childName.trim();
|
||||
if (name.isEmpty) {
|
||||
_showError('Indiquez le prénom de l\'enfant.');
|
||||
return;
|
||||
}
|
||||
_goTo(2);
|
||||
},
|
||||
),
|
||||
PinStep(
|
||||
step: 3,
|
||||
onBack: () => _goTo(1),
|
||||
onNext: () {
|
||||
final draft = ref.read(onboardingDraftProvider);
|
||||
if (draft.pin.length < 4) {
|
||||
_showError('Le PIN doit contenir au moins 4 chiffres.');
|
||||
return;
|
||||
}
|
||||
if (draft.pin != draft.pinConfirm) {
|
||||
_showError('Les deux PIN ne correspondent pas.');
|
||||
return;
|
||||
}
|
||||
_goTo(3);
|
||||
},
|
||||
),
|
||||
RulesStep(
|
||||
step: 4,
|
||||
isSaving: _isSaving,
|
||||
onBack: () => _goTo(2),
|
||||
onFinish: _finish,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
49
mobile/lib/features/onboarding/onboarding_state.dart
Normal file
49
mobile/lib/features/onboarding/onboarding_state.dart
Normal file
@ -0,0 +1,49 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class OnboardingDraft {
|
||||
const OnboardingDraft({
|
||||
this.childName = '',
|
||||
this.color = '#6C63FF',
|
||||
this.pin = '',
|
||||
this.pinConfirm = '',
|
||||
this.importDefaultRules = true,
|
||||
});
|
||||
|
||||
final String childName;
|
||||
final String color;
|
||||
final String pin;
|
||||
final String pinConfirm;
|
||||
final bool importDefaultRules;
|
||||
|
||||
OnboardingDraft copyWith({
|
||||
String? childName,
|
||||
String? color,
|
||||
String? pin,
|
||||
String? pinConfirm,
|
||||
bool? importDefaultRules,
|
||||
}) {
|
||||
return OnboardingDraft(
|
||||
childName: childName ?? this.childName,
|
||||
color: color ?? this.color,
|
||||
pin: pin ?? this.pin,
|
||||
pinConfirm: pinConfirm ?? this.pinConfirm,
|
||||
importDefaultRules: importDefaultRules ?? this.importDefaultRules,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class OnboardingDraftNotifier extends StateNotifier<OnboardingDraft> {
|
||||
OnboardingDraftNotifier() : super(const OnboardingDraft());
|
||||
|
||||
void setChildName(String value) => state = state.copyWith(childName: value);
|
||||
void setColor(String value) => state = state.copyWith(color: value);
|
||||
void setPin(String value) => state = state.copyWith(pin: value);
|
||||
void setPinConfirm(String value) => state = state.copyWith(pinConfirm: value);
|
||||
void setImportDefaultRules(bool value) =>
|
||||
state = state.copyWith(importDefaultRules: value);
|
||||
}
|
||||
|
||||
final onboardingDraftProvider =
|
||||
StateNotifierProvider<OnboardingDraftNotifier, OnboardingDraft>(
|
||||
(ref) => OnboardingDraftNotifier(),
|
||||
);
|
||||
137
mobile/lib/features/onboarding/steps/child_step.dart
Normal file
137
mobile/lib/features/onboarding/steps/child_step.dart
Normal file
@ -0,0 +1,137 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../onboarding_state.dart';
|
||||
import '../widgets/onboarding_scaffold.dart';
|
||||
|
||||
class ChildStep extends ConsumerStatefulWidget {
|
||||
const ChildStep({
|
||||
super.key,
|
||||
required this.step,
|
||||
required this.onBack,
|
||||
required this.onNext,
|
||||
});
|
||||
|
||||
final int step;
|
||||
final VoidCallback onBack;
|
||||
final VoidCallback onNext;
|
||||
|
||||
@override
|
||||
ConsumerState<ChildStep> createState() => _ChildStepState();
|
||||
}
|
||||
|
||||
class _ChildStepState extends ConsumerState<ChildStep> {
|
||||
late final TextEditingController _nameController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_nameController = TextEditingController(
|
||||
text: ref.read(onboardingDraftProvider).childName,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final draft = ref.watch(onboardingDraftProvider);
|
||||
final notifier = ref.read(onboardingDraftProvider.notifier);
|
||||
|
||||
return OnboardingScaffold(
|
||||
step: widget.step,
|
||||
title: 'Premier enfant',
|
||||
subtitle: 'Créez le profil de votre enfant',
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
TextField(
|
||||
autofocus: true,
|
||||
controller: _nameController,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Prénom',
|
||||
hintText: 'Ex. Léa',
|
||||
),
|
||||
onChanged: notifier.setChildName,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Couleur',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: AppTheme.childColors.map((hex) {
|
||||
final selected = draft.color == hex;
|
||||
return GestureDetector(
|
||||
onTap: () => notifier.setColor(hex),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.parseColor(hex),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: selected ? Colors.black87 : Colors.transparent,
|
||||
width: 3,
|
||||
),
|
||||
boxShadow: selected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: AppTheme.parseColor(hex)
|
||||
.withValues(alpha: 0.5),
|
||||
blurRadius: 8,
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: selected
|
||||
? const Icon(Icons.check, color: Colors.white)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
const Spacer(),
|
||||
Center(
|
||||
child: CircleAvatar(
|
||||
radius: 40,
|
||||
backgroundColor: AppTheme.parseColor(draft.color),
|
||||
child: Text(
|
||||
draft.childName.isNotEmpty
|
||||
? draft.childName[0].toUpperCase()
|
||||
: '?',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
bottom: Row(
|
||||
children: [
|
||||
TextButton(onPressed: widget.onBack, child: const Text('Retour')),
|
||||
const Spacer(),
|
||||
FilledButton(onPressed: widget.onNext, child: const Text('Suivant')),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
115
mobile/lib/features/onboarding/steps/pin_step.dart
Normal file
115
mobile/lib/features/onboarding/steps/pin_step.dart
Normal file
@ -0,0 +1,115 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../onboarding_state.dart';
|
||||
import '../widgets/onboarding_scaffold.dart';
|
||||
|
||||
class PinStep extends ConsumerStatefulWidget {
|
||||
const PinStep({
|
||||
super.key,
|
||||
required this.step,
|
||||
required this.onBack,
|
||||
required this.onNext,
|
||||
});
|
||||
|
||||
final int step;
|
||||
final VoidCallback onBack;
|
||||
final VoidCallback onNext;
|
||||
|
||||
@override
|
||||
ConsumerState<PinStep> createState() => _PinStepState();
|
||||
}
|
||||
|
||||
class _PinStepState extends ConsumerState<PinStep> {
|
||||
final _pinController = TextEditingController();
|
||||
final _confirmController = TextEditingController();
|
||||
bool _obscurePin = true;
|
||||
bool _obscureConfirm = true;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pinController.dispose();
|
||||
_confirmController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final notifier = ref.read(onboardingDraftProvider.notifier);
|
||||
|
||||
return OnboardingScaffold(
|
||||
step: widget.step,
|
||||
title: 'PIN parent',
|
||||
subtitle: 'Protège l\'espace parent (4 chiffres minimum)',
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
TextField(
|
||||
controller: _pinController,
|
||||
obscureText: _obscurePin,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
LengthLimitingTextInputFormatter(8),
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Choisir un PIN',
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_obscurePin ? Icons.visibility : Icons.visibility_off,
|
||||
),
|
||||
onPressed: () =>
|
||||
setState(() => _obscurePin = !_obscurePin),
|
||||
),
|
||||
),
|
||||
onChanged: notifier.setPin,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: _confirmController,
|
||||
obscureText: _obscureConfirm,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
LengthLimitingTextInputFormatter(8),
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Confirmer le PIN',
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_obscureConfirm
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
),
|
||||
onPressed: () =>
|
||||
setState(() => _obscureConfirm = !_obscureConfirm),
|
||||
),
|
||||
),
|
||||
onChanged: notifier.setPinConfirm,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'Ce PIN sera demandé pour accéder à l\'espace parent '
|
||||
'(appliquer des règles, modifier les réglages).',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
bottom: Row(
|
||||
children: [
|
||||
TextButton(onPressed: widget.onBack, child: const Text('Retour')),
|
||||
const Spacer(),
|
||||
FilledButton(onPressed: widget.onNext, child: const Text('Suivant')),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
117
mobile/lib/features/onboarding/steps/rules_step.dart
Normal file
117
mobile/lib/features/onboarding/steps/rules_step.dart
Normal file
@ -0,0 +1,117 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../data/seed/default_rewards.dart';
|
||||
import '../../../data/seed/default_rules.dart';
|
||||
import '../onboarding_state.dart';
|
||||
import '../widgets/onboarding_scaffold.dart';
|
||||
|
||||
class RulesStep extends ConsumerWidget {
|
||||
const RulesStep({
|
||||
super.key,
|
||||
required this.step,
|
||||
required this.isSaving,
|
||||
required this.onBack,
|
||||
required this.onFinish,
|
||||
});
|
||||
|
||||
final int step;
|
||||
final bool isSaving;
|
||||
final VoidCallback onBack;
|
||||
final VoidCallback onFinish;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final draft = ref.watch(onboardingDraftProvider);
|
||||
final notifier = ref.read(onboardingDraftProvider.notifier);
|
||||
|
||||
return OnboardingScaffold(
|
||||
step: step,
|
||||
title: 'Règles par défaut',
|
||||
subtitle: 'Pack « Maison & école » prêt à l\'emploi',
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('Importer le pack par défaut'),
|
||||
subtitle: Text(
|
||||
'${defaultRules.length} règles et '
|
||||
'${defaultRewards.length} récompenses',
|
||||
),
|
||||
value: draft.importDefaultRules,
|
||||
onChanged: isSaving ? null : notifier.setImportDefaultRules,
|
||||
),
|
||||
const Divider(),
|
||||
Expanded(
|
||||
child: draft.importDefaultRules
|
||||
? ListView(
|
||||
children: [
|
||||
Text(
|
||||
'Familles de règles incluses :',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
..._ruleFamilies.map(
|
||||
(f) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Text('• $f'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Vous pourrez modifier les règles plus tard '
|
||||
'depuis l\'espace parent (Premium pour '
|
||||
'l\'édition complète).',
|
||||
style: Theme.of(context).textTheme.bodySmall
|
||||
?.copyWith(color: Colors.grey.shade600),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Center(
|
||||
child: Text(
|
||||
'Vous partirez sans règles ni récompenses. '
|
||||
'Vous pourrez les ajouter manuellement plus tard.',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
bottom: Row(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: isSaving ? null : onBack,
|
||||
child: const Text('Retour'),
|
||||
),
|
||||
const Spacer(),
|
||||
FilledButton(
|
||||
onPressed: isSaving ? null : onFinish,
|
||||
child: isSaving
|
||||
? const SizedBox(
|
||||
width: 22,
|
||||
height: 22,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('C\'est parti !'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const _ruleFamilies = [
|
||||
RuleFamilies.maison,
|
||||
RuleFamilies.routine,
|
||||
RuleFamilies.fratrie,
|
||||
RuleFamilies.respect,
|
||||
RuleFamilies.ecrans,
|
||||
RuleFamilies.bonus,
|
||||
];
|
||||
76
mobile/lib/features/onboarding/steps/welcome_step.dart
Normal file
76
mobile/lib/features/onboarding/steps/welcome_step.dart
Normal file
@ -0,0 +1,76 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../widgets/onboarding_scaffold.dart';
|
||||
|
||||
class WelcomeStep extends StatelessWidget {
|
||||
const WelcomeStep({super.key, required this.onNext});
|
||||
|
||||
final VoidCallback onNext;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return OnboardingScaffold(
|
||||
step: 1,
|
||||
title: 'Bons Points',
|
||||
subtitle: 'Simple, privé, 100 % sur votre téléphone',
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('⭐', style: TextStyle(fontSize: 56)),
|
||||
const SizedBox(height: 20),
|
||||
_FeatureRow(
|
||||
icon: Icons.phone_android,
|
||||
text: 'Aucun compte, aucun cloud',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_FeatureRow(
|
||||
icon: Icons.wifi_off,
|
||||
text: 'Fonctionne hors ligne',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_FeatureRow(
|
||||
icon: Icons.family_restroom,
|
||||
text: 'Bons points pour toute la famille',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_FeatureRow(
|
||||
icon: Icons.lock_outline,
|
||||
text: 'Espace parent protégé par PIN',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
bottom: FilledButton(
|
||||
onPressed: onNext,
|
||||
child: const Text('Commencer'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FeatureRow extends StatelessWidget {
|
||||
const _FeatureRow({required this.icon, required this.text});
|
||||
|
||||
final IconData icon;
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, color: const Color(0xFF667EEA)),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,88 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
|
||||
class OnboardingScaffold extends StatelessWidget {
|
||||
const OnboardingScaffold({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.child,
|
||||
this.bottom,
|
||||
this.step,
|
||||
this.totalSteps = 4,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final Widget child;
|
||||
final Widget? bottom;
|
||||
final int? step;
|
||||
final int totalSteps;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DecoratedBox(
|
||||
decoration: const BoxDecoration(gradient: AppTheme.primaryGradient),
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (step != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: List.generate(totalSteps, (index) {
|
||||
final active = index < step!;
|
||||
return Expanded(
|
||||
child: Container(
|
||||
height: 4,
|
||||
margin: EdgeInsets.only(
|
||||
right: index < totalSteps - 1 ? 6 : 0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: active
|
||||
? Colors.white
|
||||
: Colors.white.withValues(alpha: 0.35),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
subtitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: Colors.white.withValues(alpha: 0.9),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Expanded(child: child),
|
||||
if (bottom != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
bottom!,
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
13
mobile/lib/main.dart
Normal file
13
mobile/lib/main.dart
Normal file
@ -0,0 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'app.dart';
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
runApp(
|
||||
const ProviderScope(
|
||||
child: BonpointApp(),
|
||||
),
|
||||
);
|
||||
}
|
||||
24
mobile/lib/providers/database_provider.dart
Normal file
24
mobile/lib/providers/database_provider.dart
Normal file
@ -0,0 +1,24 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../data/database/app_database.dart';
|
||||
import '../data/repositories/onboarding_repository.dart';
|
||||
|
||||
final databaseProvider = Provider<AppDatabase>((ref) {
|
||||
final db = AppDatabase();
|
||||
ref.onDispose(db.close);
|
||||
return db;
|
||||
});
|
||||
|
||||
final onboardingRepositoryProvider = Provider<OnboardingRepository>((ref) {
|
||||
return OnboardingRepository(ref.watch(databaseProvider));
|
||||
});
|
||||
|
||||
final onboardingCompleteProvider = FutureProvider<bool>((ref) async {
|
||||
final repo = ref.watch(onboardingRepositoryProvider);
|
||||
return repo.isComplete();
|
||||
});
|
||||
|
||||
final childrenProvider = FutureProvider<List<Enfant>>((ref) async {
|
||||
final db = ref.watch(databaseProvider);
|
||||
return db.listChildren();
|
||||
});
|
||||
741
mobile/pubspec.lock
Normal file
741
mobile/pubspec.lock
Normal file
@ -0,0 +1,741 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
_fe_analyzer_shared:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: a49d6cf99e8d8e7a8e93668d09ced0bbdb954d0b4fccc2f5f9241c6b87fad95c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "99.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "663efa951fb8a45e06f491223a604c93820598f20e6a99c25617a1576065e8b7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "12.1.0"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.1"
|
||||
bcrypt:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: bcrypt
|
||||
sha256: "6073a700cbbc59f1d4ab27cd532755e3de5e676c4941f535f351374df849270b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
build:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build
|
||||
sha256: a156715e7cd728130c592f30552575908aae5b100005fbc1f0fb16b3c03a3d10
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.6"
|
||||
build_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_config
|
||||
sha256: "4070d2a59f8eec34c97c86ceb44403834899075f66e8a9d59706f8e7834f6f71"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
build_daemon:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_daemon
|
||||
sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.1"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: "1523ce62448ebac2c15a8ba5fbad8acac169788658a7dd2a1c2d9c2a9318b9a6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.15.0"
|
||||
built_collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_collection
|
||||
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.1"
|
||||
built_value:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_value
|
||||
sha256: "34e4067d30ce212937df995f03b69992eea683539ceeac7f679a1f1eba055b56"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.12.6"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: charcode
|
||||
sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: checked_yaml
|
||||
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.4"
|
||||
cli_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cli_util
|
||||
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.2"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
code_assets:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_assets
|
||||
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: convert
|
||||
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cupertino_icons
|
||||
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.9"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
sha256: a4c1ccfee44c7e75ed80484071a5c142a385345e658fd8bd7c4b5c97e7198f98
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.8"
|
||||
drift:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: drift
|
||||
sha256: "6cc0b623c0e83f7080524d8396e9301b1d78b9c66a4fdceeb0f798211303254c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.34.0"
|
||||
drift_dev:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: drift_dev
|
||||
sha256: "9cfff1576b49725da0d32c040651a41ae195e8c4af8d8da301593e41d7abc2f7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.34.0"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_riverpod:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_riverpod
|
||||
sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.1"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: graphs
|
||||
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
hooks:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hooks
|
||||
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_multi_server
|
||||
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: io
|
||||
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
jni:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jni
|
||||
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
jni_flutter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jni_flutter
|
||||
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.12.0"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.0.2"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.10"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
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: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.19"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.13.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
native_toolchain_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: native_toolchain_c
|
||||
sha256: f59351d28f49520cd3a74eb1f41c5f19ae15e53c65a3231d14af672e46510a96
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.19.1"
|
||||
objective_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: objective_c
|
||||
sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.4.1"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_config
|
||||
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
path:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_provider:
|
||||
dependency: "direct main"
|
||||
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: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.0"
|
||||
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"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pool
|
||||
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.2"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
pubspec_parse:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pubspec_parse
|
||||
sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
recase:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: recase
|
||||
sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
record_use:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: record_use
|
||||
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.0"
|
||||
riverpod:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: riverpod
|
||||
sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.1"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf
|
||||
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.2"
|
||||
shelf_web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_web_socket
|
||||
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
source_gen:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_gen
|
||||
sha256: ec37cc0e6694374cbef59ed79685572c870a54ede6fa30a3e420feb3adffea02
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.3"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.2"
|
||||
sqlite3:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlite3
|
||||
sha256: "9488c7d2cdb1091c91cacf7e207cff81b28bff8e366f042bad3afe7d34afe189"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.2"
|
||||
sqlite3_flutter_libs:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqlite3_flutter_libs
|
||||
sha256: eeb9e3a45207649076b808f8a5a74d68770d0b7f26ccef6d5f43106eee5375ad
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.42"
|
||||
sqlparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlparser
|
||||
sha256: "40bdddb306a727be9ce510bd2d2b9a6c9db6c586d846ef7b22e3990a2b24f02d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.44.5"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.1"
|
||||
state_notifier:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: state_notifier
|
||||
sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
stream_transform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_transform
|
||||
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
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: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.11"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.2.0"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket
|
||||
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.12.2 <4.0.0"
|
||||
flutter: ">=3.38.4"
|
||||
30
mobile/pubspec.yaml
Normal file
30
mobile/pubspec.yaml
Normal file
@ -0,0 +1,30 @@
|
||||
name: bonpoint
|
||||
description: "Bons Points — système de bons points familial, 100 % local."
|
||||
publish_to: 'none'
|
||||
|
||||
version: 1.0.0+1
|
||||
|
||||
environment:
|
||||
sdk: ^3.12.2
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
cupertino_icons: ^1.0.8
|
||||
flutter_riverpod: ^2.6.1
|
||||
drift: ^2.26.0
|
||||
sqlite3_flutter_libs: ^0.5.31
|
||||
path_provider: ^2.1.5
|
||||
path: ^1.9.1
|
||||
bcrypt: ^1.1.3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^6.0.0
|
||||
drift_dev: ^2.26.0
|
||||
build_runner: ^2.4.15
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
60
mobile/test/score_logic_test.dart
Normal file
60
mobile/test/score_logic_test.dart
Normal file
@ -0,0 +1,60 @@
|
||||
import 'package:bonpoint/data/database/app_database.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
late AppDatabase db;
|
||||
|
||||
setUp(() async {
|
||||
db = AppDatabase.forTesting(NativeDatabase.memory());
|
||||
await db.importDefaultPack();
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
test('appliquerRegle respecte le plancher à 0', () async {
|
||||
final childId = await db.insertChild(
|
||||
prenom: 'Test',
|
||||
couleur: '#6C63FF',
|
||||
);
|
||||
|
||||
final rules = await db.select(db.regles).get();
|
||||
final positiveRule = rules.firstWhere((r) => r.points == 1);
|
||||
final negativeRule = rules.firstWhere((r) => r.points == -2);
|
||||
|
||||
await db.applyRule(childId, positiveRule.id);
|
||||
// Score = 1, puis règle -2 → score 0, delta réel -1
|
||||
await db.applyRule(childId, negativeRule.id);
|
||||
// Score déjà 0, règle -2 → score 0, delta réel 0
|
||||
await db.applyRule(childId, negativeRule.id);
|
||||
|
||||
final child = await (db.select(db.enfants)
|
||||
..where((e) => e.id.equals(childId)))
|
||||
.getSingle();
|
||||
|
||||
expect(child.score, 0);
|
||||
|
||||
final movements = await db.select(db.mouvements).get();
|
||||
expect(movements.length, 3);
|
||||
expect(movements[0].delta, 1);
|
||||
expect(movements[1].delta, -1);
|
||||
expect(movements[2].delta, 0);
|
||||
});
|
||||
|
||||
test('delta enregistré est le delta réel (pas le delta théorique)', () async {
|
||||
final childId = await db.insertChild(
|
||||
prenom: 'Test',
|
||||
couleur: '#6C63FF',
|
||||
);
|
||||
|
||||
final rules = await db.select(db.regles).get();
|
||||
final rule = rules.firstWhere((r) => r.points == -2);
|
||||
|
||||
await db.applyRule(childId, rule.id);
|
||||
|
||||
final movement = await db.select(db.mouvements).getSingle();
|
||||
expect(movement.delta, 0);
|
||||
});
|
||||
}
|
||||
23
mobile/test/widget_test.dart
Normal file
23
mobile/test/widget_test.dart
Normal file
@ -0,0 +1,23 @@
|
||||
import 'package:bonpoint/app.dart';
|
||||
import 'package:bonpoint/providers/database_provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Affiche l\'écran de bienvenue au premier lancement',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
onboardingCompleteProvider.overrideWith((ref) async => false),
|
||||
],
|
||||
child: const BonpointApp(),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
expect(find.text('Bons Points'), findsWidgets);
|
||||
expect(find.text('Commencer'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user