initial commit
31
.github/copilot-instructions.md
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
<!-- Use this file to provide workspace-specific custom instructions to Copilot. For more details, visit https://code.visualstudio.com/docs/copilot/copilot-customization#_use-a-githubcopilotinstructionsmd-file -->
|
||||
|
||||
# RoadTrip Kids App Instructions
|
||||
|
||||
This is a Flutter application for children aged 4-6 years, designed to provide entertainment during car trips. The app is fully speech-controlled and features both offline games and AI-powered premium games.
|
||||
|
||||
## App Structure
|
||||
|
||||
- `/lib/screens`: Contains the main UI screens of the app
|
||||
- `/lib/models`: Data models for games, users, and content
|
||||
- `/lib/services`: Services for speech recognition, text-to-speech, and OpenAI integration
|
||||
- `/lib/widgets`: Reusable UI components like the animated character
|
||||
- `/lib/utils`: Utility classes like the app theme
|
||||
|
||||
## Key Technologies
|
||||
|
||||
- Flutter for cross-platform mobile development
|
||||
- Speech-to-Text for voice recognition
|
||||
- Text-to-Speech for audio output
|
||||
- OpenAI API for dynamic content generation
|
||||
- Firebase for backend services and authentication
|
||||
|
||||
## Design Principles
|
||||
|
||||
- Child-friendly interface with large elements and vibrant colors
|
||||
- Speech-first interaction to minimize need for manual input
|
||||
- Engaging animated character for visual feedback
|
||||
- Simple, intuitive navigation designed for children
|
||||
- Safety features including parental controls
|
||||
|
||||
When making changes to the code, ensure they align with these design principles and maintain the child-friendly nature of the app.
|
||||
162
.gitignore
vendored
Normal file
@ -0,0 +1,162 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.build/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.packages
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
# iOS related
|
||||
**/ios/**/*.mode1v3
|
||||
**/ios/**/*.mode2v3
|
||||
**/ios/**/*.moved-aside
|
||||
**/ios/**/*.pbxuser
|
||||
**/ios/**/*.perspectivev3
|
||||
**/ios/**/*sync/
|
||||
**/ios/**/.sconsign.dblite
|
||||
**/ios/**/.tags*
|
||||
**/ios/**/.vagrant/
|
||||
**/ios/**/DerivedData/
|
||||
**/ios/**/Icon?
|
||||
**/ios/**/Pods/
|
||||
**/ios/**/.symlinks/
|
||||
**/ios/**/profile
|
||||
**/ios/**/xcuserdata
|
||||
**/ios/.generated/
|
||||
**/ios/Flutter/App.framework
|
||||
**/ios/Flutter/Flutter.framework
|
||||
**/ios/Flutter/Flutter.podspec
|
||||
**/ios/Flutter/Generated.xcconfig
|
||||
**/ios/Flutter/ephemeral/
|
||||
**/ios/Flutter/app.flx
|
||||
**/ios/Flutter/app.zip
|
||||
**/ios/Flutter/flutter_assets/
|
||||
**/ios/Flutter/flutter_export_environment.sh
|
||||
**/ios/ServiceDefinitions.json
|
||||
**/ios/Runner/GeneratedPluginRegistrant.*
|
||||
|
||||
# macOS related
|
||||
**/macos/Flutter/GeneratedPluginRegistrant.swift
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Firebase related (if using Firebase)
|
||||
firebase_options.dart
|
||||
google-services.json
|
||||
GoogleService-Info.plist
|
||||
|
||||
# API keys and sensitive configuration
|
||||
lib/config/api_keys.dart
|
||||
lib/config/secrets.dart
|
||||
lib/config/openai_config.dart
|
||||
|
||||
# Build outputs
|
||||
*.apk
|
||||
*.ipa
|
||||
*.aab
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
# Windows specific
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
|
||||
# Generated files
|
||||
*.g.dart
|
||||
*.freezed.dart
|
||||
*.chopper.dart
|
||||
*.gr.dart
|
||||
|
||||
# Asset files that might be generated
|
||||
assets/fonts/generated/
|
||||
assets/images/generated/
|
||||
|
||||
# Test related
|
||||
test/coverage/
|
||||
coverage/
|
||||
lcov.info
|
||||
|
||||
# Documentation generated files
|
||||
doc/api/
|
||||
|
||||
# Platform specific directories
|
||||
linux/flutter/generated_plugin_registrant.cc
|
||||
linux/flutter/generated_plugin_registrant.h
|
||||
linux/flutter/generated_plugins.cmake
|
||||
windows/flutter/generated_plugin_registrant.cc
|
||||
windows/flutter/generated_plugin_registrant.h
|
||||
windows/flutter/generated_plugins.cmake
|
||||
web/flutter_service_worker.js
|
||||
web/manifest.json
|
||||
web/version.json
|
||||
web/assets/AssetManifest.json
|
||||
web/assets/FontManifest.json
|
||||
|
||||
# FVM (Flutter Version Management) files
|
||||
.fvm/flutter_sdk
|
||||
|
||||
# Other
|
||||
.flutter-devtools/
|
||||
.metadata
|
||||
|
||||
# Speech and audio cache files
|
||||
*.wav
|
||||
*.mp3.tmp
|
||||
audio_cache/
|
||||
|
||||
# RoadTrip Kids specific files
|
||||
# Exclude user data and analytics
|
||||
user_data/
|
||||
analytics/
|
||||
crash_reports/
|
||||
|
||||
# Exclude OpenAI API keys in any config files
|
||||
*openai*key*
|
||||
*api*key*
|
||||
33
.metadata
Normal file
@ -0,0 +1,33 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "6fba2447e95c451518584c35e25f5433f14d888c"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 6fba2447e95c451518584c35e25f5433f14d888c
|
||||
base_revision: 6fba2447e95c451518584c35e25f5433f14d888c
|
||||
- platform: android
|
||||
create_revision: 6fba2447e95c451518584c35e25f5433f14d888c
|
||||
base_revision: 6fba2447e95c451518584c35e25f5433f14d888c
|
||||
- platform: ios
|
||||
create_revision: 6fba2447e95c451518584c35e25f5433f14d888c
|
||||
base_revision: 6fba2447e95c451518584c35e25f5433f14d888c
|
||||
|
||||
# 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'
|
||||
60
README.md
Normal file
@ -0,0 +1,60 @@
|
||||
# RoadTrip Kids
|
||||
|
||||
Eine sprachgesteuerte Spiele-App für Kinder (4-6 Jahre) auf Autofahrten, die Unterhaltung mit pädagogischem Wert verbindet.
|
||||
|
||||
## Projektübersicht
|
||||
|
||||
RoadTrip Kids ist eine innovative, vollständig sprachgesteuerte App, die speziell für Kinder im Alter von 4-6 Jahren entwickelt wurde, um Autofahrten unterhaltsamer zu gestalten. Die App bietet eine kostenlose Offline-Basis mit vorbereiteten Spielen und eine Premium-Version mit einer KI-gesteuerten dynamischen Spielerfahrung.
|
||||
|
||||
## Kernfunktionalitäten
|
||||
|
||||
### Vollständige Sprachsteuerung
|
||||
- Präzise Spracherkennung, optimiert für Kinder im Alter von 4-6 Jahren
|
||||
- Natürliche Sprachausgabe mit einer freundlichen, altersgerechten Stimme
|
||||
- Keine Ablenkung durch manuelle Bedienung während der Fahrt
|
||||
|
||||
### Offline-Spiele (Kostenlos)
|
||||
- Geräusche-Detektiv: Erkennen von Tiergeräuschen
|
||||
- Ich sehe was: Klassisches "Ich sehe was, was du nicht siehst"-Spiel
|
||||
- Reime-Reise: Reimspiele zur Sprachentwicklung
|
||||
- Geschichtenzeit: Interaktive Kurzgeschichten
|
||||
|
||||
### Premium KI-Spiele (Abonnement)
|
||||
- Dynamischer KI-Spielleiter mittels OpenAI
|
||||
- Unbegrenzte Vielfalt an Spielinhalten
|
||||
- Adaptive Reaktion auf die Antworten der Kinder
|
||||
- Personalisierte Spielerfahrung
|
||||
|
||||
## Technischer Aufbau
|
||||
|
||||
### Frontend
|
||||
- Entwickelt mit Flutter für iOS und Android
|
||||
- Responsive und kinderfreundliche Benutzeroberfläche
|
||||
- Animierter Spielleiter-Charakter
|
||||
|
||||
### Backend
|
||||
- Firebase für Benutzerverwaltung und Datenbank
|
||||
- Cloud Functions für die Anbindung an die OpenAI API
|
||||
- Sichere Speicherung von Benutzereinstellungen
|
||||
|
||||
### Spracherkennung & Sprachausgabe
|
||||
- Plattform-native Spracherkennung über Flutter-Plugins
|
||||
- Hochwertige Text-to-Speech-Engine für natürliche Sprachausgabe
|
||||
|
||||
## Monetarisierungsmodell
|
||||
|
||||
- **Kostenlos**: Zugriff auf die Basisversion mit festgelegten Offline-Spielen
|
||||
- **Premium (Abonnement)**: Freischaltung der KI-gesteuerten Spielerfahrung mit unbegrenzten dynamischen Inhalten
|
||||
|
||||
## Installation
|
||||
|
||||
Um das Projekt lokal zu entwickeln:
|
||||
|
||||
1. Stellen Sie sicher, dass Flutter SDK installiert ist
|
||||
2. Klonen Sie das Repository
|
||||
3. Führen Sie `flutter pub get` aus, um Abhängigkeiten zu installieren
|
||||
4. Führen Sie `flutter run` aus, um die App zu starten
|
||||
|
||||
## Lizenz
|
||||
|
||||
© 2025 RoadTrip Kids. Alle Rechte vorbehalten.
|
||||
28
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
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
|
||||
44
android/app/build.gradle.kts
Normal file
@ -0,0 +1,44 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.road_trip_kids"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = "27.0.12077973" // Updated to match required NDK version
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "com.example.road_trip_kids"
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
7
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>
|
||||
57
android/app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,57 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <!-- Permissions for speech recognition and audio -->
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<!-- Audio capture permissions for better speech recognition -->
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
|
||||
<!-- Bluetooth permissions - only needed if we explicitly use Bluetooth -->
|
||||
<!-- If we don't need Bluetooth functionality, we can remove these permissions -->
|
||||
<!--
|
||||
<uses-permission android:name="android.permission.BLUETOOTH"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
|
||||
-->
|
||||
|
||||
<application
|
||||
android:label="road_trip_kids"
|
||||
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:enableOnBackInvokedCallback="true"
|
||||
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 com.example.road_trip_kids
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
12
android/app/src/main/res/drawable-v21/launch_background.xml
Normal file
@ -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>
|
||||
12
android/app/src/main/res/drawable/launch_background.xml
Normal file
@ -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
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 544 B |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 442 B |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 721 B |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
18
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
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
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>
|
||||
21
android/build.gradle.kts
Normal file
@ -0,0 +1,21 @@
|
||||
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)
|
||||
}
|
||||
663
android/build/reports/problems/problems-report.html
Normal file
3
android/gradle.properties
Normal file
@ -0,0 +1,3 @@
|
||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
5
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-8.12-all.zip
|
||||
25
android/settings.gradle.kts
Normal file
@ -0,0 +1,25 @@
|
||||
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 "8.7.3" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
34
ios/.gitignore
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
**/dgph
|
||||
*.mode1v3
|
||||
*.mode2v3
|
||||
*.moved-aside
|
||||
*.pbxuser
|
||||
*.perspectivev3
|
||||
**/*sync/
|
||||
.sconsign.dblite
|
||||
.tags*
|
||||
**/.vagrant/
|
||||
**/DerivedData/
|
||||
Icon?
|
||||
**/Pods/
|
||||
**/.symlinks/
|
||||
profile
|
||||
xcuserdata
|
||||
**/.generated/
|
||||
Flutter/App.framework
|
||||
Flutter/Flutter.framework
|
||||
Flutter/Flutter.podspec
|
||||
Flutter/Generated.xcconfig
|
||||
Flutter/ephemeral/
|
||||
Flutter/app.flx
|
||||
Flutter/app.zip
|
||||
Flutter/flutter_assets/
|
||||
Flutter/flutter_export_environment.sh
|
||||
ServiceDefinitions.json
|
||||
Runner/GeneratedPluginRegistrant.*
|
||||
|
||||
# Exceptions to above rules.
|
||||
!default.mode1v3
|
||||
!default.mode2v3
|
||||
!default.pbxuser
|
||||
!default.perspectivev3
|
||||
26
ios/Flutter/AppFrameworkInfo.plist
Normal file
@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>App</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>io.flutter.flutter.app</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>App</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>12.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
1
ios/Flutter/Debug.xcconfig
Normal file
@ -0,0 +1 @@
|
||||
#include "Generated.xcconfig"
|
||||
1
ios/Flutter/Release.xcconfig
Normal file
@ -0,0 +1 @@
|
||||
#include "Generated.xcconfig"
|
||||
616
ios/Runner.xcodeproj/project.pbxproj
Normal file
@ -0,0 +1,616 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
|
||||
remoteInfo = Runner;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */,
|
||||
);
|
||||
path = RunnerTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
||||
);
|
||||
name = Flutter;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146E51CF9000F007C117D = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9740EEB11CF90186004384FC /* Flutter */,
|
||||
97C146F01CF9000F007C117D /* Runner */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146EF1CF9000F007C117D /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146F01CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||
97C147021CF9000F007C117D /* Info.plist */,
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||
);
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
331C8080294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||
buildPhases = (
|
||||
331C807D294A63A400263BE5 /* Sources */,
|
||||
331C807F294A63A400263BE5 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
331C8086294A63A400263BE5 /* PBXTargetDependency */,
|
||||
);
|
||||
name = RunnerTests;
|
||||
productName = RunnerTests;
|
||||
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = Runner;
|
||||
productName = Runner;
|
||||
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
331C8080294A63A400263BE5 = {
|
||||
CreatedOnToolsVersion = 14.0;
|
||||
TestTargetID = 97C146ED1CF9000F007C117D;
|
||||
};
|
||||
97C146ED1CF9000F007C117D = {
|
||||
CreatedOnToolsVersion = 7.3.1;
|
||||
LastSwiftMigration = 1100;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
|
||||
compatibilityVersion = "Xcode 9.3";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 97C146E51CF9000F007C117D;
|
||||
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
97C146ED1CF9000F007C117D /* Runner */,
|
||||
331C8080294A63A400263BE5 /* RunnerTests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
331C807F294A63A400263BE5 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EC1CF9000F007C117D /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||
);
|
||||
name = "Thin Binary";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Run Script";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
331C807D294A63A400263BE5 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EA1CF9000F007C117D /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 97C146ED1CF9000F007C117D /* Runner */;
|
||||
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C146FB1CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C147001CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
249021D4217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.roadTripKids;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
331C8088294A63A400263BE5 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.roadTripKids.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
331C8089294A63A400263BE5 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.roadTripKids.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
331C808A294A63A400263BE5 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.roadTripKids.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
97C147031CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147041CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
97C147061CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.roadTripKids;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147071CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.roadTripKids;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
331C8088294A63A400263BE5 /* Debug */,
|
||||
331C8089294A63A400263BE5 /* Release */,
|
||||
331C808A294A63A400263BE5 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
97C147031CF9000F007C117D /* Debug */,
|
||||
97C147041CF9000F007C117D /* Release */,
|
||||
249021D3217E4FDB00AE95B9 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
97C147061CF9000F007C117D /* Debug */,
|
||||
97C147071CF9000F007C117D /* Release */,
|
||||
249021D4217E4FDB00AE95B9 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||
}
|
||||
7
ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
101
ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
Normal file
@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "331C8080294A63A400263BE5"
|
||||
BuildableName = "RunnerTests.xctest"
|
||||
BlueprintName = "RunnerTests"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
enableGPUValidationMode = "1"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Profile"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
7
ios/Runner.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
13
ios/Runner/AppDelegate.swift
Normal file
@ -0,0 +1,13 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
}
|
||||
122
ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal file
@ -0,0 +1,122 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-83.5x83.5@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "Icon-App-1024x1024@1x.png",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 282 B |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 704 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 586 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 762 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
23
ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
5
ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
# Launch Screen Assets
|
||||
|
||||
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
|
||||
|
||||
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
|
||||
37
ios/Runner/Base.lproj/LaunchScreen.storyboard
Normal file
@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="LaunchImage" width="168" height="185"/>
|
||||
</resources>
|
||||
</document>
|
||||
26
ios/Runner/Base.lproj/Main.storyboard
Normal file
@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Flutter View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
52
ios/Runner/Info.plist
Normal file
@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Road Trip Kids</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>road_trip_kids</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/> <key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>This app uses the microphone for speech recognition to enable voice control</string>
|
||||
<key>NSSpeechRecognitionUsageDescription</key>
|
||||
<string>Speech recognition is used to understand your voice commands</string>
|
||||
</dict>
|
||||
</plist>
|
||||
1
ios/Runner/Runner-Bridging-Header.h
Normal file
@ -0,0 +1 @@
|
||||
#import "GeneratedPluginRegistrant.h"
|
||||
12
ios/RunnerTests/RunnerTests.swift
Normal file
@ -0,0 +1,12 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
import XCTest
|
||||
|
||||
class RunnerTests: XCTestCase {
|
||||
|
||||
func testExample() {
|
||||
// If you add code to the Runner application, consider adding tests here.
|
||||
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
|
||||
}
|
||||
|
||||
}
|
||||
39
lib/main.dart
Normal file
@ -0,0 +1,39 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'screens/splash_screen.dart';
|
||||
import 'screens/home_screen.dart';
|
||||
import 'services/speech_service.dart';
|
||||
import 'services/tts_service.dart';
|
||||
import 'utils/app_theme.dart';
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
// Firebase initialization would go here when Flutter SDK is installed
|
||||
// await Firebase.initializeApp();
|
||||
runApp(const RoadTripKidsApp());
|
||||
}
|
||||
|
||||
class RoadTripKidsApp extends StatelessWidget {
|
||||
const RoadTripKidsApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (_) => SpeechService()),
|
||||
ChangeNotifierProvider(create: (_) => TTSService()),
|
||||
// Add more providers as needed
|
||||
],
|
||||
child: MaterialApp(
|
||||
title: 'RoadTrip Kids',
|
||||
theme: AppTheme.lightTheme,
|
||||
debugShowCheckedModeBanner: false,
|
||||
home: const SplashScreen(),
|
||||
routes: {
|
||||
'/home': (context) => const HomeScreen(),
|
||||
// Add more routes as needed
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
146
lib/models/game.dart
Normal file
@ -0,0 +1,146 @@
|
||||
class Game {
|
||||
final String id;
|
||||
final String name;
|
||||
final String description;
|
||||
final String iconPath;
|
||||
final String type;
|
||||
final bool requiresInternet;
|
||||
final bool isPremium;
|
||||
final List<String> ageGroups;
|
||||
|
||||
Game({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.iconPath,
|
||||
required this.type,
|
||||
required this.requiresInternet,
|
||||
required this.isPremium,
|
||||
required this.ageGroups,
|
||||
});
|
||||
|
||||
// Factory method to create Game from JSON
|
||||
factory Game.fromJson(Map<String, dynamic> json) {
|
||||
return Game(
|
||||
id: json['id'],
|
||||
name: json['name'],
|
||||
description: json['description'],
|
||||
iconPath: json['iconPath'],
|
||||
type: json['type'],
|
||||
requiresInternet: json['requiresInternet'],
|
||||
isPremium: json['isPremium'],
|
||||
ageGroups: List<String>.from(json['ageGroups']),
|
||||
);
|
||||
}
|
||||
|
||||
// Convert Game to JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'description': description,
|
||||
'iconPath': iconPath,
|
||||
'type': type,
|
||||
'requiresInternet': requiresInternet,
|
||||
'isPremium': isPremium,
|
||||
'ageGroups': ageGroups,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Sample game data
|
||||
List<Game> offlineGames = [
|
||||
Game(
|
||||
id: 'animal_sounds',
|
||||
name: 'Geräusche-Detektiv',
|
||||
description: 'Erkenne Tiergeräusche und finde heraus, welches Tier sie macht.',
|
||||
iconPath: 'assets/images/animal_sounds_icon.png',
|
||||
type: 'animal_sounds',
|
||||
requiresInternet: false,
|
||||
isPremium: false,
|
||||
ageGroups: ['4-6'],
|
||||
),
|
||||
Game(
|
||||
id: 'i_spy',
|
||||
name: 'Ich sehe was',
|
||||
description: 'Ich sehe was, was du nicht siehst - finde Gegenstände im Auto und draußen!',
|
||||
iconPath: 'assets/images/i_spy_icon.png',
|
||||
type: 'i_spy',
|
||||
requiresInternet: false,
|
||||
isPremium: false,
|
||||
ageGroups: ['4-6'],
|
||||
),
|
||||
Game(
|
||||
id: 'rhyme_game',
|
||||
name: 'Reime-Reise',
|
||||
description: 'Finde Wörter, die sich reimen und erweitere deinen Wortschatz.',
|
||||
iconPath: 'assets/images/rhyme_icon.png',
|
||||
type: 'rhyme_game',
|
||||
requiresInternet: false,
|
||||
isPremium: false,
|
||||
ageGroups: ['4-6'],
|
||||
),
|
||||
Game(
|
||||
id: 'story_time',
|
||||
name: 'Geschichtenzeit',
|
||||
description: 'Höre spannende Geschichten und entscheide, wie sie weitergehen.',
|
||||
iconPath: 'assets/images/story_icon.png',
|
||||
type: 'story_time',
|
||||
requiresInternet: false,
|
||||
isPremium: false,
|
||||
ageGroups: ['4-6'],
|
||||
),
|
||||
];
|
||||
|
||||
List<Game> onlineGames = [
|
||||
Game(
|
||||
id: 'ai_animal_sounds',
|
||||
name: 'KI Geräusche-Detektiv',
|
||||
description: 'Unser KI-Freund denkt sich immer neue Tiere und ihre Geräusche aus!',
|
||||
iconPath: 'assets/images/ai_animal_sounds_icon.png',
|
||||
type: 'animal_sounds',
|
||||
requiresInternet: true,
|
||||
isPremium: true,
|
||||
ageGroups: ['4-6'],
|
||||
),
|
||||
Game(
|
||||
id: 'ai_i_spy',
|
||||
name: 'KI Ich sehe was',
|
||||
description: 'Unser KI-Freund spielt mit dir "Ich sehe was" mit unendlich vielen Ideen!',
|
||||
iconPath: 'assets/images/ai_i_spy_icon.png',
|
||||
type: 'i_spy',
|
||||
requiresInternet: true,
|
||||
isPremium: true,
|
||||
ageGroups: ['4-6'],
|
||||
),
|
||||
Game(
|
||||
id: 'ai_rhyme_game',
|
||||
name: 'KI Reime-Reise',
|
||||
description: 'Unser KI-Freund kennt tausende Reimwörter und bringt dir viele bei!',
|
||||
iconPath: 'assets/images/ai_rhyme_icon.png',
|
||||
type: 'rhyme_game',
|
||||
requiresInternet: true,
|
||||
isPremium: true,
|
||||
ageGroups: ['4-6'],
|
||||
),
|
||||
Game(
|
||||
id: 'ai_story_time',
|
||||
name: 'KI Geschichtenzeit',
|
||||
description: 'Unser KI-Freund erzählt dir aufregende, neue Geschichten, die du mitgestalten kannst!',
|
||||
iconPath: 'assets/images/ai_story_icon.png',
|
||||
type: 'story_time',
|
||||
requiresInternet: true,
|
||||
isPremium: true,
|
||||
ageGroups: ['4-6'],
|
||||
),
|
||||
Game(
|
||||
id: 'ai_guess_object',
|
||||
name: 'KI Rätselspiel',
|
||||
description: 'Unser KI-Freund gibt dir Hinweise zu einem geheimen Objekt. Kannst du es erraten?',
|
||||
iconPath: 'assets/images/ai_guess_icon.png',
|
||||
type: 'guess_object',
|
||||
requiresInternet: true,
|
||||
isPremium: true,
|
||||
ageGroups: ['4-6'],
|
||||
),
|
||||
];
|
||||
186
lib/models/game_content.dart
Normal file
@ -0,0 +1,186 @@
|
||||
class GameContent {
|
||||
final String id;
|
||||
final String gameType;
|
||||
final String text;
|
||||
final String question;
|
||||
final String answer;
|
||||
final List<String> hints;
|
||||
final List<String> options;
|
||||
final String? audioPath;
|
||||
final String? imagePath;
|
||||
|
||||
GameContent({
|
||||
required this.id,
|
||||
required this.gameType,
|
||||
required this.text,
|
||||
required this.question,
|
||||
required this.answer,
|
||||
required this.hints,
|
||||
required this.options,
|
||||
this.audioPath,
|
||||
this.imagePath,
|
||||
});
|
||||
|
||||
// Factory method to create GameContent from JSON
|
||||
factory GameContent.fromJson(Map<String, dynamic> json) {
|
||||
return GameContent(
|
||||
id: json['id'],
|
||||
gameType: json['gameType'],
|
||||
text: json['text'],
|
||||
question: json['question'],
|
||||
answer: json['answer'],
|
||||
hints: List<String>.from(json['hints']),
|
||||
options: List<String>.from(json['options']),
|
||||
audioPath: json['audioPath'],
|
||||
imagePath: json['imagePath'],
|
||||
);
|
||||
}
|
||||
|
||||
// Convert GameContent to JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'gameType': gameType,
|
||||
'text': text,
|
||||
'question': question,
|
||||
'answer': answer,
|
||||
'hints': hints,
|
||||
'options': options,
|
||||
'audioPath': audioPath,
|
||||
'imagePath': imagePath,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Sample content for offline games
|
||||
List<GameContent> animalSoundsContent = [ GameContent(
|
||||
id: 'as_1',
|
||||
gameType: 'animal_sounds',
|
||||
text: 'Ich denke an ein Tier, das "Miau" macht und gerne schnurrt. Welches Tier ist das?',
|
||||
question: 'Welches Tier macht "Miau"?',
|
||||
answer: 'Katze',
|
||||
hints: ['Es hat einen langen Schwanz', 'Es hat Whisker', 'Es ist ein Haustier'],
|
||||
options: ['Katze', 'Hund', 'Vogel'],
|
||||
audioPath: 'assets/sounds/cat.mp3',
|
||||
imagePath: 'assets/images/cat.png',
|
||||
),
|
||||
GameContent(
|
||||
id: 'as_2',
|
||||
gameType: 'animal_sounds',
|
||||
text: 'Ich denke an ein Tier, das "Wau Wau" macht und gerne mit dem Schwanz wedelt. Welches Tier ist das?',
|
||||
question: 'Welches Tier macht "Wau Wau"?',
|
||||
answer: 'Hund',
|
||||
hints: ['Es ist ein treuer Freund', 'Manche bringen Stöcke zurück', 'Es geht gerne spazieren'],
|
||||
options: ['Katze', 'Hund', 'Pferd'],
|
||||
audioPath: 'assets/sounds/dog.mp3',
|
||||
imagePath: 'assets/images/dog.png',
|
||||
),
|
||||
GameContent(
|
||||
id: 'as_3',
|
||||
gameType: 'animal_sounds',
|
||||
text: 'Ich denke an ein Tier, das "Muh" macht und uns Milch gibt. Welches Tier ist das?',
|
||||
question: 'Welches Tier macht "Muh"?',
|
||||
answer: 'Kuh',
|
||||
hints: ['Es lebt auf dem Bauernhof', 'Es hat schwarze und weiße Flecken', 'Es frisst Gras'],
|
||||
options: ['Schwein', 'Kuh', 'Schaf'],
|
||||
audioPath: 'assets/sounds/cow.mp3',
|
||||
imagePath: 'assets/images/cow.png',
|
||||
),
|
||||
];
|
||||
|
||||
List<GameContent> iSpyContent = [ GameContent(
|
||||
id: 'is_1',
|
||||
gameType: 'i_spy',
|
||||
text: 'Ich sehe was, was du nicht siehst, und das ist blau und sehr groß über uns. Was könnte das sein?',
|
||||
question: 'Was könnte das sein?',
|
||||
answer: 'Himmel',
|
||||
hints: ['Es ist über uns', 'Es ist sehr groß', 'Vögel fliegen dort'],
|
||||
options: ['der Himmel', 'Himmel', 'See', 'Auto'],
|
||||
imagePath: 'assets/images/sky.png',
|
||||
), GameContent(
|
||||
id: 'is_2',
|
||||
gameType: 'i_spy',
|
||||
text: 'Ich sehe was, was du nicht siehst, und das ist rund und hilft uns beim Fahren. Was könnte das sein?',
|
||||
question: 'Was könnte das sein?',
|
||||
answer: 'Rad',
|
||||
hints: ['Es ist am Auto', 'Es dreht sich', 'Ohne es können wir nicht fahren'],
|
||||
options: ['das Rad', 'Rad', 'Lenkrad', 'Ball'],
|
||||
imagePath: 'assets/images/wheel.png',
|
||||
),
|
||||
GameContent(
|
||||
id: 'is_3',
|
||||
gameType: 'i_spy',
|
||||
text: 'Ich sehe was, was du nicht siehst, und das ist grün und wächst draußen. Was könnte das sein?',
|
||||
question: 'Was könnte das sein?',
|
||||
answer: 'Baum',
|
||||
hints: ['Es wächst draußen', 'Es hat Blätter', 'Vögel bauen Nester darin'],
|
||||
options: ['der Baum', 'Baum', 'Gras', 'Busch'],
|
||||
imagePath: 'assets/images/tree.png',
|
||||
),
|
||||
];
|
||||
|
||||
List<GameContent> rhymeGameContent = [
|
||||
GameContent(
|
||||
id: 'rg_1',
|
||||
gameType: 'rhyme_game',
|
||||
text: 'Finde ein Wort, das sich auf "Haus" reimt. Was könnte das sein?',
|
||||
question: 'Was reimt sich auf "Haus"?',
|
||||
answer: 'Maus', // Hauptantwort für Hints
|
||||
hints: ['Es ist ein kleines Tier', 'Es hat einen langen Schwanz', 'Katzen jagen es'],
|
||||
options: ['Maus', 'Klaus', 'raus', 'Applaus'], // Alle möglichen Reimwörter
|
||||
imagePath: 'assets/images/mouse.png',
|
||||
),
|
||||
GameContent(
|
||||
id: 'rg_2',
|
||||
gameType: 'rhyme_game',
|
||||
text: 'Finde ein Wort, das sich auf "Hund" reimt. Was könnte das sein?',
|
||||
question: 'Was reimt sich auf "Hund"?',
|
||||
answer: 'rund', // Hauptantwort für Hints
|
||||
hints: ['Es beschreibt eine Form', 'Wie ein Kreis', 'Wie ein Ball'],
|
||||
options: ['bunt', 'rund', 'Mund', 'gesund'], // Alle möglichen Reimwörter
|
||||
imagePath: 'assets/images/round.png',
|
||||
),
|
||||
GameContent(
|
||||
id: 'rg_3',
|
||||
gameType: 'rhyme_game',
|
||||
text: 'Finde ein Wort, das sich auf "Ball" reimt. Was könnte das sein?',
|
||||
question: 'Was reimt sich auf "Ball"?',
|
||||
answer: 'Knall', // Hauptantwort für Hints
|
||||
hints: ['Es ist ein lautes Geräusch', 'Wie ein Ballon, der platzt', 'Es kann erschrecken'],
|
||||
options: ['Fall', 'Knall', 'Hall', 'all'], // Alle möglichen Reimwörter
|
||||
imagePath: 'assets/images/bang.png',
|
||||
),
|
||||
];
|
||||
|
||||
List<GameContent> storyTimeContent = [
|
||||
GameContent(
|
||||
id: 'st_1',
|
||||
gameType: 'story_time',
|
||||
text: 'Der kleine Bär ging im Wald spazieren. Er sah einen großen, alten Baum mit einem Loch darin. Was denkst du, was in dem Loch war?',
|
||||
question: 'Was war in dem Loch im Baum?',
|
||||
answer: '',
|
||||
hints: ['Vielleicht war es ein Tier?', 'Oder etwas Überraschendes?', 'Was könnte in einem Baumloch leben?'],
|
||||
options: ['Ein Eichhörnchen', 'Ein Schatz', 'Eine Fee'],
|
||||
imagePath: 'assets/images/bear_tree.png',
|
||||
),
|
||||
GameContent(
|
||||
id: 'st_2',
|
||||
gameType: 'story_time',
|
||||
text: 'Die kleine Maus wollte über den Fluss, aber es gab keine Brücke. Sie brauchte Hilfe. Wer könnte ihr helfen?',
|
||||
question: 'Wer könnte der kleinen Maus helfen?',
|
||||
answer: '',
|
||||
hints: ['Vielleicht ein anderes Tier?', 'Jemand, der schwimmen kann?', 'Oder jemand, der etwas bauen kann?'],
|
||||
options: ['Ein Frosch', 'Ein Biber', 'Eine Ente'],
|
||||
imagePath: 'assets/images/mouse_river.png',
|
||||
),
|
||||
GameContent(
|
||||
id: 'st_3',
|
||||
gameType: 'story_time',
|
||||
text: 'Die Sonne ging unter und der kleine Igel suchte einen Platz zum Schlafen. Er war müde von seinem langen Tag. Wo könnte er schlafen?',
|
||||
question: 'Wo könnte der Igel schlafen?',
|
||||
answer: '',
|
||||
hints: ['Ein gemütlicher Ort?', 'Ein geschützter Ort?', 'Irgendwo in der Natur?'],
|
||||
options: ['Unter einem Busch', 'In einem Laubhaufen', 'In einer Höhle'],
|
||||
imagePath: 'assets/images/hedgehog.png',
|
||||
),
|
||||
];
|
||||
76
lib/models/user_profile.dart
Normal file
@ -0,0 +1,76 @@
|
||||
class UserProfile {
|
||||
final String id;
|
||||
final String childName;
|
||||
final String childAge;
|
||||
final bool isPremium;
|
||||
final DateTime? premiumExpiryDate;
|
||||
final Map<String, int> gameStats;
|
||||
|
||||
UserProfile({
|
||||
required this.id,
|
||||
required this.childName,
|
||||
required this.childAge,
|
||||
required this.isPremium,
|
||||
this.premiumExpiryDate,
|
||||
required this.gameStats,
|
||||
});
|
||||
|
||||
// Factory method to create UserProfile from JSON
|
||||
factory UserProfile.fromJson(Map<String, dynamic> json) {
|
||||
return UserProfile(
|
||||
id: json['id'],
|
||||
childName: json['childName'],
|
||||
childAge: json['childAge'],
|
||||
isPremium: json['isPremium'],
|
||||
premiumExpiryDate: json['premiumExpiryDate'] != null
|
||||
? DateTime.parse(json['premiumExpiryDate'])
|
||||
: null,
|
||||
gameStats: Map<String, int>.from(json['gameStats']),
|
||||
);
|
||||
}
|
||||
|
||||
// Convert UserProfile to JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'childName': childName,
|
||||
'childAge': childAge,
|
||||
'isPremium': isPremium,
|
||||
'premiumExpiryDate': premiumExpiryDate?.toIso8601String(),
|
||||
'gameStats': gameStats,
|
||||
};
|
||||
}
|
||||
|
||||
// Create a copy with updated fields
|
||||
UserProfile copyWith({
|
||||
String? id,
|
||||
String? childName,
|
||||
String? childAge,
|
||||
bool? isPremium,
|
||||
DateTime? premiumExpiryDate,
|
||||
Map<String, int>? gameStats,
|
||||
}) {
|
||||
return UserProfile(
|
||||
id: id ?? this.id,
|
||||
childName: childName ?? this.childName,
|
||||
childAge: childAge ?? this.childAge,
|
||||
isPremium: isPremium ?? this.isPremium,
|
||||
premiumExpiryDate: premiumExpiryDate ?? this.premiumExpiryDate,
|
||||
gameStats: gameStats ?? this.gameStats,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Sample user profile for demonstration
|
||||
UserProfile sampleUserProfile = UserProfile(
|
||||
id: 'user123',
|
||||
childName: 'Max',
|
||||
childAge: '5',
|
||||
isPremium: false,
|
||||
gameStats: {
|
||||
'animal_sounds': 10,
|
||||
'i_spy': 5,
|
||||
'rhyme_game': 3,
|
||||
'story_time': 7,
|
||||
},
|
||||
);
|
||||
1075
lib/screens/game_screen.dart
Normal file
498
lib/screens/home_screen.dart
Normal file
@ -0,0 +1,498 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../models/game.dart';
|
||||
import '../services/tts_service.dart';
|
||||
import '../utils/app_theme.dart';
|
||||
import 'game_screen.dart';
|
||||
import 'parent_dashboard.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@override
|
||||
State<HomeScreen> createState() => _HomeScreenState();
|
||||
}
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
bool _isPremium = false; // This would be set from user profile
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 2, vsync: this);
|
||||
|
||||
// Welcome message when app opens
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final ttsService = Provider.of<TTSService>(context, listen: false);
|
||||
ttsService.speak('Willkommen bei RoadTrip Kids! Wähle ein Spiel aus!');
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('RoadTrip Kids'),
|
||||
elevation: 0,
|
||||
actions: [
|
||||
// Parent settings button
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings),
|
||||
onPressed: () {
|
||||
// Show PIN entry dialog before opening parent dashboard
|
||||
_showPinEntryDialog();
|
||||
},
|
||||
),
|
||||
],
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
indicatorColor: Colors.white,
|
||||
labelStyle: const TextStyle(
|
||||
fontFamily: 'Bubblegum',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
tabs: const [
|
||||
Tab(text: 'Offline Spiele'),
|
||||
Tab(text: 'KI Spiele'),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
AppTheme.primaryColor.withOpacity(0.7),
|
||||
AppTheme.backgroundColor,
|
||||
],
|
||||
),
|
||||
),
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
// Offline Games Tab
|
||||
_buildOfflineGamesGrid(),
|
||||
|
||||
// Online (Premium) Games Tab
|
||||
_buildOnlineGamesGrid(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOfflineGamesGrid() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: GridView.builder(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
childAspectRatio: 0.8,
|
||||
),
|
||||
itemCount: offlineGames.length,
|
||||
itemBuilder: (context, index) {
|
||||
final game = offlineGames[index];
|
||||
return _buildGameCard(game);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOnlineGamesGrid() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
// Premium banner if not subscribed
|
||||
if (!_isPremium)
|
||||
Card(
|
||||
color: AppTheme.secondaryColor,
|
||||
elevation: 4,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text(
|
||||
'Premium Spiele',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Bubblegum',
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Erwecke unseren KI-Spielleiter zum Leben und erlebe unendlich viele neue Spiele!',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Bubblegum',
|
||||
fontSize: 14,
|
||||
color: Colors.white,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: AppTheme.secondaryColor,
|
||||
),
|
||||
child: const Text('Premium freischalten'),
|
||||
onPressed: () {
|
||||
// Show subscription dialog
|
||||
_showSubscriptionDialog();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Online games grid
|
||||
Expanded(
|
||||
child: GridView.builder(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
childAspectRatio: 0.8,
|
||||
),
|
||||
itemCount: onlineGames.length,
|
||||
itemBuilder: (context, index) {
|
||||
final game = onlineGames[index];
|
||||
return _buildGameCard(game);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGameCard(Game game) {
|
||||
final bool isLocked = game.isPremium && !_isPremium;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () { if (isLocked) {
|
||||
// Show subscription dialog if locked
|
||||
_showSubscriptionDialog();
|
||||
} else {
|
||||
// Don't speak the game name here anymore - let the GameScreen handle it
|
||||
// Navigate to game screen immediately
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => GameScreen(game: game),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
elevation: 5,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// Game content
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Game image - placeholder for now
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.gameColors[offlineGames.indexOf(game) % AppTheme.gameColors.length],
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
_getIconForGame(game.type),
|
||||
size: 50,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Game info
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
game.name,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Bubblegum',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
game.description,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.secondaryTextColor,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Lock overlay for premium games
|
||||
if (isLocked)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Icons.lock,
|
||||
color: Colors.white,
|
||||
size: 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
IconData _getIconForGame(String gameType) {
|
||||
switch (gameType) {
|
||||
case 'animal_sounds':
|
||||
return Icons.pets;
|
||||
case 'i_spy':
|
||||
return Icons.visibility;
|
||||
case 'rhyme_game':
|
||||
return Icons.format_quote;
|
||||
case 'story_time':
|
||||
return Icons.book;
|
||||
case 'guess_object':
|
||||
return Icons.help_outline;
|
||||
default:
|
||||
return Icons.games;
|
||||
}
|
||||
}
|
||||
|
||||
// Show PIN entry dialog for parent area
|
||||
void _showPinEntryDialog() {
|
||||
final TextEditingController pinController = TextEditingController();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text(
|
||||
'Elternbereich',
|
||||
style: TextStyle(fontFamily: 'Bubblegum'),
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Bitte gib den PIN ein:',
|
||||
style: TextStyle(fontFamily: 'Bubblegum'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: pinController,
|
||||
keyboardType: TextInputType.number,
|
||||
maxLength: 4,
|
||||
obscureText: true,
|
||||
textAlign: TextAlign.center,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
counterText: '',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text('Abbrechen'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: const Text('Bestätigen'),
|
||||
onPressed: () {
|
||||
// Default PIN is 1234 for demo
|
||||
if (pinController.text == '1234') {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const ParentDashboard(),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Falscher PIN'),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Show subscription dialog
|
||||
void _showSubscriptionDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
title: const Text(
|
||||
'Premium freischalten',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Bubblegum',
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Premium icon
|
||||
const Icon(
|
||||
Icons.stars,
|
||||
size: 60,
|
||||
color: AppTheme.secondaryColor,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Premium description
|
||||
const Text(
|
||||
'Mit Premium erhältst du:',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Bubblegum',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Premium features
|
||||
_buildPremiumFeature(
|
||||
'Unendlich viele neue Spiele durch unsere KI',
|
||||
Icons.computer,
|
||||
),
|
||||
_buildPremiumFeature(
|
||||
'Dynamisch reagierender Spielleiter',
|
||||
Icons.chat,
|
||||
),
|
||||
_buildPremiumFeature(
|
||||
'Personalisierte Spiele',
|
||||
Icons.person,
|
||||
),
|
||||
_buildPremiumFeature(
|
||||
'Keine Werbung',
|
||||
Icons.block,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Subscription price
|
||||
const Text(
|
||||
'Nur 4,99€ pro Monat',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Bubblegum',
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.secondaryColor,
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
'Jederzeit kündbar',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text('Später'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.secondaryColor,
|
||||
),
|
||||
child: const Text('Jetzt abonnieren'),
|
||||
onPressed: () {
|
||||
// In a real app, this would trigger the subscription flow
|
||||
Navigator.of(context).pop();
|
||||
|
||||
// For demo purposes, just show a success message
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Dies ist eine Demo - Kein Abonnement abgeschlossen'),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPremiumFeature(String text, IconData icon) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 20,
|
||||
color: AppTheme.secondaryColor,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(text),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
633
lib/screens/parent_dashboard.dart
Normal file
@ -0,0 +1,633 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/user_profile.dart';
|
||||
import '../utils/app_theme.dart';
|
||||
|
||||
class ParentDashboard extends StatefulWidget {
|
||||
const ParentDashboard({super.key});
|
||||
|
||||
@override
|
||||
State<ParentDashboard> createState() => _ParentDashboardState();
|
||||
}
|
||||
|
||||
class _ParentDashboardState extends State<ParentDashboard> with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
UserProfile _userProfile = sampleUserProfile; // Use the sample profile for demo
|
||||
final TextEditingController _childNameController = TextEditingController();
|
||||
final TextEditingController _childAgeController = TextEditingController();
|
||||
String _selectedPin = '1234'; // Default PIN
|
||||
bool _isChangingPin = false;
|
||||
final TextEditingController _newPinController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 3, vsync: this);
|
||||
|
||||
// Initialize controllers with user data
|
||||
_childNameController.text = _userProfile.childName;
|
||||
_childAgeController.text = _userProfile.childAge;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
_childNameController.dispose();
|
||||
_childAgeController.dispose();
|
||||
_newPinController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Elternbereich'),
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
indicatorColor: Colors.white,
|
||||
tabs: const [
|
||||
Tab(text: 'Profil'),
|
||||
Tab(text: 'Abonnement'),
|
||||
Tab(text: 'Einstellungen'),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
// Profile Tab
|
||||
_buildProfileTab(),
|
||||
|
||||
// Subscription Tab
|
||||
_buildSubscriptionTab(),
|
||||
|
||||
// Settings Tab
|
||||
_buildSettingsTab(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProfileTab() {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Profile card
|
||||
Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Profile header
|
||||
const Text(
|
||||
'Kinderprofil',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Bubblegum',
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Child name
|
||||
TextFormField(
|
||||
controller: _childNameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Name des Kindes',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.person),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Child age
|
||||
TextFormField(
|
||||
controller: _childAgeController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Alter des Kindes',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.cake),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Save button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: _saveProfile,
|
||||
child: const Text('Speichern'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Statistics card
|
||||
Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Statistics header
|
||||
const Text(
|
||||
'Spielstatistik',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Bubblegum',
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Statistics
|
||||
_buildStatisticsItem('Geräusche-Detektiv', _userProfile.gameStats['animal_sounds'] ?? 0),
|
||||
_buildStatisticsItem('Ich sehe was', _userProfile.gameStats['i_spy'] ?? 0),
|
||||
_buildStatisticsItem('Reime-Reise', _userProfile.gameStats['rhyme_game'] ?? 0),
|
||||
_buildStatisticsItem('Geschichtenzeit', _userProfile.gameStats['story_time'] ?? 0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubscriptionTab() {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Current plan card
|
||||
Card(
|
||||
elevation: 4,
|
||||
color: _userProfile.isPremium ? AppTheme.secondaryColor : null,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Plan header
|
||||
Text(
|
||||
'Aktueller Plan',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Bubblegum',
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _userProfile.isPremium ? Colors.white : null,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Plan details
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
_userProfile.isPremium ? Icons.star : Icons.star_border,
|
||||
size: 48,
|
||||
color: _userProfile.isPremium ? Colors.white : AppTheme.secondaryColor,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_userProfile.isPremium ? 'Premium' : 'Kostenlos',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Bubblegum',
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _userProfile.isPremium ? Colors.white : null,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_userProfile.isPremium
|
||||
? 'Aktiv bis: ${_userProfile.premiumExpiryDate?.toString().split(' ')[0] ?? 'Unbekannt'}'
|
||||
: 'Nur Offline-Spiele verfügbar',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: _userProfile.isPremium ? Colors.white70 : AppTheme.secondaryTextColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Subscription button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _userProfile.isPremium ? Colors.white : null,
|
||||
foregroundColor: _userProfile.isPremium ? AppTheme.secondaryColor : null,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
// Toggle premium status (demo only)
|
||||
_userProfile = _userProfile.copyWith(
|
||||
isPremium: !_userProfile.isPremium,
|
||||
premiumExpiryDate: _userProfile.isPremium
|
||||
? null
|
||||
: DateTime.now().add(const Duration(days: 30)),
|
||||
);
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(_userProfile.isPremium
|
||||
? 'Premium aktiviert (Demo)'
|
||||
: 'Premium deaktiviert (Demo)'),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(_userProfile.isPremium
|
||||
? 'Abonnement kündigen'
|
||||
: 'Jetzt Premium werden'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Subscription benefits
|
||||
if (!_userProfile.isPremium)
|
||||
Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Premium-Vorteile',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Bubblegum',
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Benefits
|
||||
_buildBenefitItem('Unbegrenzt neue Spiele durch AI'),
|
||||
_buildBenefitItem('Dynamisch reagierender Spielleiter'),
|
||||
_buildBenefitItem('Personalisierte Spiele'),
|
||||
_buildBenefitItem('Keine Werbung'),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Price
|
||||
const Text(
|
||||
'Nur 4,99€ pro Monat',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.secondaryColor,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const Text(
|
||||
'Jederzeit kündbar',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSettingsTab() {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Security card
|
||||
Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Sicherheit',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Bubblegum',
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// PIN settings
|
||||
if (_isChangingPin)
|
||||
Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _newPinController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Neuer PIN',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.lock_outline),
|
||||
),
|
||||
obscureText: true,
|
||||
keyboardType: TextInputType.number,
|
||||
maxLength: 4,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isChangingPin = false;
|
||||
_newPinController.clear();
|
||||
});
|
||||
},
|
||||
child: const Text('Abbrechen'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
if (_newPinController.text.length == 4) {
|
||||
setState(() {
|
||||
_selectedPin = _newPinController.text;
|
||||
_isChangingPin = false;
|
||||
_newPinController.clear();
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('PIN geändert'),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('PIN muss 4 Zahlen haben'),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Text('Speichern'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
ListTile(
|
||||
title: const Text('PIN für Elternbereich ändern'),
|
||||
subtitle: const Text('Aktueller PIN: ****'),
|
||||
leading: const Icon(Icons.lock_outline),
|
||||
trailing: const Icon(Icons.edit),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_isChangingPin = true;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// App settings card
|
||||
Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'App-Einstellungen',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Bubblegum',
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Sound settings
|
||||
SwitchListTile(
|
||||
title: const Text('Hintergrundmusik'),
|
||||
subtitle: const Text('Musik im Hintergrund abspielen'),
|
||||
value: true,
|
||||
onChanged: (value) {
|
||||
// In a real app, this would toggle background music
|
||||
},
|
||||
),
|
||||
|
||||
SwitchListTile(
|
||||
title: const Text('Soundeffekte'),
|
||||
subtitle: const Text('Soundeffekte während des Spiels'),
|
||||
value: true,
|
||||
onChanged: (value) {
|
||||
// In a real app, this would toggle sound effects
|
||||
},
|
||||
),
|
||||
|
||||
// Voice settings
|
||||
ListTile(
|
||||
title: const Text('Sprachgeschwindigkeit'),
|
||||
subtitle: const Text('Mittel'),
|
||||
leading: const Icon(Icons.speed),
|
||||
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
|
||||
onTap: () {
|
||||
// In a real app, this would open voice speed settings
|
||||
},
|
||||
),
|
||||
|
||||
// Language settings
|
||||
ListTile(
|
||||
title: const Text('Sprache'),
|
||||
subtitle: const Text('Deutsch'),
|
||||
leading: const Icon(Icons.language),
|
||||
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
|
||||
onTap: () {
|
||||
// In a real app, this would open language settings
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// About card
|
||||
Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Über die App',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Bubblegum',
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// About items
|
||||
ListTile(
|
||||
title: const Text('Version'),
|
||||
subtitle: const Text('1.0.0'),
|
||||
leading: const Icon(Icons.info_outline),
|
||||
),
|
||||
|
||||
ListTile(
|
||||
title: const Text('Datenschutz'),
|
||||
leading: const Icon(Icons.privacy_tip_outlined),
|
||||
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
|
||||
onTap: () {
|
||||
// In a real app, this would open privacy policy
|
||||
},
|
||||
),
|
||||
|
||||
ListTile(
|
||||
title: const Text('Nutzungsbedingungen'),
|
||||
leading: const Icon(Icons.description_outlined),
|
||||
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
|
||||
onTap: () {
|
||||
// In a real app, this would open terms of service
|
||||
},
|
||||
),
|
||||
|
||||
ListTile(
|
||||
title: const Text('Support kontaktieren'),
|
||||
leading: const Icon(Icons.support_agent),
|
||||
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
|
||||
onTap: () {
|
||||
// In a real app, this would open support contact options
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatisticsItem(String name, int count) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(name),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Text(
|
||||
count.toString(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBenefitItem(String text) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.check_circle,
|
||||
color: AppTheme.secondaryColor,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(text),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _saveProfile() {
|
||||
setState(() {
|
||||
_userProfile = _userProfile.copyWith(
|
||||
childName: _childNameController.text,
|
||||
childAge: _childAgeController.text,
|
||||
);
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Profil gespeichert'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
166
lib/screens/splash_screen.dart
Normal file
@ -0,0 +1,166 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../utils/app_theme.dart';
|
||||
import 'home_screen.dart';
|
||||
|
||||
class SplashScreen extends StatefulWidget {
|
||||
const SplashScreen({super.key});
|
||||
|
||||
@override
|
||||
State<SplashScreen> createState() => _SplashScreenState();
|
||||
}
|
||||
|
||||
class _SplashScreenState extends State<SplashScreen> with SingleTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _scaleAnimation;
|
||||
late Animation<double> _rotationAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Initialize animation controller
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
|
||||
// Create scale animation
|
||||
_scaleAnimation = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: Curves.elasticOut,
|
||||
),
|
||||
);
|
||||
|
||||
// Create rotation animation
|
||||
_rotationAnimation = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 0.1,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: Curves.elasticInOut,
|
||||
),
|
||||
);
|
||||
|
||||
// Start the animation
|
||||
_animationController.forward();
|
||||
|
||||
// Navigate to home screen after 3 seconds
|
||||
Timer(const Duration(seconds: 3), () {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const HomeScreen()),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
AppTheme.primaryColor,
|
||||
AppTheme.accentColor,
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Animated Logo
|
||||
AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _scaleAnimation.value,
|
||||
child: Transform.rotate(
|
||||
angle: _rotationAnimation.value,
|
||||
child: Container(
|
||||
width: 150,
|
||||
height: 150,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(75),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 10,
|
||||
spreadRadius: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Icons.directions_car,
|
||||
size: 80,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 30), // App Name
|
||||
const Text(
|
||||
'RoadTrip Kids',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Comic Sans MS', // Changed from 'Bubblegum'
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
shadows: [
|
||||
Shadow(
|
||||
blurRadius: 10.0,
|
||||
color: Colors.black26,
|
||||
offset: Offset(2.0, 2.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// Tagline
|
||||
const Text(
|
||||
'Spaß auf jeder Fahrt!',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Comic Sans MS', // Changed from 'Bubblegum'
|
||||
fontSize: 18,
|
||||
color: Colors.white,
|
||||
shadows: [
|
||||
Shadow(
|
||||
blurRadius: 5.0,
|
||||
color: Colors.black26,
|
||||
offset: Offset(1.0, 1.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 50),
|
||||
// Loading indicator
|
||||
const CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
157
lib/services/openai_service.dart
Normal file
@ -0,0 +1,157 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class OpenAIService extends ChangeNotifier {
|
||||
static const String _baseUrl = 'https://api.openai.com/v1';
|
||||
String _apiKey = ''; // API key will be set through Firebase Cloud Functions
|
||||
bool _isLoading = false;
|
||||
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
// This would be set through Firebase to keep the API key secure
|
||||
void setApiKey(String apiKey) {
|
||||
_apiKey = apiKey;
|
||||
}
|
||||
|
||||
// Generate game content using OpenAI API
|
||||
Future<Map<String, dynamic>?> generateGameContent({
|
||||
required String gameType,
|
||||
String? childAge,
|
||||
String? location,
|
||||
String? previousResponse,
|
||||
int maxTokens = 150,
|
||||
}) async {
|
||||
if (_apiKey.isEmpty) {
|
||||
debugPrint('API key not set');
|
||||
return null;
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
// Create a prompt based on the game type
|
||||
String prompt = _createGamePrompt(
|
||||
gameType: gameType,
|
||||
childAge: childAge,
|
||||
location: location,
|
||||
previousResponse: previousResponse,
|
||||
);
|
||||
|
||||
final response = await http.post(
|
||||
Uri.parse('$_baseUrl/chat/completions'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer $_apiKey',
|
||||
},
|
||||
body: jsonEncode({
|
||||
'model': 'gpt-4-turbo', // Use the appropriate model
|
||||
'messages': [ {
|
||||
'role': 'system',
|
||||
'content': 'You are a friendly game host designed to entertain children aged 4-6 during car trips. '
|
||||
'Keep your responses simple, engaging, and appropriate for young children. '
|
||||
'Use short sentences and friendly language. Answer in German.'
|
||||
},
|
||||
{
|
||||
'role': 'user',
|
||||
'content': prompt
|
||||
}
|
||||
],
|
||||
'max_tokens': maxTokens,
|
||||
'temperature': 0.7,
|
||||
}),
|
||||
);
|
||||
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
final content = data['choices'][0]['message']['content'];
|
||||
|
||||
// Parse the content based on the game type
|
||||
return _parseGameResponse(gameType, content);
|
||||
} else {
|
||||
debugPrint('Error: ${response.statusCode}, ${response.body}');
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Exception: $e');
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Create game-specific prompts
|
||||
String _createGamePrompt({
|
||||
required String gameType,
|
||||
String? childAge,
|
||||
String? location,
|
||||
String? previousResponse,
|
||||
}) {
|
||||
switch (gameType) { case 'animal_sounds':
|
||||
return 'Denke dir ein Tier aus und beschreibe das Geräusch, das es macht. '
|
||||
'Stelle eine Frage an das Kind, welches Tier dieses Geräusch macht. '
|
||||
'Halte es einfach und kindgerecht für ${childAge ?? '4-6'}-jährige.';
|
||||
case 'i_spy':
|
||||
return 'Spiele "Ich sehe was, was du nicht siehst" mit einem Kind im Alter von ${childAge ?? '4-6'} Jahren. '
|
||||
'Beschreibe ein Objekt, das typischerweise in einem Auto oder auf einer Straße zu sehen ist. '
|
||||
'Beginne mit "Ich sehe was, was du nicht siehst, und das ist..." '
|
||||
'${location != null ? "Wir befinden uns gerade in der Nähe von $location." : ""}';
|
||||
case 'rhyme_game':
|
||||
return 'Denke dir ein einfaches Wort aus und frage das Kind nach einem Reimwort. '
|
||||
'Das Wort sollte einfach sein und viele mögliche Reimwörter haben. '
|
||||
'Für Kinder im Alter von ${childAge ?? '4-6'} Jahren.';
|
||||
case 'story_time':
|
||||
return 'Erzähle den Anfang einer kurzen, kindgerechten Geschichte für ein ${childAge ?? '4-6'}-jähriges Kind. '
|
||||
'Die Geschichte sollte interaktiv sein, mit einer einfachen Frage am Ende, '
|
||||
'damit das Kind entscheiden kann, wie die Geschichte weitergehen soll.';
|
||||
case 'guess_object':
|
||||
return 'Denke dir ein Objekt aus, das Kinder kennen. '
|
||||
'Gib 3 einfache Hinweise und frage ein Kind im Alter von ${childAge ?? '4-6'} Jahren, was es sein könnte. '
|
||||
'${previousResponse != null ? "Das Kind hat geantwortet: \"$previousResponse\". Reagiere darauf." : ""}';
|
||||
default:
|
||||
return 'Schlage ein einfaches, interaktives Spiel für ein Kind im Alter von ${childAge ?? '4-6'} Jahren vor, '
|
||||
'das während einer Autofahrt gespielt werden kann. Erkläre kurz die Regeln.';
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the AI response based on game type
|
||||
Map<String, dynamic> _parseGameResponse(String gameType, String content) {
|
||||
// Default structure for game responses
|
||||
final response = {
|
||||
'text': content,
|
||||
'question': '',
|
||||
'answer': '',
|
||||
'hints': <String>[],
|
||||
'options': <String>[],
|
||||
};
|
||||
|
||||
// Extract specific parts based on game type
|
||||
switch (gameType) {
|
||||
case 'animal_sounds':
|
||||
// Try to extract the animal name as answer
|
||||
final animalRegex = RegExp(r'(?:ist|ein|der|die|das)\s+([A-Za-zäöüÄÖÜß]+)', caseSensitive: false);
|
||||
final match = animalRegex.firstMatch(content);
|
||||
if (match != null && match.groupCount >= 1) {
|
||||
response['answer'] = match.group(1)!;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'i_spy':
|
||||
// Try to extract color or property and object
|
||||
final propertyRegex = RegExp(r'das ist\s+([A-Za-zäöüÄÖÜß]+)', caseSensitive: false);
|
||||
final match = propertyRegex.firstMatch(content);
|
||||
if (match != null && match.groupCount >= 1) {
|
||||
response['answer'] = match.group(1)!;
|
||||
}
|
||||
break;
|
||||
|
||||
// Add more game-specific parsing as needed
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
244
lib/services/speech_service.dart
Normal file
@ -0,0 +1,244 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:speech_to_text/speech_to_text.dart' as stt;
|
||||
|
||||
class SpeechService extends ChangeNotifier {
|
||||
final stt.SpeechToText _speech = stt.SpeechToText();
|
||||
bool _isListening = false;
|
||||
String _lastRecognizedWords = '';
|
||||
bool _isAvailable = false;
|
||||
bool _isInitialized = false;
|
||||
|
||||
bool get isListening => _isListening;
|
||||
String get lastRecognizedWords => _lastRecognizedWords;
|
||||
bool get isAvailable => _isAvailable;
|
||||
bool get isInitialized => _isInitialized;
|
||||
|
||||
SpeechService() {
|
||||
_initSpeech();
|
||||
} // Initialize speech recognition with optimized settings
|
||||
Future<void> _initSpeech() async {
|
||||
try {
|
||||
_isAvailable = await _speech.initialize(
|
||||
onError: (error) {
|
||||
debugPrint('Speech recognition error: $error');
|
||||
// Try to recover from errors by reinitializing
|
||||
if (error.errorMsg.contains('error_audio_error') ||
|
||||
error.errorMsg.contains('error_no_match')) {
|
||||
debugPrint('Attempting to recover from speech error...');
|
||||
Future.delayed(const Duration(milliseconds: 1000), () {
|
||||
if (!_isInitialized) {
|
||||
_initSpeech();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
onStatus: (status) {
|
||||
debugPrint('Speech recognition status: $status');
|
||||
if (status == 'listening') {
|
||||
debugPrint('Speech recognition is now actively listening');
|
||||
}
|
||||
},
|
||||
);
|
||||
_isInitialized = true;
|
||||
debugPrint('Speech recognition initialized, available: $_isAvailable');
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('Error initializing speech recognition: $e');
|
||||
_isAvailable = false;
|
||||
_isInitialized = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}// Start listening for speech input with improved reliability and sensitivity
|
||||
Future<bool> startListening({
|
||||
String? localeId,
|
||||
Function(String text)? onResult,
|
||||
Function()? onComplete,
|
||||
int listenTimeoutSeconds = 60, // Longer listening time
|
||||
int pauseTimeoutSeconds = 2, // Shorter pause timeout for better responsiveness
|
||||
}) async {
|
||||
if (!_isInitialized) {
|
||||
debugPrint('Speech service not initialized, trying to initialize...');
|
||||
await _initSpeech();
|
||||
if (!_isInitialized) {
|
||||
// Try one more time after a delay
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
await _initSpeech();
|
||||
if (!_isInitialized) {
|
||||
debugPrint('Could not initialize speech service after retry');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If already listening, stop first
|
||||
if (_isListening) {
|
||||
await stopListening();
|
||||
// Add short delay to make sure previous session is fully stopped
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
}
|
||||
|
||||
if (_isAvailable) {
|
||||
_isListening = true;
|
||||
_lastRecognizedWords = '';
|
||||
notifyListeners();
|
||||
|
||||
debugPrint('Starting to listen for speech input');
|
||||
try {
|
||||
// Cancel any Bluetooth headset connections if they're confusing the app
|
||||
// This is a workaround for the repeated BluetoothHeadset logs
|
||||
try {
|
||||
await _speech.stop();
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
} catch (e) {
|
||||
// Ignore errors here, just trying to clean up
|
||||
}
|
||||
await _speech.listen(
|
||||
localeId: localeId ?? 'de_DE', // Set German as default, can be changed
|
||||
listenFor: Duration(seconds: listenTimeoutSeconds),
|
||||
pauseFor: Duration(seconds: pauseTimeoutSeconds),
|
||||
partialResults: true,
|
||||
// Make speech recognition more sensitive
|
||||
onSoundLevelChange: (level) {
|
||||
// Optional: could use this to show visual feedback of sound level
|
||||
},
|
||||
onResult: (result) {
|
||||
// Only update if we have actual words
|
||||
if (result.recognizedWords.isNotEmpty) {
|
||||
_lastRecognizedWords = result.recognizedWords;
|
||||
debugPrint('Speech recognized: $_lastRecognizedWords');
|
||||
if (onResult != null) {
|
||||
try {
|
||||
onResult(_lastRecognizedWords);
|
||||
} catch (e) {
|
||||
debugPrint('Error in onResult callback: $e');
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Handle completion with more reliability
|
||||
_speech.statusListener = (status) {
|
||||
debugPrint('Speech recognition status: $status');
|
||||
if (status == 'done' || status == 'notListening') {
|
||||
_isListening = false;
|
||||
|
||||
// Delay the onComplete callback slightly to ensure all results are processed
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
if (onComplete != null) {
|
||||
// Use try-catch to prevent setState errors after dispose
|
||||
try {
|
||||
onComplete();
|
||||
} catch (e) {
|
||||
debugPrint('Error calling onComplete: $e');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
};
|
||||
|
||||
// Add error handler explicitly
|
||||
_speech.errorListener = (error) {
|
||||
debugPrint('Speech recognition error: $error');
|
||||
// If we get a timeout error, we should consider whether to retry
|
||||
if (error.errorMsg == 'error_speech_timeout' && _isListening) {
|
||||
_isListening = false;
|
||||
notifyListeners();
|
||||
|
||||
// Let the caller know there was a timeout so they can restart if needed
|
||||
if (onComplete != null) {
|
||||
try {
|
||||
onComplete();
|
||||
} catch (e) {
|
||||
debugPrint('Error calling onComplete after timeout: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint('Error starting speech recognition: $e');
|
||||
_isListening = false;
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
debugPrint('Speech recognition not available');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Stop listening with improved reliability
|
||||
Future<void> stopListening() async {
|
||||
if (_isListening) {
|
||||
try {
|
||||
// First try to cancel any Bluetooth connections
|
||||
try {
|
||||
await _speech.cancel();
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
} catch (e) {
|
||||
// Ignore errors here
|
||||
}
|
||||
|
||||
// Then properly stop
|
||||
await _speech.stop();
|
||||
} catch (e) {
|
||||
debugPrint('Error stopping speech recognition: $e');
|
||||
|
||||
// If stop fails, try to force a reset
|
||||
try {
|
||||
await _speech.cancel();
|
||||
} catch (_) {
|
||||
// Ignore nested errors
|
||||
}
|
||||
} finally {
|
||||
_isListening = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel listening with improved reliability
|
||||
Future<void> cancelListening() async {
|
||||
if (_isListening) {
|
||||
try {
|
||||
await _speech.cancel();
|
||||
} catch (e) {
|
||||
debugPrint('Error cancelling speech recognition: $e');
|
||||
|
||||
// Try more forcefully
|
||||
try {
|
||||
await _speech.stop();
|
||||
} catch (_) {
|
||||
// Ignore nested errors
|
||||
}
|
||||
|
||||
// If still failing, try to reset the speech recognition
|
||||
if (_isListening) {
|
||||
_isInitialized = false;
|
||||
await _initSpeech();
|
||||
}
|
||||
} finally {
|
||||
_isListening = false;
|
||||
_lastRecognizedWords = '';
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset recognized words
|
||||
void resetRecognizedWords() {
|
||||
_lastRecognizedWords = '';
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Check if speech matches expected keywords/phrases
|
||||
bool checkSpeechMatch(List<String> keywords) {
|
||||
if (_lastRecognizedWords.isEmpty) return false;
|
||||
final String normalizedSpeech = _lastRecognizedWords.toLowerCase();
|
||||
return keywords.any((keyword) => normalizedSpeech.contains(keyword.toLowerCase()));
|
||||
}
|
||||
}
|
||||
200
lib/services/tts_service.dart
Normal file
@ -0,0 +1,200 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_tts/flutter_tts.dart';
|
||||
|
||||
class TTSService extends ChangeNotifier {
|
||||
final FlutterTts _flutterTts = FlutterTts();
|
||||
bool _isSpeaking = false; String _currentUtterance = ''; double _volume = 1.0;
|
||||
double _pitch = 1.0; // Neutral pitch for more natural sound
|
||||
double _rate = 0.5; // More natural speech rate (less sloth-like)
|
||||
|
||||
bool get isSpeaking => _isSpeaking;
|
||||
String get currentUtterance => _currentUtterance;
|
||||
|
||||
TTSService() {
|
||||
_initTts();
|
||||
}
|
||||
|
||||
// Initialize text-to-speech with better quality settings
|
||||
Future<void> _initTts() async {
|
||||
await _flutterTts.setLanguage('de-DE'); // Set German as default language await _flutterTts.setVolume(_volume);
|
||||
await _flutterTts.setPitch(_pitch);
|
||||
// Use a more natural speech rate that doesn't sound too slow
|
||||
await _flutterTts.setSpeechRate(_rate);
|
||||
// Keep configuration simpler to avoid potential TTS issues
|
||||
debugPrint('TTS initialized with standard parameters');
|
||||
|
||||
// Try to improve quality and reliability
|
||||
try {
|
||||
// Try to get available engines
|
||||
final engines = await _flutterTts.getEngines;
|
||||
debugPrint('Available TTS engines: $engines');
|
||||
|
||||
// Try to use Google's TTS engine if available
|
||||
if (engines != null && engines.contains('com.google.android.tts')) {
|
||||
await _flutterTts.setEngine('com.google.android.tts');
|
||||
debugPrint('Using Google TTS engine');
|
||||
}
|
||||
|
||||
// Enable awaiting speak completion (safely)
|
||||
try {
|
||||
await _flutterTts.awaitSpeakCompletion(true);
|
||||
} catch (e) {
|
||||
debugPrint('awaitSpeakCompletion not supported: $e');
|
||||
}
|
||||
|
||||
// Get available voices to check if everything is working
|
||||
try {
|
||||
final voices = await _flutterTts.getVoices;
|
||||
debugPrint('Available voices: ${voices?.length ?? 0}');
|
||||
} catch (e) {
|
||||
debugPrint('Error getting voices: $e');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Setting TTS options failed: $e');
|
||||
}
|
||||
|
||||
_flutterTts.setStartHandler(() {
|
||||
_isSpeaking = true;
|
||||
notifyListeners();
|
||||
});
|
||||
|
||||
_flutterTts.setCompletionHandler(() {
|
||||
_isSpeaking = false;
|
||||
notifyListeners();
|
||||
});
|
||||
|
||||
_flutterTts.setErrorHandler((error) {
|
||||
_isSpeaking = false;
|
||||
debugPrint('TTS Error: $error');
|
||||
notifyListeners();
|
||||
});
|
||||
|
||||
_flutterTts.setCancelHandler(() {
|
||||
_isSpeaking = false;
|
||||
notifyListeners();
|
||||
});
|
||||
|
||||
debugPrint('TTS initialized successfully with improved quality settings');
|
||||
} // Speak text with improved clarity
|
||||
Future<void> speak(String text, {Function? onComplete}) async {
|
||||
if (text.isEmpty) return;
|
||||
|
||||
// Make sure any previous speech is stopped
|
||||
if (_isSpeaking) {
|
||||
await stop();
|
||||
// Wait a moment to ensure the TTS engine has fully stopped
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
}
|
||||
|
||||
_currentUtterance = text;
|
||||
debugPrint('TTS speaking: $text');
|
||||
|
||||
// No segmentation - speak the entire text at once to prevent stuttering
|
||||
// This ensures game titles don't get split and repeated
|
||||
// Set completion handler to call onComplete callback
|
||||
_flutterTts.setCompletionHandler(() {
|
||||
_isSpeaking = false;
|
||||
|
||||
// Add a short delay before calling onComplete
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
// Call the provided completion callback if any
|
||||
if (onComplete != null) {
|
||||
try {
|
||||
onComplete();
|
||||
} catch (e) {
|
||||
debugPrint('Error in onComplete callback: $e');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
notifyListeners();
|
||||
});
|
||||
|
||||
// Speak the text - simple and direct approach to prevent stuttering
|
||||
try {
|
||||
// Just speak the entire text at once
|
||||
_isSpeaking = true;
|
||||
notifyListeners();
|
||||
await _flutterTts.speak(text);
|
||||
} catch (e) {
|
||||
debugPrint('Error speaking text: $e');
|
||||
_isSpeaking = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// Stop speaking
|
||||
Future<void> stop() async {
|
||||
await _flutterTts.stop();
|
||||
_isSpeaking = false;
|
||||
notifyListeners();
|
||||
}
|
||||
// Set volume
|
||||
Future<void> setVolume(double volume) async {
|
||||
if (volume >= 0.0 && volume <= 1.0) {
|
||||
_volume = volume;
|
||||
await _flutterTts.setVolume(volume);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// Set pitch
|
||||
Future<void> setPitch(double pitch) async {
|
||||
if (pitch >= 0.5 && pitch <= 2.0) {
|
||||
_pitch = pitch;
|
||||
await _flutterTts.setPitch(pitch);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// Set speech rate
|
||||
Future<void> setRate(double rate) async {
|
||||
if (rate >= 0.1 && rate <= 1.0) {
|
||||
_rate = rate;
|
||||
await _flutterTts.setSpeechRate(rate);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// Set language
|
||||
Future<void> setLanguage(String language) async {
|
||||
await _flutterTts.setLanguage(language);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Method to say a short acknowledgement phrase
|
||||
// This can be used to give quick feedback without interrupting the flow
|
||||
Future<void> sayFeedback(String feedback) async {
|
||||
if (feedback.isEmpty) return;
|
||||
|
||||
// Don't interrupt existing speech for feedback
|
||||
if (_isSpeaking) {
|
||||
debugPrint('Not giving feedback - already speaking');
|
||||
return;
|
||||
}
|
||||
|
||||
_currentUtterance = feedback;
|
||||
debugPrint('TTS feedback: $feedback');
|
||||
|
||||
// Use a faster rate for quick feedback
|
||||
final originalRate = _rate;
|
||||
await _flutterTts.setSpeechRate(0.45); // Slightly faster for feedback
|
||||
|
||||
try {
|
||||
// Speak the feedback directly without pauses
|
||||
await _flutterTts.speak(feedback);
|
||||
} catch (e) {
|
||||
debugPrint('Error speaking feedback: $e');
|
||||
} finally {
|
||||
// Reset to original rate
|
||||
await _flutterTts.setSpeechRate(originalRate);
|
||||
}
|
||||
}
|
||||
|
||||
// Dispose
|
||||
@override
|
||||
void dispose() {
|
||||
_flutterTts.stop();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
113
lib/utils/app_theme.dart
Normal file
@ -0,0 +1,113 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppTheme {
|
||||
// Primary Colors
|
||||
static const Color primaryColor = Color(0xFF4CAF50); // Vibrant Green
|
||||
static const Color secondaryColor = Color(0xFFFF9800); // Playful Orange
|
||||
static const Color accentColor = Color(0xFF03A9F4); // Sky Blue
|
||||
|
||||
// Background Colors
|
||||
static const Color backgroundColor = Color(0xFFF5F5F5); // Light Grey
|
||||
static const Color cardColor = Colors.white;
|
||||
|
||||
// Text Colors
|
||||
static const Color primaryTextColor = Color(0xFF212121); // Dark Grey
|
||||
static const Color secondaryTextColor = Color(0xFF757575); // Medium Grey
|
||||
static const Color buttonTextColor = Colors.white;
|
||||
|
||||
// Game-specific Colors
|
||||
static const Color correctAnswerColor = Color(0xFF4CAF50); // Green
|
||||
static const Color wrongAnswerColor = Color(0xFFF44336); // Red
|
||||
static const Color hintColor = Color(0xFFFFEB3B); // Yellow
|
||||
|
||||
// Fun Colors for Different Game Categories
|
||||
static const Color animalCategoryColor = Color(0xFFE91E63); // Pink
|
||||
static const Color natureCategoryColor = Color(0xFF009688); // Teal
|
||||
static const Color vehicleCategoryColor = Color(0xFF673AB7); // Purple
|
||||
static const Color foodCategoryColor = Color(0xFFFF5722); // Deep Orange
|
||||
// Child-friendly color palette
|
||||
static const List<Color> gameColors = [
|
||||
Color(0xFFFF5252), // Red
|
||||
Color(0xFF536DFE), // Blue
|
||||
Color(0xFFFFD600), // Yellow
|
||||
Color(0xFF00E676), // Green
|
||||
Color(0xFFFF6D00), // Orange
|
||||
Color(0xFFD500F9), // Purple
|
||||
Color(0xFF00B0FF), // Light Blue
|
||||
Color(0xFFFFAB00), // Amber
|
||||
];
|
||||
|
||||
// Theme data
|
||||
static ThemeData lightTheme = ThemeData(
|
||||
primaryColor: primaryColor,
|
||||
colorScheme: ColorScheme.light(
|
||||
primary: primaryColor,
|
||||
secondary: secondaryColor,
|
||||
),
|
||||
scaffoldBackgroundColor: backgroundColor,
|
||||
cardTheme: const CardThemeData(
|
||||
color: cardColor,
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||
),
|
||||
),
|
||||
textTheme: const TextTheme( displayLarge: TextStyle(
|
||||
// Using system font instead of 'Bubblegum'
|
||||
fontFamily: 'Comic Sans MS',
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: primaryTextColor,
|
||||
),
|
||||
displayMedium: TextStyle(
|
||||
// Using system font instead of 'Bubblegum'
|
||||
fontFamily: 'Comic Sans MS',
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: primaryTextColor,
|
||||
),
|
||||
bodyLarge: TextStyle(
|
||||
// Using system font instead of 'Bubblegum'
|
||||
fontFamily: 'Comic Sans MS',
|
||||
fontSize: 18,
|
||||
color: primaryTextColor,
|
||||
),
|
||||
bodyMedium: TextStyle(
|
||||
// Using system font instead of 'Bubblegum'
|
||||
fontFamily: 'Comic Sans MS',
|
||||
fontSize: 16,
|
||||
color: secondaryTextColor,
|
||||
),
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: primaryColor,
|
||||
foregroundColor: buttonTextColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24), textStyle: const TextStyle(
|
||||
// Using system font instead of 'Bubblegum'
|
||||
fontFamily: 'Comic Sans MS',
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
centerTitle: true,
|
||||
elevation: 0, titleTextStyle: TextStyle(
|
||||
// Using system font instead of 'Bubblegum'
|
||||
fontFamily: 'Comic Sans MS',
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
iconTheme: const IconThemeData(
|
||||
color: primaryColor,
|
||||
size: 28,
|
||||
),
|
||||
);
|
||||
}
|
||||
376
lib/widgets/animated_character.dart
Normal file
@ -0,0 +1,376 @@
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../services/tts_service.dart';
|
||||
|
||||
class AnimatedCharacter extends StatefulWidget {
|
||||
final String mood;
|
||||
|
||||
const AnimatedCharacter({
|
||||
this.mood = 'happy',
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AnimatedCharacter> createState() => _AnimatedCharacterState();
|
||||
}
|
||||
|
||||
class _AnimatedCharacterState extends State<AnimatedCharacter> with SingleTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _bounceAnimation;
|
||||
late Animation<double> _mouthAnimation;
|
||||
bool _isSpeaking = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Initialize animation controller
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
);
|
||||
|
||||
// Create bounce animation
|
||||
_bounceAnimation = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
);
|
||||
|
||||
// Create mouth animation
|
||||
_mouthAnimation = Tween<double>(
|
||||
begin: 0.2,
|
||||
end: 0.5,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
);
|
||||
|
||||
// Start bounce animation
|
||||
_animationController.repeat(reverse: true);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Listen to TTS service to animate mouth when speaking
|
||||
final ttsService = Provider.of<TTSService>(context);
|
||||
_isSpeaking = ttsService.isSpeaking;
|
||||
|
||||
// If speaking, make animation faster
|
||||
if (_isSpeaking && _animationController.duration?.inMilliseconds != 200) {
|
||||
_animationController.stop();
|
||||
_animationController.duration = const Duration(milliseconds: 200);
|
||||
_animationController.repeat(reverse: true);
|
||||
} else if (!_isSpeaking && _animationController.duration?.inMilliseconds != 500) {
|
||||
_animationController.stop();
|
||||
_animationController.duration = const Duration(milliseconds: 500);
|
||||
_animationController.repeat(reverse: true);
|
||||
}
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (context, child) {
|
||||
return Transform.translate(
|
||||
offset: Offset(0, _bounceAnimation.value * 5 - 2.5),
|
||||
child: SizedBox(
|
||||
height: 200,
|
||||
width: 200,
|
||||
child: CustomPaint(
|
||||
painter: CharacterPainter(
|
||||
mood: widget.mood,
|
||||
mouthOpenRatio: _isSpeaking ? _mouthAnimation.value : 0.2,
|
||||
blinkEyes: _bounceAnimation.value > 0.8,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CharacterPainter extends CustomPainter {
|
||||
final String mood;
|
||||
final double mouthOpenRatio;
|
||||
final bool blinkEyes;
|
||||
|
||||
CharacterPainter({
|
||||
required this.mood,
|
||||
required this.mouthOpenRatio,
|
||||
required this.blinkEyes,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final center = Offset(size.width / 2, size.height / 2);
|
||||
final radius = size.width / 2;
|
||||
|
||||
// Define colors based on mood
|
||||
Color faceColor;
|
||||
switch (mood) {
|
||||
case 'happy':
|
||||
faceColor = Colors.amber;
|
||||
break;
|
||||
case 'sad':
|
||||
faceColor = Colors.lightBlue;
|
||||
break;
|
||||
case 'excited':
|
||||
faceColor = Colors.orange;
|
||||
break;
|
||||
case 'thinking':
|
||||
faceColor = Colors.purple;
|
||||
break;
|
||||
default:
|
||||
faceColor = Colors.amber;
|
||||
}
|
||||
|
||||
// Paint face
|
||||
final facePaint = Paint()
|
||||
..color = faceColor
|
||||
..style = PaintingStyle.fill;
|
||||
canvas.drawCircle(center, radius * 0.9, facePaint);
|
||||
|
||||
// Draw shadow
|
||||
final shadowPaint = Paint()
|
||||
..color = Colors.black26
|
||||
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 8);
|
||||
canvas.drawCircle(center + const Offset(5, 5), radius * 0.9, shadowPaint);
|
||||
|
||||
// Draw face border
|
||||
final borderPaint = Paint()
|
||||
..color = faceColor.darker()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 8;
|
||||
canvas.drawCircle(center, radius * 0.9, borderPaint);
|
||||
|
||||
// Draw eyes
|
||||
_drawEyes(canvas, size);
|
||||
|
||||
// Draw mouth
|
||||
_drawMouth(canvas, size);
|
||||
|
||||
// Draw additional features based on mood
|
||||
if (mood == 'thinking') {
|
||||
_drawThinkingBubble(canvas, size);
|
||||
} else if (mood == 'excited') {
|
||||
_drawExcitedFeatures(canvas, size);
|
||||
}
|
||||
}
|
||||
|
||||
void _drawEyes(Canvas canvas, Size size) {
|
||||
final eyeRadius = size.width * 0.1;
|
||||
final leftEyeCenter = Offset(size.width * 0.35, size.height * 0.4);
|
||||
final rightEyeCenter = Offset(size.width * 0.65, size.height * 0.4);
|
||||
|
||||
final whitePaint = Paint()
|
||||
..color = Colors.white
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
final irisColor = mood == 'sad' ? Colors.blue : Colors.brown;
|
||||
final irisPaint = Paint()
|
||||
..color = irisColor
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
final pupilPaint = Paint()
|
||||
..color = Colors.black
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
// Draw eye whites
|
||||
canvas.drawCircle(leftEyeCenter, eyeRadius, whitePaint);
|
||||
canvas.drawCircle(rightEyeCenter, eyeRadius, whitePaint);
|
||||
|
||||
// Draw iris
|
||||
if (!blinkEyes) {
|
||||
canvas.drawCircle(leftEyeCenter, eyeRadius * 0.7, irisPaint);
|
||||
canvas.drawCircle(rightEyeCenter, eyeRadius * 0.7, irisPaint);
|
||||
|
||||
// Draw pupils
|
||||
canvas.drawCircle(leftEyeCenter, eyeRadius * 0.3, pupilPaint);
|
||||
canvas.drawCircle(rightEyeCenter, eyeRadius * 0.3, pupilPaint);
|
||||
|
||||
// Draw eye highlights
|
||||
final highlightPaint = Paint()
|
||||
..color = Colors.white
|
||||
..style = PaintingStyle.fill;
|
||||
canvas.drawCircle(leftEyeCenter + Offset(eyeRadius * 0.2, -eyeRadius * 0.2), eyeRadius * 0.15, highlightPaint);
|
||||
canvas.drawCircle(rightEyeCenter + Offset(eyeRadius * 0.2, -eyeRadius * 0.2), eyeRadius * 0.15, highlightPaint);
|
||||
} else {
|
||||
// Draw blinking eyes (just a line)
|
||||
final blinkPaint = Paint()
|
||||
..color = Colors.black
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 3;
|
||||
|
||||
canvas.drawLine(
|
||||
leftEyeCenter - Offset(eyeRadius * 0.8, 0),
|
||||
leftEyeCenter + Offset(eyeRadius * 0.8, 0),
|
||||
blinkPaint,
|
||||
);
|
||||
|
||||
canvas.drawLine(
|
||||
rightEyeCenter - Offset(eyeRadius * 0.8, 0),
|
||||
rightEyeCenter + Offset(eyeRadius * 0.8, 0),
|
||||
blinkPaint,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _drawMouth(Canvas canvas, Size size) {
|
||||
final mouthPaint = Paint()
|
||||
..color = Colors.black
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
final innerMouthPaint = Paint()
|
||||
..color = mood == 'sad' ? Colors.blue.shade900 : Colors.red.shade800
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
final mouthWidth = size.width * 0.5;
|
||||
final mouthHeight = size.height * mouthOpenRatio;
|
||||
|
||||
final mouthRect = Rect.fromCenter(
|
||||
center: Offset(size.width / 2, size.height * 0.65),
|
||||
width: mouthWidth,
|
||||
height: mouthHeight,
|
||||
);
|
||||
|
||||
if (mood == 'sad') {
|
||||
// Sad mouth (frown)
|
||||
canvas.drawArc(
|
||||
mouthRect,
|
||||
0,
|
||||
3.14,
|
||||
false,
|
||||
Paint()
|
||||
..color = Colors.black
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 5,
|
||||
);
|
||||
} else {
|
||||
// Draw mouth background
|
||||
canvas.drawOval(mouthRect, innerMouthPaint);
|
||||
|
||||
// Draw teeth
|
||||
if (mouthOpenRatio > 0.3) {
|
||||
final teethPaint = Paint()
|
||||
..color = Colors.white
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
final teethRect = Rect.fromLTWH(
|
||||
mouthRect.left + 5,
|
||||
mouthRect.top + 5,
|
||||
mouthRect.width - 10,
|
||||
mouthRect.height / 3,
|
||||
);
|
||||
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(teethRect, const Radius.circular(3)),
|
||||
teethPaint,
|
||||
);
|
||||
}
|
||||
|
||||
// Draw mouth border
|
||||
canvas.drawOval(
|
||||
mouthRect,
|
||||
Paint()
|
||||
..color = Colors.black
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 3,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _drawThinkingBubble(Canvas canvas, Size size) {
|
||||
final bubblePaint = Paint()
|
||||
..color = Colors.white
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
final bubbleBorderPaint = Paint()
|
||||
..color = Colors.black
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2;
|
||||
|
||||
// Draw thinking bubble
|
||||
final bubbleCenter = Offset(size.width * 0.8, size.height * 0.2);
|
||||
canvas.drawCircle(bubbleCenter, size.width * 0.1, bubblePaint);
|
||||
canvas.drawCircle(bubbleCenter, size.width * 0.1, bubbleBorderPaint);
|
||||
|
||||
// Draw smaller bubbles leading to the character
|
||||
final smallBubble1Center = Offset(size.width * 0.7, size.height * 0.3);
|
||||
canvas.drawCircle(smallBubble1Center, size.width * 0.05, bubblePaint);
|
||||
canvas.drawCircle(smallBubble1Center, size.width * 0.05, bubbleBorderPaint);
|
||||
|
||||
final smallBubble2Center = Offset(size.width * 0.6, size.height * 0.35);
|
||||
canvas.drawCircle(smallBubble2Center, size.width * 0.03, bubblePaint);
|
||||
canvas.drawCircle(smallBubble2Center, size.width * 0.03, bubbleBorderPaint);
|
||||
}
|
||||
|
||||
void _drawExcitedFeatures(Canvas canvas, Size size) {
|
||||
final sparkPaint = Paint()
|
||||
..color = Colors.yellow
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
// Draw sparks around the character
|
||||
for (int i = 0; i < 8; i++) {
|
||||
final angle = i * 3.14 / 4;
|
||||
final sparkCenter = Offset(
|
||||
size.width / 2 + cos(angle) * size.width * 0.6,
|
||||
size.height / 2 + sin(angle) * size.width * 0.6,
|
||||
);
|
||||
|
||||
_drawStar(canvas, sparkCenter, size.width * 0.08, sparkPaint);
|
||||
}
|
||||
}
|
||||
|
||||
void _drawStar(Canvas canvas, Offset center, double radius, Paint paint) {
|
||||
final path = Path();
|
||||
final outerRadius = radius;
|
||||
final innerRadius = radius * 0.4;
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
final angle = i * 3.14 / 5;
|
||||
final r = i % 2 == 0 ? outerRadius : innerRadius;
|
||||
final x = center.dx + cos(angle) * r;
|
||||
final y = center.dy + sin(angle) * r;
|
||||
|
||||
if (i == 0) {
|
||||
path.moveTo(x, y);
|
||||
} else {
|
||||
path.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
path.close();
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CharacterPainter oldDelegate) {
|
||||
return oldDelegate.mood != mood ||
|
||||
oldDelegate.mouthOpenRatio != mouthOpenRatio ||
|
||||
oldDelegate.blinkEyes != blinkEyes;
|
||||
}
|
||||
}
|
||||
|
||||
extension ColorExtension on Color {
|
||||
Color darker() {
|
||||
return Color.fromARGB(
|
||||
alpha,
|
||||
(red * 0.7).round(),
|
||||
(green * 0.7).round(),
|
||||
(blue * 0.7).round(),
|
||||
);
|
||||
}
|
||||
}
|
||||
89
lib/widgets/speech_wave.dart
Normal file
@ -0,0 +1,89 @@
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../utils/app_theme.dart';
|
||||
|
||||
class SpeechWave extends StatefulWidget {
|
||||
const SpeechWave({super.key});
|
||||
|
||||
@override
|
||||
State<SpeechWave> createState() => _SpeechWaveState();
|
||||
}
|
||||
|
||||
class _SpeechWaveState extends State<SpeechWave> with TickerProviderStateMixin {
|
||||
late List<AnimationController> _controllers;
|
||||
late List<Animation<double>> _animations;
|
||||
final int _barCount = 5;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Initialize controllers and animations
|
||||
_controllers = List.generate(
|
||||
_barCount,
|
||||
(index) => AnimationController(
|
||||
vsync: this,
|
||||
duration: Duration(milliseconds: 300 + Random().nextInt(700)),
|
||||
),
|
||||
);
|
||||
|
||||
_animations = _controllers.map((controller) {
|
||||
return Tween<double>(begin: 0.1, end: 1.0).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
// Start animations with different delays
|
||||
for (int i = 0; i < _controllers.length; i++) {
|
||||
Future.delayed(Duration(milliseconds: i * 50), () {
|
||||
if (mounted) {
|
||||
_controllers[i].repeat(reverse: true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (var controller in _controllers) {
|
||||
controller.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 120,
|
||||
height: 60,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(_barCount, (index) {
|
||||
return AnimatedBuilder(
|
||||
animation: _animations[index],
|
||||
builder: (context, child) {
|
||||
return _buildBar(_animations[index].value);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBar(double height) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: Container(
|
||||
width: 6,
|
||||
height: 40 * height,
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryColor,
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
826
pubspec.lock
Normal file
@ -0,0 +1,826 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
_flutterfire_internals:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _flutterfire_internals
|
||||
sha256: "37a42d06068e2fe3deddb2da079a8c4d105f241225ba27b7122b37e9865fd8f7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.35"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.6.1"
|
||||
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: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.0"
|
||||
audioplayers:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: audioplayers
|
||||
sha256: c05c6147124cd63e725e861335a8b4d57300b80e6e92cea7c145c739223bbaef
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.2.1"
|
||||
audioplayers_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_android
|
||||
sha256: b00e1a0e11365d88576320ec2d8c192bc21f1afb6c0e5995d1c57ae63156acb5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.3"
|
||||
audioplayers_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_darwin
|
||||
sha256: "3034e99a6df8d101da0f5082dcca0a2a99db62ab1d4ddb3277bed3f6f81afe08"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.2"
|
||||
audioplayers_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_linux
|
||||
sha256: "60787e73fefc4d2e0b9c02c69885402177e818e4e27ef087074cf27c02246c9e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
audioplayers_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_platform_interface
|
||||
sha256: "365c547f1bb9e77d94dd1687903a668d8f7ac3409e48e6e6a3668a1ac2982adb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
audioplayers_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_web
|
||||
sha256: "22cd0173e54d92bd9b2c80b1204eb1eb159ece87475ab58c9788a70ec43c2a62"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
audioplayers_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_windows
|
||||
sha256: "9536812c9103563644ada2ef45ae523806b0745f7a78e89d1b5fb1951de90e1a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
cloud_firestore:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cloud_firestore
|
||||
sha256: a0f161b92610e078b4962d7e6ebeb66dc9cce0ada3514aeee442f68165d78185
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.17.5"
|
||||
cloud_firestore_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cloud_firestore_platform_interface
|
||||
sha256: "6a55b319f8d33c307396b9104512e8130a61904528ab7bd8b5402678fca54b81"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.5"
|
||||
cloud_firestore_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cloud_firestore_web
|
||||
sha256: "89dfa1304d3da48b3039abbb2865e3d30896ef858e569a16804a99f4362283a9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.12.5"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
connectivity_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: connectivity_plus
|
||||
sha256: "77a180d6938f78ca7d2382d2240eb626c0f6a735d0bfdce227d8ffb80f95c48b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
connectivity_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: connectivity_plus_platform_interface
|
||||
sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.4"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.6"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cupertino_icons
|
||||
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.8"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dbus
|
||||
sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.11"
|
||||
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: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
firebase_auth:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_auth
|
||||
sha256: "279b2773ff61afd9763202cb5582e2b995ee57419d826b9af6517302a59b672f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.16.0"
|
||||
firebase_auth_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_auth_platform_interface
|
||||
sha256: a0270e1db3b2098a14cb2a2342b3cd2e7e458e0c391b1f64f6f78b14296ec093
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.3.0"
|
||||
firebase_auth_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_auth_web
|
||||
sha256: c7b1379ccef7abf4b6816eede67a868c44142198e42350f51c01d8fc03f95a7d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.8.13"
|
||||
firebase_core:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_core
|
||||
sha256: "26de145bb9688a90962faec6f838247377b0b0d32cc0abecd9a4e43525fc856c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.32.0"
|
||||
firebase_core_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_platform_interface
|
||||
sha256: d7253d255ff10f85cfd2adaba9ac17bae878fa3ba577462451163bd9f1d1f0bf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.4.0"
|
||||
firebase_core_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_web
|
||||
sha256: ddd72baa6f727e5b23f32d9af23d7d453d67946f380bd9c21daf474ee0f7326e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.23.0"
|
||||
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: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
flutter_secure_storage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_secure_storage
|
||||
sha256: "22dbf16f23a4bcf9d35e51be1c84ad5bb6f627750565edd70dab70f3ff5fff8f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.0"
|
||||
flutter_secure_storage_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_linux
|
||||
sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.3"
|
||||
flutter_secure_storage_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_macos
|
||||
sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
flutter_secure_storage_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_platform_interface
|
||||
sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
flutter_secure_storage_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_web
|
||||
sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
flutter_secure_storage_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_windows
|
||||
sha256: "38f9501c7cb6f38961ef0e1eacacee2b2d4715c63cc83fe56449c4d3d0b47255"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
flutter_svg:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_svg
|
||||
sha256: cd57f7969b4679317c17af6fd16ee233c1e60a82ed209d8a475c54fd6fd6f845
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_tts:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_tts
|
||||
sha256: bdf2fc4483e74450dc9fc6fe6a9b6a5663e108d4d0dad3324a22c8e26bf48af4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.3"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
in_app_purchase:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: in_app_purchase
|
||||
sha256: "5cddd7f463f3bddb1d37a72b95066e840d5822d66291331d7f8f05ce32c24b6c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.3"
|
||||
in_app_purchase_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: in_app_purchase_android
|
||||
sha256: fd76e5612da6facadcfe8a3477da092908227260a9f6ec7db9a66dd989c69b02
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.0+2"
|
||||
in_app_purchase_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: in_app_purchase_platform_interface
|
||||
sha256: "1d353d38251da5b9fea6635c0ebfc6bb17a2d28d0e86ea5e083bf64244f1fb4c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
in_app_purchase_storekit:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: in_app_purchase_storekit
|
||||
sha256: ceddd5a70d268f762d29993ed470054bc2baf8793e41800bc82cde05110260d0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.2"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.7"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.9.0"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.9"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.9"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
lottie:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: lottie
|
||||
sha256: a93542cc2d60a7057255405f62252533f8e8956e7e06754955669fd32fb4b216
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.17"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.11.1"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nested
|
||||
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
nm:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nm
|
||||
sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_parsing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_parsing
|
||||
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
path_provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.17"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
pedantic:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pedantic
|
||||
sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.1"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.6"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: provider
|
||||
sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.5"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.3"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.10"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.4"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_linux
|
||||
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
shared_preferences_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_platform_interface
|
||||
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
shared_preferences_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_web
|
||||
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.3"
|
||||
shared_preferences_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_windows
|
||||
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.1"
|
||||
speech_to_text:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: speech_to_text
|
||||
sha256: "6cf8f284997490ebef1d68f8707bef57dcf083f43c0f915cc285428520bfe6be"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
speech_to_text_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: speech_to_text_platform_interface
|
||||
sha256: a1935847704e41ee468aad83181ddd2423d0833abe55d769c59afca07adb5114
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
sprintf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sprintf
|
||||
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: synchronized
|
||||
sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.4"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: uuid
|
||||
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.1"
|
||||
vector_graphics:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics
|
||||
sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.19"
|
||||
vector_graphics_codec:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics_codec
|
||||
sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.13"
|
||||
vector_graphics_compiler:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics_compiler
|
||||
sha256: "557a315b7d2a6dbb0aaaff84d857967ce6bdc96a63dc6ee2a57ce5a6ee5d3331"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.17"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.0.0"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.5.0"
|
||||
sdks:
|
||||
dart: ">=3.7.0 <4.0.0"
|
||||
flutter: ">=3.27.0"
|
||||
54
pubspec.yaml
Normal file
@ -0,0 +1,54 @@
|
||||
name: road_trip_kids
|
||||
description: A speech-controlled game app for kids during road trips
|
||||
publish_to: 'none'
|
||||
version: 1.0.0+1
|
||||
|
||||
environment:
|
||||
sdk: ">=2.19.0 <4.0.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
cupertino_icons: ^1.0.5 # Speech recognition
|
||||
speech_to_text: ^7.0.0
|
||||
# Text to speech
|
||||
flutter_tts: ^4.2.2
|
||||
# State management
|
||||
provider: ^6.0.5
|
||||
# Firebase dependencies
|
||||
firebase_core: ^2.14.0
|
||||
firebase_auth: ^4.6.3
|
||||
cloud_firestore: ^4.8.1
|
||||
# For animations
|
||||
lottie: ^2.6.0
|
||||
# For HTTP requests to OpenAI API
|
||||
http: ^1.1.0
|
||||
# For secure storage (parental controls)
|
||||
flutter_secure_storage: ^8.0.0
|
||||
# For local storage
|
||||
shared_preferences: ^2.2.0
|
||||
# Audio files
|
||||
audioplayers: ^5.1.0
|
||||
# SVG rendering
|
||||
flutter_svg: ^2.0.7
|
||||
# Connectivity checking
|
||||
connectivity_plus: ^4.0.2
|
||||
# For in-app purchases (subscription)
|
||||
in_app_purchase: ^3.1.7
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^2.0.2
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
|
||||
assets:
|
||||
- assets/images/
|
||||
- assets/sounds/
|
||||
# Using system fonts instead to resolve build issue
|
||||
# fonts:
|
||||
# - family: Bubblegum
|
||||
# fonts:
|
||||
# - asset: assets/fonts/BubblegumSans-Regular.ttf
|
||||
21
test/widget_test.dart
Normal file
@ -0,0 +1,21 @@
|
||||
// This is a basic Flutter widget test.
|
||||
//
|
||||
// To perform an interaction with a widget in your test, use the WidgetTester
|
||||
// utility in the flutter_test package. For example, you can send tap and scroll
|
||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||
// tree, read text, and verify that the values of widget properties are correct.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:road_trip_kids/main.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('App launches smoke test', (WidgetTester tester) async {
|
||||
// Build our app and trigger a frame.
|
||||
await tester.pumpWidget(const RoadTripKidsApp());
|
||||
|
||||
// Verify that our app starts with the splash screen or home screen
|
||||
expect(find.byType(MaterialApp), findsOneWidget);
|
||||
});
|
||||
}
|
||||