Adds `ai_recipe_generation` sample (#2242)

Adding the demo app from my I/O talk. Because AI.

## Pre-launch Checklist

- [x] I read the [Flutter Style Guide] _recently_, and have followed its
advice.
- [x] I signed the [CLA].
- [x] I read the [Contributors Guide].
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] All existing and new tests are passing.

---------

Co-authored-by: Brett Morgan <brett.morgan@gmail.com>
recipe_readme
Eric Windmill 2 months ago committed by GitHub
parent 8575261d37
commit be52906894
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
.env
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

@ -0,0 +1,45 @@
# 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: "2e9cb0aa71a386a91f73f7088d115c0d96654829"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829
base_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829
- platform: android
create_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829
base_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829
- platform: ios
create_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829
base_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829
- platform: linux
create_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829
base_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829
- platform: macos
create_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829
base_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829
- platform: web
create_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829
base_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829
- platform: windows
create_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829
base_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829
# 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'

@ -0,0 +1,15 @@
This is a demo app created for the Google I/O talk "Gemini API and Flutter: Practical, AI-driven apps with Google AI tools"
## Running the app
Before running the app:
* Get an API key from [ai.google.dev](ai.google.dev).
* Create a Firebase project, and install the Flutter Firebase CLI, and configure the platforms that you want to run this app on. You can find instructions by following [steps 2-4 of the Get to know Firebase for Flutter](https://firebase.google.com/codelabs/firebase-get-to-know-flutter?hl=en#2) codelab.
Then, pass the API key in with dart define when running the app:
```bash
flutter run --dart-define=API_KEY=your_api_key
```

@ -0,0 +1 @@
include: package:analysis_defaults/flutter.yaml

@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
**/*.keystore
**/*.jks

@ -0,0 +1,67 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
android {
namespace "com.example.gemini_io_talk"
compileSdkVersion flutter.compileSdkVersion
ndkVersion flutter.ndkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.gemini_io_talk"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion flutter.minSdkVersion
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
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.debug
}
}
}
flutter {
source '../..'
}
dependencies {}

@ -0,0 +1,29 @@
{
"project_info": {
"project_number": "44885228795",
"project_id": "gemini-cat-chef",
"storage_bucket": "gemini-cat-chef.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:44885228795:android:d1ed69a5c617a8b98f845e",
"android_client_info": {
"package_name": "com.example.gemini_io_talk"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyANxBBzc4s-Yuol0xqs-mEtXe-pNcut3OU"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}

@ -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>

@ -0,0 +1,33 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="gemini_io_talk"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- 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>
</manifest>

@ -0,0 +1,6 @@
package com.example.gemini_io_talk
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
}

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@ -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>

@ -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>

@ -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>

@ -0,0 +1,30 @@
buildscript {
ext.kotlin_version = '1.7.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx4G
android.useAndroidX=true
android.enableJetifier=true

@ -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-7.5-all.zip

@ -0,0 +1,29 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}
settings.ext.flutterSdkPath = flutterSdkPath()
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
plugins {
id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.3.0" apply false
}
include ":app"

@ -0,0 +1,43 @@
<svg width="1148" height="1148" viewBox="0 0 1148 1148" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_102_68" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="1148" height="1148">
<circle cx="574" cy="574" r="574" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_102_68)">
<circle cx="574" cy="574" r="566.5" fill="#FFE5ED" stroke="black" stroke-width="15"/>
<path d="M981.119 769.349C981.119 853.471 936.262 930.167 862.677 986.078C789.087 1041.99 687.091 1076.8 574.12 1076.8C461.148 1076.8 359.152 1041.99 285.562 986.078C211.977 930.167 167.12 853.471 167.12 769.349C167.12 685.227 211.977 608.531 285.562 552.619C359.152 496.704 461.148 461.902 574.12 461.902C687.091 461.902 789.087 496.704 862.677 552.619C936.262 608.531 981.119 685.227 981.119 769.349Z" fill="white" stroke="white" stroke-width="15"/>
<path d="M572.309 1084.3C682.721 1084.3 788.611 1051.11 866.685 992.05C944.758 932.986 988.619 852.878 988.619 769.349C988.619 685.82 944.758 605.712 866.685 546.648C788.611 487.584 682.721 454.402 572.309 454.402V769.349L572.309 1084.3Z" fill="#E8947F"/>
<rect x="268.38" y="695.137" width="70.6828" height="26.9205" rx="13.4602" transform="rotate(20 268.38 695.137)" fill="#42241F"/>
<rect x="251.932" y="759.722" width="70.6828" height="26.9205" rx="13.4602" transform="rotate(-0.999999 251.932 759.722)" fill="#42241F"/>
<rect width="70.6828" height="26.9205" rx="13.4602" transform="matrix(-0.939693 0.34202 0.34202 0.939693 870.415 696.947)" fill="#42241F"/>
<rect x="258.767" y="828.934" width="70.6828" height="26.9205" rx="13.4602" transform="rotate(-18.5 258.767 828.934)" fill="#42241F"/>
<rect width="70.6828" height="26.9205" rx="13.4602" transform="matrix(-0.948324 -0.317305 -0.317305 0.948324 879.567 827.978)" fill="#42241F"/>
<rect width="70.6828" height="26.9205" rx="13.4602" transform="matrix(-0.999848 -0.0174524 -0.0174524 0.999848 885.997 761.532)" fill="#42241F"/>
<circle cx="430.221" cy="697.852" r="20.8155" fill="#42241F"/>
<circle cx="705.347" cy="697.852" r="20.8155" fill="#42241F"/>
<path d="M544.055 734.488C541.641 730.453 544.596 725.337 549.296 725.409L601.34 726.212C605.918 726.283 608.733 731.247 606.444 735.212L581.118 779.078C578.829 783.043 573.122 783.087 570.772 779.158L544.055 734.488Z" fill="#42241F"/>
<path d="M452.847 848.078C487.841 882.986 561.449 922.627 575.93 801.93" stroke="#42241F" stroke-width="12"/>
<circle cx="451.037" cy="847.181" r="10.8602" fill="#42241F"/>
<circle cx="575.93" cy="800.12" r="10.8602" fill="black"/>
<path d="M699.012 848.078C664.018 882.986 590.41 922.627 575.929 801.93" stroke="#42241F" stroke-width="12"/>
<circle cx="10.8602" cy="10.8602" r="10.8602" transform="matrix(-1 0 0 1 711.683 836.32)" fill="#42241F"/>
<circle cx="10.8602" cy="10.8602" r="10.8602" transform="matrix(-1 0 0 1 586.79 789.259)" fill="#42241F"/>
<path d="M506.972 467.616C506.972 515.6 491.979 554.498 473.486 554.498C454.992 554.498 440 515.6 440 467.616C473.486 449.516 454.992 458.566 473.486 458.566C491.979 458.566 483.441 449.516 506.972 467.616Z" fill="#B36652"/>
<path d="M604.714 467.616C604.714 536.592 588.101 592.508 567.608 592.508C547.115 592.508 530.502 536.592 530.502 467.616C555.843 422.365 567.608 476.666 567.608 454.946C588.101 454.946 582.993 442.276 604.714 467.616Z" fill="#B36652"/>
<path d="M695.216 467.616C695.216 515.6 680.224 554.498 661.73 554.498C643.236 554.498 628.244 515.6 628.244 467.616C661.73 467.616 643.236 467.617 661.73 467.617C680.224 467.617 671.685 467.616 695.216 467.616Z" fill="#B36652"/>
<path d="M981.119 769.349C981.119 853.471 936.262 930.167 862.677 986.078C789.087 1041.99 687.091 1076.8 574.12 1076.8C461.148 1076.8 359.152 1041.99 285.562 986.078C211.977 930.167 167.12 853.471 167.12 769.349C167.12 685.227 211.977 608.531 285.562 552.619C359.152 496.704 461.148 461.902 574.12 461.902C687.091 461.902 789.087 496.704 862.677 552.619C936.262 608.531 981.119 685.227 981.119 769.349Z" stroke="#42241F" stroke-width="15"/>
<path d="M739.532 467.783C836.158 368.524 870.135 460.513 874.814 530.618C875.447 540.108 866.273 546.899 857.756 542.666C853.048 540.326 848.797 537.784 844.862 535.197C832.618 527.149 729.311 478.282 739.532 467.783Z" fill="white" stroke="#B36652" stroke-width="10"/>
<path d="M868.541 576.469C888.69 482.475 867.777 321.948 712 481.307" stroke="#42241F" stroke-width="12"/>
<mask id="path-26-outside-1_102_68" maskUnits="userSpaceOnUse" x="206.046" y="-15.9623" width="604.267" height="500.807" fill="black">
<rect fill="white" x="206.046" y="-15.9623" width="604.267" height="500.807"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M600.447 66.8672C576.847 34.8128 537.281 15.0525 493.846 17.915C450.41 20.7774 413.78 45.5591 394.591 80.4332C379.542 75.3158 363.184 73.011 346.234 74.128C279.306 78.5386 228.511 134.634 232.781 199.421C236.696 258.832 285.556 304.921 345.25 308.76L355.312 461.443L689.387 439.427L679.325 286.744C737.998 275.105 780.388 223.002 776.473 163.592C772.203 98.8046 714.486 49.8599 647.558 54.2706C630.609 55.3875 614.694 59.8191 600.447 66.8672Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M600.447 66.8672C576.847 34.8128 537.281 15.0525 493.846 17.915C450.41 20.7774 413.78 45.5591 394.591 80.4332C379.542 75.3158 363.184 73.011 346.234 74.128C279.306 78.5386 228.511 134.634 232.781 199.421C236.696 258.832 285.556 304.921 345.25 308.76L355.312 461.443L689.387 439.427L679.325 286.744C737.998 275.105 780.388 223.002 776.473 163.592C772.203 98.8046 714.486 49.8599 647.558 54.2706C630.609 55.3875 614.694 59.8191 600.447 66.8672Z" fill="white"/>
<path d="M600.447 66.8672L588.367 75.7604L595.824 85.8888L607.098 80.312L600.447 66.8672ZM394.591 80.4332L389.762 94.6346L401.67 98.6837L407.733 87.6643L394.591 80.4332ZM345.25 308.76L360.217 307.773L359.351 294.636L346.212 293.791L345.25 308.76ZM355.312 461.443L340.344 462.43L341.33 477.397L356.298 476.411L355.312 461.443ZM689.387 439.427L690.374 454.395L705.341 453.409L704.355 438.441L689.387 439.427ZM679.325 286.744L676.406 272.031L663.492 274.592L664.358 287.73L679.325 286.744ZM612.526 57.9739C585.95 21.8772 541.498 -0.25787 492.859 2.94743L494.832 32.8825C533.065 30.3629 567.744 47.7484 588.367 75.7604L612.526 57.9739ZM492.859 2.94743C444.221 6.15273 403.058 33.93 381.449 73.202L407.733 87.6643C424.502 57.1881 456.6 35.4021 494.832 32.8825L492.859 2.94743ZM399.42 66.2318C382.527 60.4873 364.193 57.9119 345.248 59.1604L347.221 89.0955C362.174 88.1101 376.557 90.1444 389.762 94.6346L399.42 66.2318ZM345.248 59.1604C270.516 64.0854 212.969 126.899 217.813 200.408L247.748 198.435C244.054 142.369 288.096 92.9919 347.221 89.0955L345.248 59.1604ZM217.813 200.408C222.255 267.803 277.555 319.437 344.287 323.729L346.212 293.791C293.558 290.404 251.137 249.861 247.748 198.435L217.813 200.408ZM330.282 309.746L340.344 462.43L370.279 460.457L360.217 307.773L330.282 309.746ZM356.298 476.411L690.374 454.395L688.401 424.46L354.325 446.476L356.298 476.411ZM704.355 438.441L694.293 285.757L664.358 287.73L674.42 440.414L704.355 438.441ZM761.505 164.578C764.894 216.004 728.161 261.764 676.406 272.031L682.244 301.457C747.835 288.445 795.882 230.001 791.44 162.605L761.505 164.578ZM648.544 69.2381C707.668 65.3417 757.81 108.512 761.505 164.578L791.44 162.605C786.596 89.0969 721.303 34.3781 646.571 39.303L648.544 69.2381ZM607.098 80.312C619.599 74.1278 633.591 70.2235 648.544 69.2381L646.571 39.303C627.626 40.5515 609.789 45.5105 593.796 53.4223L607.098 80.312Z" fill="black" mask="url(#path-26-outside-1_102_68)"/>
<path d="M353.661 436.403L346.178 436.897L346.671 444.38L348.734 475.685C349.388 485.615 358.094 492.821 367.822 492.179L681.018 471.54C690.747 470.898 698.431 462.613 697.777 452.682L695.714 421.378L695.221 413.894L687.737 414.388L353.661 436.403Z" fill="#A2E3F6" stroke="black" stroke-width="15"/>
<path d="M426.064 146.542C413.396 147.376 403.695 158.263 404.533 170.976L409.231 242.257C410.069 254.97 421.114 264.489 433.782 263.654C446.45 262.819 456.151 251.933 455.313 239.22L450.616 167.939C449.778 155.226 438.732 145.707 426.064 146.542Z" fill="white" stroke="black" stroke-width="15"/>
<path d="M502.188 144.496C489.435 145.337 478.977 155.916 479.838 168.985L484.535 240.266C485.396 253.334 497.153 262.449 509.905 261.609C522.658 260.769 533.117 250.189 532.255 237.121L527.558 165.84C526.697 152.771 514.94 143.656 502.188 144.496Z" fill="white" stroke="black" stroke-width="15"/>
<path d="M573.064 142.542C560.396 143.376 550.695 154.263 551.533 166.976L556.231 238.257C557.069 250.97 568.114 260.489 580.782 259.654C593.45 258.819 603.151 247.933 602.313 235.22L597.616 163.939C596.778 151.226 585.732 141.707 573.064 142.542Z" fill="white" stroke="black" stroke-width="15"/>
<path d="M404.273 485.38C313.484 380.755 274.3 470.649 265.618 540.372C264.442 549.81 273.213 557.115 281.958 553.376C286.792 551.309 291.182 549.015 295.259 546.657C307.942 539.323 413.876 496.446 404.273 485.38Z" fill="#E4BFB5" stroke="#B36652" stroke-width="12"/>
<path d="M262.295 582.856C247.557 487.863 277.621 328.795 424.025 496.806" stroke="#42241F" stroke-width="12"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

@ -0,0 +1,11 @@
<svg width="1148" height="1148" viewBox="0 0 1148 1148" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_106_3" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="1148" height="1148">
<circle cx="574" cy="574" r="574" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_106_3)">
<circle cx="574" cy="574" r="566.5" fill="#E4BFB5" stroke="black" stroke-width="15"/>
<path d="M996 986C996 1277.61 807.064 1514 574 1514C340.936 1514 152 1277.61 152 986C152 694.394 340.936 574 574 574C807.064 574 996 694.394 996 986Z" fill="#DE7A60"/>
<ellipse cx="574" cy="367" rx="264" ry="276" fill="#DE7A60"/>
<circle cx="574" cy="574" r="566.5" stroke="black" stroke-width="15"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 705 B

@ -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

@ -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>

@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

@ -0,0 +1,44 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '12.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
use_modular_headers!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end

@ -0,0 +1,728 @@
// !$*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 */; };
6CEAC1BB3402D2FB3D949175 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3AEDF9CC1F2478538C36F4EC /* GoogleService-Info.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 */; };
D176046199A01D7761A9B663 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 55AAE8E90222A83A2A3C5725 /* Pods_RunnerTests.framework */; };
EC8FDDE32B650F9294E84E49 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 560968E9DE345774409B17C1 /* Pods_Runner.framework */; };
/* 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>"; };
26885269168D3B8FD1AC5E99 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; 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; };
3AEDF9CC1F2478538C36F4EC /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
55AAE8E90222A83A2A3C5725 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
560968E9DE345774409B17C1 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
6E452F04B76B6311A2D269D2 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; 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>"; };
8AC0061116C886C86A9B5490 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
8C41D0F2AC811E3A0FF6FC41 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.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>"; };
CE028EB35B4B427E70F8476C /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
D397ABCFD4CECC2D1C87B0F5 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
3C97DB56F9108CED1F143C54 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
D176046199A01D7761A9B663 /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
EC8FDDE32B650F9294E84E49 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
4DC0F3FBD4704312B8DDA602 /* Pods */ = {
isa = PBXGroup;
children = (
D397ABCFD4CECC2D1C87B0F5 /* Pods-Runner.debug.xcconfig */,
8AC0061116C886C86A9B5490 /* Pods-Runner.release.xcconfig */,
8C41D0F2AC811E3A0FF6FC41 /* Pods-Runner.profile.xcconfig */,
26885269168D3B8FD1AC5E99 /* Pods-RunnerTests.debug.xcconfig */,
6E452F04B76B6311A2D269D2 /* Pods-RunnerTests.release.xcconfig */,
CE028EB35B4B427E70F8476C /* Pods-RunnerTests.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
774D05E8B814CE61D1EBF4C8 /* Frameworks */ = {
isa = PBXGroup;
children = (
560968E9DE345774409B17C1 /* Pods_Runner.framework */,
55AAE8E90222A83A2A3C5725 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
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 */,
4DC0F3FBD4704312B8DDA602 /* Pods */,
774D05E8B814CE61D1EBF4C8 /* Frameworks */,
3AEDF9CC1F2478538C36F4EC /* GoogleService-Info.plist */,
);
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 = (
825D4F9DB9CE5CD128B67AF7 /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
3C97DB56F9108CED1F143C54 /* Frameworks */,
);
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 = (
9B274ACE53869D8B50572937 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
04F3356025EB4C21B4C3E754 /* [CP] Embed Pods Frameworks */,
);
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 */,
6CEAC1BB3402D2FB3D949175 /* GoogleService-Info.plist in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
04F3356025EB4C21B4C3E754 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
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";
};
825D4F9DB9CE5CD128B67AF7 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
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";
};
9B274ACE53869D8B50572937 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* 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;
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;
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)";
DEVELOPMENT_TEAM = 2UUT9AMTS2;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.geminiIoTalk;
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;
baseConfigurationReference = 26885269168D3B8FD1AC5E99 /* Pods-RunnerTests.debug.xcconfig */;
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.geminiIoTalk.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;
baseConfigurationReference = 6E452F04B76B6311A2D269D2 /* Pods-RunnerTests.release.xcconfig */;
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.geminiIoTalk.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;
baseConfigurationReference = CE028EB35B4B427E70F8476C /* Pods-RunnerTests.profile.xcconfig */;
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.geminiIoTalk.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;
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;
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;
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;
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)";
DEVELOPMENT_TEAM = 2UUT9AMTS2;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.geminiIoTalk;
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)";
DEVELOPMENT_TEAM = 2UUT9AMTS2;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.geminiIoTalk;
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 */;
}

@ -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>

@ -0,0 +1,98 @@
<?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"
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"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
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>

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.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>

@ -0,0 +1,13 @@
import UIKit
import Flutter
@UIApplicationMain
@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)
}
}

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

@ -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.

@ -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>

@ -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>

@ -0,0 +1,30 @@
<?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>API_KEY</key>
<string>AIzaSyBNEtAHdjm3oFV6JZ6nbx5t6Pfyj4w4hbo</string>
<key>GCM_SENDER_ID</key>
<string>44885228795</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.example.geminiIoTalk</string>
<key>PROJECT_ID</key>
<string>gemini-cat-chef</string>
<key>STORAGE_BUCKET</key>
<string>gemini-cat-chef.appspot.com</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:44885228795:ios:8ef52eb95f012a148f845e</string>
</dict>
</plist>

@ -0,0 +1,59 @@
<?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>Gemini Io Talk</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>gemini_io_talk</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>NSCameraUsageDescription</key>
<string>Used to demonstrate image picker plugin</string>
<key>NSMicrophoneUsageDescription</key>
<string>Used to capture audio for image picker plugin</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Used to demonstrate image picker plugin</string>
<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>NSCameraUsageDescription</key>
<string>your usage description here</string>
<key>NSMicrophoneUsageDescription</key>
<string>your usage description here</string>
</dict>
</plist>

@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

@ -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.
}
}

@ -0,0 +1,7 @@
{
"file_generated_by": "FlutterFire CLI",
"purpose": "FirebaseAppID & ProjectID for this Firebase app in this directory",
"GOOGLE_APP_ID": "1:44885228795:ios:8ef52eb95f012a148f845e",
"FIREBASE_PROJECT_ID": "gemini-cat-chef",
"GCM_SENDER_ID": "44885228795"
}

@ -0,0 +1,160 @@
import 'package:ai_recipe_generation/util/extensions.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'features/prompt/widgets/app_info_dialog_widget.dart';
import 'theme.dart';
import 'widgets/appbar_shape_border.dart';
class AnimatedAppBar extends StatelessWidget {
const AnimatedAppBar({
super.key,
required this.scrollController,
required this.textStyle,
required this.tabController,
});
final ScrollController scrollController;
final double collapsedHeight = 100;
final double expandedHeight = 300;
final double avatarSize = 50;
final TextStyle textStyle;
final TabController tabController;
String get headerText {
return switch (tabController.index) {
0 => 'Create a recipe',
1 => 'Saved recipes',
2 => 'Settings',
_ => 'Uh oh!',
};
}
String get helperText {
return switch (tabController.index) {
0 =>
"Tell me what ingredients you have and what you're feelin', and I'll create a recipe for you!",
1 => "These are all my saved recipes created by Chef Noodle.",
2 => 'Settings',
_ => 'Uh oh!',
};
}
@override
Widget build(BuildContext context) {
return SliverLayoutBuilder(
builder: (context, constraints) {
return SliverAppBar(
automaticallyImplyLeading: false,
pinned: true,
forceElevated: true,
elevation: 2,
shadowColor: Colors.black,
expandedHeight: expandedHeight,
collapsedHeight: collapsedHeight,
backgroundColor: Theme.of(context).primaryColor,
shape: const AppBarShapeBorder(50),
title: Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: avatarSize,
height: avatarSize,
child: SvgPicture.asset(
'assets/chef_cat.svg',
semanticsLabel: 'Chef cat icon',
),
),
const SizedBox(
width: MarketplaceTheme.spacing1,
),
if (scrollController.positions.isNotEmpty &&
scrollController.offset < 200)
Text(
"Meowdy! Let's get cooking!",
style: MarketplaceTheme.heading3,
),
if (scrollController.positions.isNotEmpty &&
scrollController.offset > 200)
Text(
headerText,
style: MarketplaceTheme.heading3,
),
const Spacer(),
if (scrollController.positions.isNotEmpty &&
scrollController.offset > 200)
IconButton(
onPressed: () => showDialog<Null>(
context: context,
builder: (context) => const AppInfoDialog(),
),
icon: const Icon(
Symbols.info,
color: Colors.black12,
),
),
],
),
],
),
flexibleSpace: FlexibleSpaceBar(
background: Padding(
padding: const EdgeInsets.all(MarketplaceTheme.spacing4),
child: SizedBox(
width: MediaQuery.of(context).size.width,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Expanded(
child: Text(
helperText,
style: constraints.isMobile
? MarketplaceTheme.subheading2
: MarketplaceTheme.subheading1,
),
),
IconButton(
onPressed: () {
showDialog<Null>(
context: context,
builder: (context) => const AppInfoDialog(),
);
},
icon: const Icon(
Symbols.info,
color: Colors.black12,
),
),
],
),
),
),
),
bottom: PreferredSize(
preferredSize: const Size(double.infinity, 0),
child: Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.only(
left: constraints.isMobile
? MarketplaceTheme.spacing2
: MarketplaceTheme.spacing1,
),
child: AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 0),
style: textStyle,
child: Text(
headerText,
),
),
),
),
),
);
},
);
}
}

@ -0,0 +1,66 @@
import 'package:image_picker/image_picker.dart';
import '../../util/filter_chip_enums.dart';
class PromptData {
PromptData({
required this.images,
required this.textInput,
Set<BasicIngredientsFilter>? basicIngredients,
Set<CuisineFilter>? cuisines,
Set<DietaryRestrictionsFilter>? dietaryRestrictions,
List<String>? additionalTextInputs,
}) : additionalTextInputs = additionalTextInputs ?? [],
selectedBasicIngredients = basicIngredients ?? {},
selectedCuisines = cuisines ?? {},
selectedDietaryRestrictions = dietaryRestrictions ?? {};
PromptData.empty()
: images = [],
additionalTextInputs = [],
selectedBasicIngredients = {},
selectedCuisines = {},
selectedDietaryRestrictions = {},
textInput = '';
String get cuisines {
return selectedCuisines.map((catFilter) => catFilter.name).join(",");
}
String get ingredients {
return selectedBasicIngredients
.map((ingredient) => ingredient.name)
.join(", ");
}
String get dietaryRestrictions {
return selectedDietaryRestrictions
.map((restriction) => restriction.name)
.join(", ");
}
List<XFile> images;
String textInput;
List<String> additionalTextInputs;
Set<BasicIngredientsFilter> selectedBasicIngredients;
Set<CuisineFilter> selectedCuisines;
Set<DietaryRestrictionsFilter> selectedDietaryRestrictions;
PromptData copyWith({
List<XFile>? images,
String? textInput,
List<String>? additionalTextInputs,
Set<BasicIngredientsFilter>? basicIngredients,
Set<CuisineFilter>? cuisineSelections,
Set<DietaryRestrictionsFilter>? dietaryRestrictions,
}) {
return PromptData(
images: images ?? this.images,
textInput: textInput ?? this.textInput,
additionalTextInputs: additionalTextInputs ?? this.additionalTextInputs,
basicIngredients: basicIngredients ?? selectedBasicIngredients,
cuisines: cuisineSelections ?? selectedCuisines,
dietaryRestrictions: dietaryRestrictions ?? selectedDietaryRestrictions,
);
}
}

@ -0,0 +1,388 @@
import 'package:ai_recipe_generation/features/prompt/prompt_view_model.dart';
import 'package:ai_recipe_generation/util/extensions.dart';
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:provider/provider.dart';
import '../../theme.dart';
import '../../util/filter_chip_enums.dart';
import '../../widgets/filter_chip_selection_input.dart';
import '../../widgets/highlight_border_on_hover_widget.dart';
import '../../widgets/marketplace_button_widget.dart';
import '../recipes/widgets/recipe_fullscreen_dialog.dart';
import 'widgets/full_prompt_dialog_widget.dart';
import 'widgets/image_input_widget.dart';
const double kAvatarSize = 50;
const double collapsedHeight = 100;
const double expandedHeight = 300;
const double elementPadding = MarketplaceTheme.spacing7;
class PromptScreen extends StatelessWidget {
const PromptScreen({super.key, required this.canScroll});
final bool canScroll;
@override
Widget build(BuildContext context) {
final viewModel = context.watch<PromptViewModel>();
return LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
physics: canScroll
? const BouncingScrollPhysics()
: const NeverScrollableScrollPhysics(),
child: Container(
padding: constraints.isMobile
? const EdgeInsets.only(
left: MarketplaceTheme.spacing7,
right: MarketplaceTheme.spacing7,
bottom: MarketplaceTheme.spacing7,
top: MarketplaceTheme.spacing7,
)
: const EdgeInsets.only(
left: MarketplaceTheme.spacing7,
right: MarketplaceTheme.spacing7,
bottom: MarketplaceTheme.spacing1,
top: MarketplaceTheme.spacing7,
),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: MarketplaceTheme.borderColor),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(4),
topRight: Radius.circular(50),
bottomRight:
Radius.circular(MarketplaceTheme.defaultBorderRadius),
bottomLeft:
Radius.circular(MarketplaceTheme.defaultBorderRadius),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(elementPadding + 10),
child: Text(
'Create a recipe:',
style: MarketplaceTheme.dossierParagraph.copyWith(
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
),
Padding(
padding: const EdgeInsets.all(
elementPadding,
),
child: SizedBox(
height: constraints.isMobile ? 130 : 230,
child: AddImageToPromptWidget(
height: constraints.isMobile ? 100 : 200,
width: constraints.isMobile ? 100 : 200,
),
),
),
if (constraints.isMobile)
Padding(
padding: const EdgeInsets.all(elementPadding),
child: _FilterChipSection(
label: "I also have these staple ingredients: ",
child: FilterChipSelectionInput<BasicIngredientsFilter>(
onChipSelected: (selected) {
viewModel.addBasicIngredients(
selected as Set<BasicIngredientsFilter>);
},
allValues: BasicIngredientsFilter.values,
selectedValues:
viewModel.userPrompt.selectedBasicIngredients,
),
),
),
if (constraints.isMobile)
Padding(
padding: const EdgeInsets.all(elementPadding),
child: _FilterChipSection(
label: "I'm in the mood for: ",
child: FilterChipSelectionInput<CuisineFilter>(
onChipSelected: (selected) {
viewModel.addCategoryFilters(
selected as Set<CuisineFilter>);
},
allValues: CuisineFilter.values,
selectedValues: viewModel.userPrompt.selectedCuisines,
),
),
),
if (constraints.isMobile)
Padding(
padding: const EdgeInsets.all(elementPadding),
child: _FilterChipSection(
label: "I have the following dietary restrictions:",
child:
FilterChipSelectionInput<DietaryRestrictionsFilter>(
onChipSelected: (selected) {
viewModel.addDietaryRestrictionFilter(
selected as Set<DietaryRestrictionsFilter>);
},
allValues: DietaryRestrictionsFilter.values,
selectedValues:
viewModel.userPrompt.selectedDietaryRestrictions,
),
),
),
if (!constraints.isMobile)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(elementPadding),
child: _FilterChipSection(
label: "I'm in the mood for: ",
child: FilterChipSelectionInput<CuisineFilter>(
onChipSelected: (selected) {
viewModel.addCategoryFilters(
selected as Set<CuisineFilter>);
},
allValues: CuisineFilter.values,
selectedValues:
viewModel.userPrompt.selectedCuisines,
),
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(elementPadding),
child: _FilterChipSection(
label: "I also have these staple ingredients: ",
child: FilterChipSelectionInput<
BasicIngredientsFilter>(
onChipSelected: (selected) {
viewModel.addBasicIngredients(
selected as Set<BasicIngredientsFilter>);
},
allValues: BasicIngredientsFilter.values,
selectedValues: viewModel
.userPrompt.selectedBasicIngredients,
),
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(elementPadding),
child: _FilterChipSection(
label:
"I have the following dietary restrictions:",
child: FilterChipSelectionInput<
DietaryRestrictionsFilter>(
onChipSelected: (selected) {
viewModel.addDietaryRestrictionFilter(selected
as Set<DietaryRestrictionsFilter>);
},
allValues: DietaryRestrictionsFilter.values,
selectedValues: viewModel
.userPrompt.selectedDietaryRestrictions,
),
),
),
),
],
),
Padding(
padding: const EdgeInsets.all(elementPadding),
child: _TextField(
controller: viewModel.promptTextController,
onChanged: (value) {
viewModel.notify();
},
),
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: MarketplaceTheme.spacing4,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (!constraints.isMobile) const Spacer(flex: 1),
if (!constraints.isMobile)
Expanded(
flex: 3,
child: MarketplaceButton(
onPressed: viewModel.resetPrompt,
buttonText: 'Reset prompt',
icon: Symbols.restart_alt,
iconColor: Colors.black45,
buttonBackgroundColor: Colors.transparent,
hoverColor:
MarketplaceTheme.secondary.withOpacity(.1),
),
),
const Spacer(flex: 1),
Expanded(
flex: constraints.isMobile ? 10 : 3,
child: MarketplaceButton(
onPressed: () {
final promptData = viewModel.buildPrompt();
showDialog<Null>(
context: context,
builder: (context) {
return FullPromptDialog(
promptData: promptData,
);
},
);
},
buttonText: 'Full prompt',
icon: Symbols.info_rounded,
),
),
const Spacer(flex: 1),
Expanded(
flex: constraints.isMobile ? 10 : 3,
child: MarketplaceButton(
onPressed: () async {
await viewModel.submitPrompt().then((_) async {
if (viewModel.recipe != null) {
bool? shouldSave = await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (context) => RecipeDialogScreen(
recipe: viewModel.recipe!,
actions: [
MarketplaceButton(
onPressed: () {
Navigator.of(context).pop(true);
},
buttonText: "Save Recipe",
icon: Symbols.save,
),
],
),
);
if (shouldSave != null && shouldSave) {
viewModel.saveRecipe();
}
}
});
},
buttonText: 'Submit prompt',
icon: Symbols.send,
),
),
const Spacer(flex: 1),
],
),
),
if (constraints.isMobile)
Align(
alignment: Alignment.center,
child: MarketplaceButton(
onPressed: viewModel.resetPrompt,
buttonText: 'Reset prompt',
icon: Symbols.restart_alt,
iconColor: Colors.black45,
buttonBackgroundColor: Colors.transparent,
hoverColor: MarketplaceTheme.secondary.withOpacity(.1),
),
),
const SizedBox(height: 200.0),
],
),
),
),
);
},
);
}
}
class _FilterChipSection extends StatelessWidget {
const _FilterChipSection({
required this.child,
required this.label,
});
final Widget child;
final String label;
@override
Widget build(BuildContext context) {
return HighlightBorderOnHoverWidget(
borderRadius: BorderRadius.zero,
child: Container(
height: 230,
decoration: BoxDecoration(
color: Theme.of(context).splashColor.withOpacity(.1),
border: Border.all(
color: MarketplaceTheme.borderColor,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.max,
children: [
Padding(
padding: const EdgeInsets.all(MarketplaceTheme.spacing7),
child: Text(
label,
style: MarketplaceTheme.dossierParagraph,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: child,
),
],
),
),
);
}
}
class _TextField extends StatelessWidget {
const _TextField({
required this.controller,
this.onChanged,
});
final TextEditingController controller;
final Null Function(String)? onChanged;
@override
Widget build(BuildContext context) {
return TextField(
scrollPadding: const EdgeInsets.only(bottom: 150),
maxLines: null,
onChanged: onChanged,
minLines: 3,
controller: controller,
style: WidgetStateTextStyle.resolveWith(
(states) => MarketplaceTheme.dossierParagraph),
decoration: InputDecoration(
fillColor: Theme.of(context).splashColor,
hintText: "Add additional context...",
hintStyle: WidgetStateTextStyle.resolveWith(
(states) => MarketplaceTheme.dossierParagraph,
),
enabledBorder: const OutlineInputBorder(
borderRadius: BorderRadius.zero,
borderSide: BorderSide(width: 1, color: Colors.black12),
),
focusedBorder: const OutlineInputBorder(
borderRadius: BorderRadius.zero,
borderSide: BorderSide(width: 1, color: Colors.black45),
),
filled: true,
),
);
}
}

@ -0,0 +1,168 @@
import 'package:ai_recipe_generation/services/gemini.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:google_generative_ai/google_generative_ai.dart';
import 'package:image_picker/image_picker.dart';
import '../../services/firestore.dart';
import '../../util/filter_chip_enums.dart';
import '../recipes/recipe_model.dart';
import 'prompt_model.dart';
class PromptViewModel extends ChangeNotifier {
PromptViewModel({
required this.multiModalModel,
required this.textModel,
});
final GenerativeModel multiModalModel;
final GenerativeModel textModel;
bool loadingNewRecipe = false;
PromptData userPrompt = PromptData.empty();
TextEditingController promptTextController = TextEditingController();
String badImageFailure =
"The recipe request either does not contain images, or does not contain images of food items. I cannot recommend a recipe.";
Recipe? recipe;
String? _geminiFailureResponse;
String? get geminiFailureResponse => _geminiFailureResponse;
set geminiFailureResponse(String? value) {
_geminiFailureResponse = value;
notifyListeners();
}
void notify() => notifyListeners();
void addImage(XFile image) {
userPrompt.images.insert(0, image);
notifyListeners();
}
void addAdditionalPromptContext(String text) {
final existingInputs = userPrompt.additionalTextInputs;
userPrompt.copyWith(additionalTextInputs: [...existingInputs, text]);
}
void removeImage(XFile image) {
userPrompt.images.removeWhere((el) => el.path == image.path);
notifyListeners();
}
void resetPrompt() {
userPrompt = PromptData.empty();
notifyListeners();
}
// Creates an ephemeral prompt with additional text that the user shouldn't be
// concerned with to send to Gemini, such as formatting.
PromptData buildPrompt() {
return PromptData(
images: userPrompt.images,
textInput: mainPrompt,
basicIngredients: userPrompt.selectedBasicIngredients,
cuisines: userPrompt.selectedCuisines,
dietaryRestrictions: userPrompt.selectedDietaryRestrictions,
additionalTextInputs: [format],
);
}
Future<void> submitPrompt() async {
loadingNewRecipe = true;
notifyListeners();
// Create an ephemeral PromptData, preserving the user prompt data without
// adding the additional context to it.
var model = userPrompt.images.isEmpty ? textModel : multiModalModel;
final prompt = buildPrompt();
try {
final content = await GeminiService.generateContent(model, prompt);
// handle no image or image of not-food
if (content.text != null && content.text!.contains(badImageFailure)) {
geminiFailureResponse = badImageFailure;
} else {
recipe = Recipe.fromGeneratedContent(content);
}
} catch (error) {
geminiFailureResponse = 'Failed to reach Gemini. \n\n$error';
if (kDebugMode) {
print(error);
}
loadingNewRecipe = false;
}
loadingNewRecipe = false;
resetPrompt();
notifyListeners();
}
void saveRecipe() {
FirestoreService.saveRecipe(recipe!);
}
void addBasicIngredients(Set<BasicIngredientsFilter> ingredients) {
userPrompt.selectedBasicIngredients.addAll(ingredients);
notifyListeners();
}
void addCategoryFilters(Set<CuisineFilter> categories) {
userPrompt.selectedCuisines.addAll(categories);
notifyListeners();
}
void addDietaryRestrictionFilter(
Set<DietaryRestrictionsFilter> restrictions) {
userPrompt.selectedDietaryRestrictions.addAll(restrictions);
notifyListeners();
}
String get mainPrompt {
return '''
You are a Cat who's a chef that travels around the world a lot, and your travels inspire recipes.
Recommend a recipe for me based on the provided image.
The recipe should only contain real, edible ingredients.
If there are no images attached, or if the image does not contain food items, respond exactly with: $badImageFailure
Adhere to food safety and handling best practices like ensuring that poultry is fully cooked.
I'm in the mood for the following types of cuisine: ${userPrompt.cuisines},
I have the following dietary restrictions: ${userPrompt.dietaryRestrictions}
Optionally also include the following ingredients: ${userPrompt.ingredients}
Do not repeat any ingredients.
After providing the recipe, add an descriptions that creatively explains why the recipe is good based on only the ingredients used in the recipe. Tell a short story of a travel experience that inspired the recipe.
List out any ingredients that are potential allergens.
Provide a summary of how many people the recipe will serve and the the nutritional information per serving.
${promptTextController.text.isNotEmpty ? promptTextController.text : ''}
''';
}
final String format = '''
Return the recipe as valid JSON using the following structure:
{
"id": \$uniqueId,
"title": \$recipeTitle,
"ingredients": \$ingredients,
"description": \$description,
"instructions": \$instructions,
"cuisine": \$cuisineType,
"allergens": \$allergens,
"servings": \$servings,
"nutritionInformation": {
"calories": "\$calories",
"fat": "\$fat",
"carbohydrates": "\$carbohydrates",
"protein": "\$protein",
},
}
uniqueId should be unique and of type String.
title, description, cuisine, allergens, and servings should be of String type.
ingredients and instructions should be of type List<String>.
nutritionInformation should be of type Map<String, String>.
''';
}

@ -0,0 +1,118 @@
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../../theme.dart';
class AppInfoDialog extends StatelessWidget {
const AppInfoDialog({super.key});
Widget bulletRow(String text, {IconData? icon}) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon ?? Symbols.label_important_outline),
const SizedBox(
width: 10,
),
Expanded(
child: Text(
text,
),
),
],
);
}
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
MarketplaceTheme.defaultBorderRadius,
),
),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: MarketplaceTheme.borderColor),
),
padding: const EdgeInsets.all(MarketplaceTheme.spacing4),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"Use the form on this screen to ask Cat Chef to make a recipe for you.",
style: MarketplaceTheme.heading3,
),
const SizedBox(
height: MarketplaceTheme.spacing4,
),
bulletRow(
"Add images of ingredients you have, like a picture of the inside of your fridge or pantry.",
icon: Symbols.looks_one,
),
const SizedBox(
height: MarketplaceTheme.spacing7,
),
bulletRow(
"Choose what kind of food you're in the mood for, and what staple ingredients you have that might not be pictured.",
icon: Symbols.looks_two,
),
const SizedBox(
height: MarketplaceTheme.spacing7,
),
bulletRow(
"In the text box at the bottom, add any additional context that you'd like. \nFor example, you could say \"I'm in a hurry! Make sure the recipe doesn't take longer than 30 minutes to make.\"",
icon: Symbols.looks_3,
),
const SizedBox(
height: MarketplaceTheme.spacing7,
),
bulletRow(
"Submit the prompt, and Chef Noodle will give you a recipe!",
icon: Symbols.looks_4,
),
const SizedBox(
height: MarketplaceTheme.spacing4,
),
Text(
"Steps 1, 2 and 3 are optional. More information will provide better results.",
style: MarketplaceTheme.label,
),
const SizedBox(height: MarketplaceTheme.spacing4),
TextButton.icon(
icon: const Icon(
Symbols.close,
color: Colors.black87,
),
label: Text(
'Close',
style: MarketplaceTheme.dossierParagraph,
),
onPressed: () {
Navigator.pop(context);
},
style: ButtonStyle(
shape: WidgetStateProperty.resolveWith(
(states) {
return const RoundedRectangleBorder(
side: BorderSide(color: Colors.black26),
borderRadius: BorderRadius.all(
Radius.circular(MarketplaceTheme.defaultBorderRadius),
),
);
},
),
textStyle: WidgetStateTextStyle.resolveWith(
(states) {
return MarketplaceTheme.dossierParagraph
.copyWith(color: Colors.black45);
},
),
),
),
],
),
),
);
}
}

@ -0,0 +1,112 @@
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../../theme.dart';
import '../../../widgets/prompt_image_widget.dart';
import '../prompt_model.dart';
class FullPromptDialog extends StatelessWidget {
const FullPromptDialog({super.key, required this.promptData});
final PromptData promptData;
Widget bulletRow(String text, {IconData? icon}) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon ?? Symbols.label_important_outline),
const SizedBox(
width: 10,
),
Expanded(
child: Text(
text,
),
),
],
);
}
@override
Widget build(BuildContext context) {
return Dialog.fullscreen(
child: SingleChildScrollView(
child: Container(
decoration: BoxDecoration(
border: Border.all(color: MarketplaceTheme.borderColor),
),
padding: const EdgeInsets.all(MarketplaceTheme.spacing4),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"This is the full prompt that will be sent to Google's Gemini model.",
style: MarketplaceTheme.heading3,
),
const SizedBox(height: MarketplaceTheme.spacing4),
if (promptData.images.isNotEmpty)
Container(
height: 100,
decoration: const BoxDecoration(
border: Border.symmetric(
horizontal: BorderSide(
color: MarketplaceTheme.borderColor,
),
),
),
child: ListView(
scrollDirection: Axis.horizontal,
children: [
for (var image in promptData.images)
Padding(
padding: const EdgeInsets.all(8.0),
child: PromptImage(
file: image,
),
),
],
),
),
const SizedBox(height: MarketplaceTheme.spacing4),
bulletRow(promptData.textInput),
if (promptData.additionalTextInputs.isNotEmpty)
...promptData.additionalTextInputs.map((i) => bulletRow(i)),
const SizedBox(height: MarketplaceTheme.spacing4),
TextButton.icon(
icon: const Icon(
Symbols.close,
color: Colors.black87,
),
label: Text(
'Close',
style: MarketplaceTheme.dossierParagraph,
),
onPressed: () {
Navigator.pop(context);
},
style: ButtonStyle(
shape: WidgetStateProperty.resolveWith(
(states) {
return const RoundedRectangleBorder(
side: BorderSide(color: Colors.black26),
borderRadius: BorderRadius.all(
Radius.circular(MarketplaceTheme.defaultBorderRadius),
),
);
},
),
textStyle: WidgetStateTextStyle.resolveWith(
(states) {
return MarketplaceTheme.dossierParagraph
.copyWith(color: Colors.black45);
},
),
),
),
],
),
),
),
);
}
}

@ -0,0 +1,278 @@
import 'package:ai_recipe_generation/widgets/highlight_border_on_hover_widget.dart';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import '../../../main.dart';
import '../../../theme.dart';
import '../../../util/device_info.dart';
import '../../../widgets/add_image_widget.dart';
import '../../../widgets/prompt_image_widget.dart';
import '../prompt_view_model.dart';
class AddImageToPromptWidget extends StatefulWidget {
const AddImageToPromptWidget({
super.key,
this.width = 100,
this.height = 100,
});
final double width;
final double height;
@override
State<AddImageToPromptWidget> createState() => _AddImageToPromptWidgetState();
}
class _AddImageToPromptWidgetState extends State<AddImageToPromptWidget> {
final ImagePicker picker = ImagePicker();
late CameraController _controller;
late Future<void> _initializeControllerFuture;
bool flashOn = false;
@override
void initState() {
super.initState();
if (DeviceInfo.isPhysicalDeviceWithCamera(deviceInfo)) {
_controller = CameraController(
camera,
ResolutionPreset.medium,
);
_initializeControllerFuture = _controller.initialize();
}
}
Future<XFile> _showCamera() async {
final image = await showGeneralDialog<XFile?>(
context: context,
transitionBuilder: (context, animation, secondaryAnimation, child) {
return AnimatedOpacity(
opacity: animation.value,
duration: const Duration(milliseconds: 100),
child: child,
);
},
pageBuilder: (context, animation, secondaryAnimation) {
return Dialog.fullscreen(
insetAnimationDuration: const Duration(seconds: 1),
child: FutureBuilder(
future: _initializeControllerFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
// If the Future is complete, display the preview.
return CameraView(
controller: _controller,
initializeControllerFuture: _initializeControllerFuture,
);
} else {
// Otherwise, display a loading indicator.
return const Center(child: CircularProgressIndicator());
}
},
),
);
},
);
if (image != null) {
return image;
} else {
throw "failed to take image";
}
}
Future<XFile> _pickImage() async {
final image = await picker.pickImage(source: ImageSource.gallery);
if (image != null) {
return image;
} else {
throw "failed to take image";
}
}
Future<XFile> _addImage() async {
if (DeviceInfo.isPhysicalDeviceWithCamera(deviceInfo)) {
return await _showCamera();
} else {
return await _pickImage();
}
}
@override
Widget build(BuildContext context) {
final viewModel = context.watch<PromptViewModel>();
return HighlightBorderOnHoverWidget(
borderRadius: BorderRadius.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(
left: MarketplaceTheme.spacing7,
top: MarketplaceTheme.spacing7,
),
child: Text(
'I have these ingredients:',
style: MarketplaceTheme.dossierParagraph,
),
),
SizedBox(
height: widget.height,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
Padding(
padding: const EdgeInsets.all(MarketplaceTheme.spacing7),
child: AddImage(
width: widget.width,
height: widget.height,
onTap: () async {
final image = await _addImage();
viewModel.addImage(image);
}),
),
for (var image in viewModel.userPrompt.images)
Padding(
padding: const EdgeInsets.all(MarketplaceTheme.spacing7),
child: PromptImage(
width: widget.width,
file: image,
onTapIcon: () => viewModel.removeImage(image),
),
),
],
),
),
],
),
);
}
}
class CameraView extends StatefulWidget {
final CameraController controller;
final Future initializeControllerFuture;
const CameraView(
{super.key,
required this.controller,
required this.initializeControllerFuture});
@override
State<CameraView> createState() => _CameraViewState();
}
class _CameraViewState extends State<CameraView> {
bool flashOn = false;
@override
Widget build(BuildContext context) {
CameraController controller = widget.controller;
return Stack(
children: [
Center(
child: AspectRatio(
aspectRatio: 9 / 14,
child: ClipRect(
child: FittedBox(
fit: BoxFit.cover,
child: SizedBox(
height: controller.value.previewSize!.width,
width: controller.value.previewSize!.height,
child: Center(
child: CameraPreview(
controller,
// child: ElevatedButton(
// child: Text('Button'),
// onPressed: () {},
// ),
),
),
),
),
),
),
),
Positioned(
top: 0,
left: 0,
right: 0,
height: 89.5,
child: Container(
color: Colors.black.withOpacity(.7),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding:
const EdgeInsets.only(left: MarketplaceTheme.spacing4),
child: IconButton(
icon: Icon(
flashOn ? Symbols.flash_on : Symbols.flash_off,
size: 40,
color: flashOn ? Colors.yellowAccent : Colors.white,
),
onPressed: () {
controller.setFlashMode(
flashOn ? FlashMode.off : FlashMode.always);
setState(() {
flashOn = !flashOn;
});
},
),
),
Padding(
padding:
const EdgeInsets.only(right: MarketplaceTheme.spacing4),
child: IconButton(
icon: const Icon(
Symbols.cancel,
color: Colors.white,
size: 40,
),
onPressed: () async {
Navigator.of(context).pop();
},
),
),
],
),
),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
height: 150,
child: Container(
color: Colors.black.withOpacity(.7),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: const Icon(
Symbols.camera,
color: Colors.white,
size: 70,
),
onPressed: () async {
try {
await widget.initializeControllerFuture;
final image = await controller.takePicture();
if (!context.mounted) return;
Navigator.of(context).pop(image);
} catch (e) {
rethrow;
}
},
),
],
),
),
)
],
);
}
}

@ -0,0 +1,110 @@
import 'dart:convert';
import 'package:ai_recipe_generation/util/json_parsing.dart';
import 'package:google_generative_ai/google_generative_ai.dart';
class Recipe {
Recipe({
required this.title,
required this.id,
required this.description,
required this.ingredients,
required this.instructions,
required this.cuisine,
required this.allergens,
required this.servings,
required this.nutritionInformation,
this.rating = -1,
});
final String id;
final String title;
final String description;
final List<String> ingredients;
final List<String> instructions;
final String cuisine;
final List<String> allergens;
final String servings;
final Map<String, dynamic> nutritionInformation;
int rating;
factory Recipe.fromGeneratedContent(GenerateContentResponse content) {
/// failures should be handled when the response is received
assert(content.text != null);
final validJson = cleanJson(content.text!);
final json = jsonDecode(validJson);
if (json
case {
"ingredients": List<dynamic> ingredients,
"instructions": List<dynamic> instructions,
"title": String title,
"id": String id,
"cuisine": String cuisine,
"description": String description,
"servings": String servings,
"nutritionInformation": Map<String, dynamic> nutritionInformation,
"allergens": List<dynamic> allergens,
}) {
return Recipe(
id: id,
title: title,
ingredients: ingredients.map((i) => i.toString()).toList(),
instructions: instructions.map((i) => i.toString()).toList(),
nutritionInformation: nutritionInformation,
allergens: allergens.map((i) => i.toString()).toList(),
cuisine: cuisine,
servings: servings,
description: description);
}
throw JsonUnsupportedObjectError(json);
}
Map<String, Object?> toFirestore() {
return {
'id': id,
'title': title,
'instructions': instructions,
'ingredients': ingredients,
'cuisine': cuisine,
'rating': rating,
'allergens': allergens,
'nutritionInformation': nutritionInformation,
'servings': servings,
'description': description,
};
}
factory Recipe.fromFirestore(Map<String, Object?> data) {
if (data
case {
"ingredients": List<dynamic> ingredients,
"instructions": List<dynamic> instructions,
"title": String title,
"id": String id,
"cuisine": String cuisine,
"description": String description,
"servings": String servings,
"nutritionInformation": Map<String, dynamic> nutritionInformation,
"allergens": List<dynamic> allergens,
"rating": int rating
}) {
return Recipe(
id: id,
title: title,
ingredients: ingredients.map((i) => i.toString()).toList(),
instructions: instructions.map((i) => i.toString()).toList(),
nutritionInformation: nutritionInformation,
allergens: allergens.map((i) => i.toString()).toList(),
cuisine: cuisine,
servings: servings,
description: description,
rating: rating,
);
}
throw "Malformed Firestore data";
}
}

@ -0,0 +1,31 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/cupertino.dart';
import '../../services/firestore.dart';
import 'recipe_model.dart';
class SavedRecipesViewModel extends ChangeNotifier {
List<Recipe> recipes = [];
final recipePath = '/recipes';
final firestore = FirebaseFirestore.instance;
SavedRecipesViewModel() {
firestore.collection(recipePath).snapshots().listen((querySnapshot) {
recipes = querySnapshot.docs.map((doc) {
final data = doc.data();
return Recipe.fromFirestore(data);
}).toList();
notifyListeners();
});
}
void deleteRecipe(Recipe recipe) {
FirestoreService.deleteRecipe(recipe);
}
void updateRecipe(Recipe recipe) {
FirestoreService.updateRecipe(recipe);
notifyListeners();
}
}

@ -0,0 +1,263 @@
import 'package:ai_recipe_generation/features/recipes/recipes_view_model.dart';
import 'package:ai_recipe_generation/features/recipes/widgets/recipe_fullscreen_dialog.dart';
import 'package:ai_recipe_generation/theme.dart';
import 'package:ai_recipe_generation/util/extensions.dart';
import 'package:ai_recipe_generation/widgets/highlight_border_on_hover_widget.dart';
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import '../../widgets/marketplace_button_widget.dart';
import '../../widgets/star_rating.dart';
import 'recipe_model.dart';
class SavedRecipesScreen extends StatefulWidget {
const SavedRecipesScreen({super.key, required this.canScroll});
final bool canScroll;
@override
State<SavedRecipesScreen> createState() => _SavedRecipesScreenState();
}
class _SavedRecipesScreenState extends State<SavedRecipesScreen>
with TickerProviderStateMixin {
@override
Widget build(BuildContext context) {
final viewModel = context.watch<SavedRecipesViewModel>();
return LayoutBuilder(
builder: (context, constraints) {
return Padding(
padding: constraints.isMobile
? const EdgeInsets.only(
left: MarketplaceTheme.spacing7,
right: MarketplaceTheme.spacing7,
bottom: MarketplaceTheme.spacing7,
top: MarketplaceTheme.spacing7,
)
: const EdgeInsets.only(
left: MarketplaceTheme.spacing7,
right: MarketplaceTheme.spacing7,
bottom: MarketplaceTheme.spacing1,
top: MarketplaceTheme.spacing7,
),
child: ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(MarketplaceTheme.defaultBorderRadius),
topRight: Radius.circular(50),
bottomRight:
Radius.circular(MarketplaceTheme.defaultBorderRadius),
bottomLeft: Radius.circular(MarketplaceTheme.defaultBorderRadius),
),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: MarketplaceTheme.borderColor),
borderRadius: const BorderRadius.only(
topLeft:
Radius.circular(MarketplaceTheme.defaultBorderRadius),
topRight: Radius.circular(50),
bottomRight:
Radius.circular(MarketplaceTheme.defaultBorderRadius),
bottomLeft:
Radius.circular(MarketplaceTheme.defaultBorderRadius),
),
color: Colors.white,
),
child: constraints.isMobile
? ListView.builder(
physics: widget.canScroll
? const PageScrollPhysics()
: const NeverScrollableScrollPhysics(),
itemCount: viewModel.recipes.length,
itemBuilder: (context, idx) {
final recipe = viewModel.recipes[idx];
return Container(
margin: EdgeInsets.only(top: idx == 0 ? 70 : 0),
child: Align(
heightFactor: .5,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: MarketplaceTheme.spacing7,
vertical: MarketplaceTheme.spacing7,
),
child: SizedBox(
width: MediaQuery.of(context).size.width * .99,
height: 200,
child: _ListTile(
constraints: constraints,
key: Key('$idx-${recipe.hashCode}'),
recipe: recipe,
idx: idx,
),
),
),
),
);
},
)
: GridView.count(
physics: widget.canScroll
? const PageScrollPhysics()
: const NeverScrollableScrollPhysics(),
crossAxisCount: 3,
childAspectRatio: 1.5,
children: [
...List.generate(viewModel.recipes.length, (idx) {
final recipe = viewModel.recipes[idx];
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: MarketplaceTheme.spacing7,
vertical: MarketplaceTheme.spacing7,
),
child: _ListTile(
key: Key('$idx-${recipe.hashCode}'),
recipe: recipe,
idx: idx,
constraints: constraints,
),
);
}),
],
),
),
),
);
},
);
}
}
class _ListTile extends StatefulWidget {
const _ListTile({
super.key,
required this.recipe,
this.idx = 0,
required this.constraints,
});
final Recipe recipe;
final int idx;
final BoxConstraints constraints;
@override
State<_ListTile> createState() => _ListTileState();
}
class _ListTileState extends State<_ListTile> {
final List<Color> colors = [
MarketplaceTheme.primary,
MarketplaceTheme.secondary,
MarketplaceTheme.tertiary,
MarketplaceTheme.scrim,
];
@override
Widget build(BuildContext context) {
final viewModel = context.watch<SavedRecipesViewModel>();
final color = colors[widget.idx % colors.length];
return GestureDetector(
child: HighlightBorderOnHoverWidget(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(MarketplaceTheme.defaultBorderRadius),
topRight: Radius.circular(50),
bottomRight: Radius.circular(MarketplaceTheme.defaultBorderRadius),
bottomLeft: Radius.circular(MarketplaceTheme.defaultBorderRadius),
),
color: color,
child: Container(
decoration: const BoxDecoration(
boxShadow: [
BoxShadow(
offset: Offset(0, -2),
color: Colors.black38,
blurRadius: 5,
),
],
borderRadius: BorderRadius.only(
topLeft: Radius.circular(MarketplaceTheme.defaultBorderRadius),
topRight: Radius.circular(50),
bottomRight:
Radius.circular(MarketplaceTheme.defaultBorderRadius),
bottomLeft: Radius.circular(MarketplaceTheme.defaultBorderRadius),
),
color: Colors.white,
),
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(MarketplaceTheme.defaultBorderRadius),
topRight: Radius.circular(50),
bottomRight:
Radius.circular(MarketplaceTheme.defaultBorderRadius),
bottomLeft:
Radius.circular(MarketplaceTheme.defaultBorderRadius),
),
color: color.withOpacity(.3),
),
padding: const EdgeInsets.all(MarketplaceTheme.spacing7),
child: Stack(
children: [
Text(
widget.recipe.title,
style: MarketplaceTheme.heading3,
),
Positioned(
top: widget.constraints.isMobile ? 40 : 60,
left: 0,
child: Text(
widget.recipe.cuisine,
style: MarketplaceTheme.subheading1,
),
),
Positioned(
right: 15,
top: widget.constraints.isMobile ? 40 : 60,
child: StartRating(
initialRating: widget.recipe.rating,
starColor: color,
onTap: null,
),
)
],
),
),
),
),
onTap: () async {
await showDialog<Null>(
context: context,
builder: (context) {
return RecipeDialogScreen(
recipe: widget.recipe,
subheading: Row(
children: [
const Text('My rating:'),
const SizedBox(width: 10),
StartRating(
initialRating: widget.recipe.rating,
starColor: MarketplaceTheme.tertiary,
onTap: (index) {
widget.recipe.rating = index + 1;
viewModel.updateRecipe(widget.recipe);
},
),
],
),
actions: [
MarketplaceButton(
onPressed: () {
viewModel.deleteRecipe(widget.recipe);
Navigator.of(context).pop();
},
buttonText: "Delete Recipe",
icon: Symbols.delete,
),
],
);
},
);
},
);
}
}

@ -0,0 +1,277 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../../theme.dart';
import '../recipe_model.dart';
class RecipeDisplayWidget extends StatelessWidget {
const RecipeDisplayWidget({
super.key,
required this.recipe,
this.subheading,
});
final Recipe recipe;
final Widget? subheading;
List<Widget> _buildIngredients(List<String> ingredients) {
final widgets = <Widget>[];
for (var ingredient in ingredients) {
widgets.add(
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Symbols.stat_0_rounded,
size: 12,
),
const SizedBox(
width: 5,
),
Expanded(
child: Text(
ingredient,
softWrap: true,
),
),
],
),
);
}
return widgets;
}
List<Widget> _buildInstructions(List<String> instructions) {
final widgets = <Widget>[];
// check for existing numbers in instructions.
if (instructions.first.startsWith(RegExp('[0-9]'))) {
for (var instruction in instructions) {
widgets.add(Text(instruction));
widgets.add(const SizedBox(height: MarketplaceTheme.spacing6));
}
} else {
for (var i = 0; i < instructions.length; i++) {
widgets.add(Text(
'${i + 1}. ${instructions[i]}',
softWrap: true,
));
widgets.add(const SizedBox(height: MarketplaceTheme.spacing6));
}
}
return widgets;
}
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
physics: const ClampingScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(MarketplaceTheme.defaultBorderRadius),
color: MarketplaceTheme.primary.withOpacity(.5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
recipe.title,
softWrap: true,
style: MarketplaceTheme.heading2,
),
if (subheading != null)
Padding(
padding: const EdgeInsets.symmetric(
vertical: MarketplaceTheme.spacing7,
),
child: subheading,
),
],
),
),
TextButton(
style: ButtonStyle(
backgroundColor: WidgetStateColor.resolveWith((states) {
if (states.contains(WidgetState.hovered)) {
return MarketplaceTheme.scrim.withOpacity(.6);
}
return Colors.white;
}),
shape: WidgetStateProperty.resolveWith(
(states) {
return RoundedRectangleBorder(
side: const BorderSide(
color: MarketplaceTheme.primary),
borderRadius: BorderRadius.circular(
MarketplaceTheme.defaultBorderRadius,
),
);
},
),
textStyle: WidgetStateTextStyle.resolveWith(
(states) {
return MarketplaceTheme.dossierParagraph.copyWith(
color: Colors.black45,
);
},
),
),
onPressed: () async {
await showDialog<dynamic>(
context: context,
builder: (context) {
return AlertDialog(
content: Padding(
padding: const EdgeInsets.all(
MarketplaceTheme.spacing7),
child: Text(recipe.description),
),
);
},
);
},
child: Transform.translate(
offset: const Offset(0, 5),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: MarketplaceTheme.spacing6),
child: Row(
children: [
SizedBox(
width: 35,
height: 35,
child: SvgPicture.asset(
'assets/chef_cat.svg',
semanticsLabel: 'Chef cat icon',
),
),
Transform.translate(
offset: const Offset(1, -6),
child: Transform.rotate(
angle: -pi / 20.0,
child: Text(
'Chef Noodle \n says...',
style: MarketplaceTheme.label,
),
),
)
],
),
),
),
)
],
),
const Divider(
height: 40,
color: Colors.black26,
),
Table(
columnWidths: const {
0: FlexColumnWidth(2),
1: FlexColumnWidth(3),
},
children: [
TableRow(
children: [
Text(
'Allergens:',
style: MarketplaceTheme.paragraph.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(recipe.allergens.join(', '))
],
),
TableRow(children: [
Text(
'Servings:',
style: MarketplaceTheme.paragraph.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(recipe.servings)
]),
TableRow(children: [
Text(
'Nutrition per serving:',
style: MarketplaceTheme.paragraph.copyWith(
fontWeight: FontWeight.bold,
),
),
const Text(''),
]),
...recipe.nutritionInformation.entries.map((entry) {
return TableRow(children: [
Row(
children: [
const Icon(
Symbols.stat_0_rounded,
size: 12,
),
const SizedBox(
width: 5,
),
Expanded(
child: Text(
entry.key,
style: MarketplaceTheme.label,
softWrap: true,
),
),
],
),
Text(entry.value as String,
style: MarketplaceTheme.label)
]);
}),
],
),
],
),
),
/// Body section
Padding(
padding: const EdgeInsets.all(MarketplaceTheme.spacing4),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
vertical: MarketplaceTheme.spacing7,
),
child:
Text('Ingredients:', style: MarketplaceTheme.subheading1),
),
..._buildIngredients(recipe.ingredients),
const SizedBox(height: MarketplaceTheme.spacing4),
Padding(
padding: const EdgeInsets.symmetric(
vertical: MarketplaceTheme.spacing7),
child: Text('Instructions:',
style: MarketplaceTheme.subheading1),
),
..._buildInstructions(recipe.instructions),
],
),
)
],
),
);
}
}

@ -0,0 +1,56 @@
import 'package:ai_recipe_generation/features/recipes/widgets/recipe_display_widget.dart';
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../../theme.dart';
import '../../../widgets/marketplace_button_widget.dart';
import '../recipe_model.dart';
class RecipeDialogScreen extends StatelessWidget {
const RecipeDialogScreen({
super.key,
required this.recipe,
required this.actions,
this.subheading,
});
final Recipe recipe;
final List<Widget> actions;
final Widget? subheading;
@override
Widget build(BuildContext context) {
return Dialog.fullscreen(
backgroundColor: Colors.white,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: RecipeDisplayWidget(
recipe: recipe,
subheading: subheading,
),
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: MarketplaceTheme.spacing5,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
MarketplaceButton(
onPressed: () {
Navigator.of(context).pop(true);
},
buttonText: 'Close',
icon: Symbols.close,
),
...actions,
],
),
),
],
),
);
}
}

@ -0,0 +1,81 @@
// File generated by FlutterFire CLI.
// ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
return web;
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
return macos;
case TargetPlatform.windows:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for windows - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions web = FirebaseOptions(
apiKey: 'FIREBASE API KEY',
appId: 'FIREBASE APP ID',
messagingSenderId: 'FIREBASE MESSAGING ID',
projectId: 'PROJECT ID',
authDomain: 'AUTH DOMAIN',
storageBucket: 'STORAGE BUCKET ID',
);
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'FIREBASE API KEY',
appId: 'FIREBASE APP ID',
messagingSenderId: 'FIREBASE MESSAGING ID',
projectId: 'PROJECT ID',
authDomain: 'AUTH DOMAIN',
storageBucket: 'STORAGE BUCKET ID',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'FIREBASE API KEY',
appId: 'FIREBASE APP ID',
messagingSenderId: 'FIREBASE MESSAGING ID',
projectId: 'PROJECT ID',
authDomain: 'AUTH DOMAIN',
storageBucket: 'STORAGE BUCKET ID',
);
static const FirebaseOptions macos = FirebaseOptions(
apiKey: 'FIREBASE API KEY',
appId: 'FIREBASE APP ID',
messagingSenderId: 'FIREBASE MESSAGING ID',
projectId: 'PROJECT ID',
authDomain: 'AUTH DOMAIN',
storageBucket: 'STORAGE BUCKET ID',
);
}

@ -0,0 +1,122 @@
import 'package:ai_recipe_generation/util/device_info.dart';
import 'package:ai_recipe_generation/util/tap_recorder.dart';
import 'package:camera/camera.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:google_generative_ai/google_generative_ai.dart';
import 'package:provider/provider.dart';
import 'features/prompt/prompt_view_model.dart';
import 'features/recipes/recipes_view_model.dart';
import 'firebase_options.dart';
import 'router.dart';
import 'theme.dart';
late CameraDescription camera;
late BaseDeviceInfo deviceInfo;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
deviceInfo = await DeviceInfo.initialize(DeviceInfoPlugin());
if (DeviceInfo.isPhysicalDeviceWithCamera(deviceInfo)) {
final cameras = await availableCameras();
camera = cameras.first;
}
runApp(const MainApp());
}
class MainApp extends StatefulWidget {
const MainApp({super.key});
@override
State<MainApp> createState() => _MainAppState();
}
class _MainAppState extends State<MainApp> {
late GenerativeModel geminiVisionProModel;
late GenerativeModel geminiProModel;
@override
void initState() {
const apiKey =
String.fromEnvironment('API_KEY', defaultValue: 'key not found');
if (apiKey == 'key not found') {
throw InvalidApiKey(
'Key not found in environment. Please add an API key.',
);
}
geminiVisionProModel = GenerativeModel(
model: 'gemini-pro-vision',
apiKey: apiKey,
generationConfig: GenerationConfig(
temperature: 0.4,
topK: 32,
topP: 1,
maxOutputTokens: 4096,
),
safetySettings: [
SafetySetting(HarmCategory.harassment, HarmBlockThreshold.high),
SafetySetting(HarmCategory.hateSpeech, HarmBlockThreshold.high),
],
);
geminiProModel = GenerativeModel(
model: 'gemini-pro',
apiKey: const String.fromEnvironment('API_KEY'),
generationConfig: GenerationConfig(
temperature: 0.4,
topK: 32,
topP: 1,
maxOutputTokens: 4096,
),
safetySettings: [
SafetySetting(HarmCategory.harassment, HarmBlockThreshold.high),
SafetySetting(HarmCategory.hateSpeech, HarmBlockThreshold.high),
],
);
super.initState();
}
@override
Widget build(BuildContext context) {
final recipesViewModel = SavedRecipesViewModel();
return TapRecorder(
child: MultiProvider(
providers: [
ChangeNotifierProvider(
create: (_) => PromptViewModel(
multiModalModel: geminiVisionProModel,
textModel: geminiProModel,
),
),
ChangeNotifierProvider(
create: (_) => recipesViewModel,
),
],
child: SafeArea(
child: MaterialApp(
debugShowCheckedModeBanner: false,
theme: MarketplaceTheme.theme,
scrollBehavior: const ScrollBehavior().copyWith(
dragDevices: {
PointerDeviceKind.mouse,
PointerDeviceKind.touch,
PointerDeviceKind.stylus,
PointerDeviceKind.unknown,
},
),
home: const AdaptiveRouter(),
),
),
),
);
}
}

@ -0,0 +1,248 @@
import 'package:ai_recipe_generation/app_bar.dart';
import 'package:ai_recipe_generation/features/prompt/prompt_screen.dart';
import 'package:ai_recipe_generation/features/prompt/prompt_view_model.dart';
import 'package:ai_recipe_generation/features/recipes/saved_recipes_screen.dart';
import 'package:ai_recipe_generation/widgets/bottom_bar_shape_border.dart';
import 'package:ai_recipe_generation/widgets/marketplace_button_widget.dart';
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import 'theme.dart';
import 'widgets/icon_loading_indicator.dart';
const double avatarSize = 50;
const double collapsedHeight = 100;
const double expandedHeight = 300;
const double bottomTabBarHeight = 50;
class AdaptiveRouter extends StatefulWidget {
const AdaptiveRouter({super.key});
@override
State<AdaptiveRouter> createState() => _AdaptiveRouterState();
}
class _AdaptiveRouterState extends State<AdaptiveRouter>
with TickerProviderStateMixin {
late TextStyle _textStyle;
late ScrollController scrollController;
late TabController tabController;
bool innerScrollAllowed = false;
@override
void initState() {
super.initState();
tabController = TabController(length: 2, vsync: this);
_textStyle = MarketplaceTheme.heading1.copyWith(
color: Colors.black87.withOpacity(
1.0,
),
);
scrollController = ScrollController();
scrollController.addListener(_scrollListener);
}
double prevOffset = 0;
void _scrollListener() {
setState(() {
innerScrollAllowed = scrollController.offset >= 230;
if (scrollController.offset >= 230) {
scrollController.animateTo(230,
duration: const Duration(milliseconds: 100),
curve: Curves.decelerate);
}
// Don't change the text opacity if scrolling down from original position (overscroll)
if (scrollController.offset < 0) return;
// By offset 200, ensure the text is transparent
if (scrollController.offset > 200) {
_textStyle = _textStyle.copyWith(
color: Colors.black87.withOpacity(0),
);
return;
}
var value = double.parse(
(1 - (scrollController.offset - 50) / 100).toStringAsFixed(2),
);
if (scrollController.offset > 200 && value > 0) value = 0;
if (value > 1) value = 1;
if (value < 0) value = 0;
_textStyle = _textStyle.copyWith(
color: Colors.black87.withOpacity(
value,
),
);
});
}
@override
void dispose() {
scrollController.dispose();
tabController.dispose();
super.dispose();
}
List<NavigationRailDestination> destinations = [
const NavigationRailDestination(
icon: Icon(Symbols.home),
label: Text('Create a recipe'),
),
const NavigationRailDestination(
icon: Icon(Symbols.bookmarks),
label: Text('Saved Recipes'),
)
];
@override
Widget build(BuildContext context) {
final viewModel = context.watch<PromptViewModel>();
return LayoutBuilder(
builder: (context, constraints) {
return Scaffold(
body: Stack(
children: [
CustomScrollView(
controller: scrollController,
keyboardDismissBehavior:
ScrollViewKeyboardDismissBehavior.onDrag,
slivers: [
AnimatedAppBar(
scrollController: scrollController,
textStyle: _textStyle,
tabController: tabController,
),
SliverToBoxAdapter(
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: constraints.minHeight,
),
child: TabBarView(
controller: tabController,
children: [
PromptScreen(
canScroll: innerScrollAllowed,
),
SavedRecipesScreen(
canScroll: innerScrollAllowed,
),
],
),
),
)
],
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
height: bottomTabBarHeight,
decoration: ShapeDecoration(
shadows: const [
BoxShadow(
offset: Offset(1, -1),
color: Colors.black45,
blurRadius: 5,
)
],
shape: const BottomBarShapeBorder(50),
color: Theme.of(context).primaryColor,
),
child: TabBar(
labelColor: Colors.black,
unselectedLabelColor: Colors.black26,
controller: tabController,
onTap: (idx) {
setState(() {});
},
dividerColor: Colors.transparent,
tabs: [
for (var destination in destinations) destination.icon,
],
),
),
),
if (viewModel.loadingNewRecipe)
Positioned(
top: (MediaQuery.of(context).size.height / 2) - 80,
left: (MediaQuery.of(context).size.width / 2) - 80,
height: 160,
width: 160,
child: IconLoadingAnimator(
icons: const [
Symbols.icecream,
Symbols.local_pizza,
Symbols.restaurant_menu,
Symbols.egg,
Symbols.bakery_dining,
Symbols.skillet,
Symbols.nutrition,
Symbols.grocery,
Symbols.set_meal,
Icons.egg_alt,
Symbols.oven,
Icons.dinner_dining,
Icons.outdoor_grill,
Icons.cookie,
Icons.blender,
Symbols.stockpot,
],
),
),
if (viewModel.geminiFailureResponse != null)
Positioned(
top: (MediaQuery.of(context).size.height / 4),
left: (MediaQuery.of(context).size.width / 2) - 160,
height: MediaQuery.of(context).size.height / 4,
width: 320,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(
MarketplaceTheme.defaultBorderRadius),
boxShadow: const [
BoxShadow(
offset: Offset(-1, 1),
color: Colors.black45,
blurRadius: 5,
)
],
color: Colors.white,
border: Border.all(
color: MarketplaceTheme.focusedBorderColor,
width: 1,
),
),
child: Padding(
padding: const EdgeInsets.all(MarketplaceTheme.spacing6),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(viewModel.geminiFailureResponse!),
Align(
alignment: Alignment.bottomRight,
child: MarketplaceButton(
onPressed: () {
viewModel.geminiFailureResponse = null;
},
buttonText: "Dismiss",
icon: Symbols.close,
),
)
],
),
),
),
),
],
),
);
},
);
}
}

@ -0,0 +1,25 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import '../features/recipes/recipe_model.dart';
const recipePath = '/recipes';
final firestore = FirebaseFirestore.instance;
class FirestoreService {
static Future<Null> saveRecipe(Recipe recipe) async {
await firestore
.collection(recipePath)
.doc(recipe.id)
.set(recipe.toFirestore());
}
static Future<Null> deleteRecipe(Recipe recipe) async {
await firestore.doc("$recipePath/${recipe.id}").delete();
}
static Future<Null> updateRecipe(Recipe recipe) async {
await firestore
.doc("$recipePath/${recipe.id}")
.update(recipe.toFirestore());
}
}

@ -0,0 +1,58 @@
import 'package:google_generative_ai/google_generative_ai.dart';
import '../features/prompt/prompt_model.dart';
class GeminiService {
static Future<GenerateContentResponse> generateContent(
GenerativeModel model, PromptData prompt) async {
if (prompt.images.isEmpty) {
return await GeminiService.generateContentFromText(model, prompt);
} else {
return await GeminiService.generateContentFromMultiModal(model, prompt);
}
}
static Future<GenerateContentResponse> generateContentFromMultiModal(
GenerativeModel model, PromptData prompt) async {
final mainText = TextPart(prompt.textInput);
final additionalTextParts =
prompt.additionalTextInputs.map((t) => TextPart(t));
final imagesParts = <DataPart>[];
for (var f in prompt.images) {
final bytes = await (f.readAsBytes());
imagesParts.add(DataPart('image/jpeg', bytes));
}
final input = [
Content.multi([...imagesParts, mainText, ...additionalTextParts])
];
return await model.generateContent(
input,
generationConfig: GenerationConfig(
temperature: 0.4,
topK: 32,
topP: 1,
maxOutputTokens: 4096,
),
safetySettings: [
SafetySetting(HarmCategory.harassment, HarmBlockThreshold.high),
SafetySetting(HarmCategory.hateSpeech, HarmBlockThreshold.high),
],
);
}
static Future<GenerateContentResponse> generateContentFromText(
GenerativeModel model, PromptData prompt) async {
final mainText = TextPart(prompt.textInput);
final additionalTextParts =
prompt.additionalTextInputs.map((t) => TextPart(t)).join("\n");
return await model.generateContent([
Content.text(
'${mainText.text} \n $additionalTextParts',
)
]);
}
}

@ -0,0 +1,134 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
abstract class MarketplaceTheme {
static ThemeData theme = ThemeData(
fontFamily: GoogleFonts.lexend().fontFamily,
textTheme: GoogleFonts.lexendTextTheme().copyWith().apply(
bodyColor: const Color(0xff000000),
displayColor: const Color(0xff000000)),
colorScheme: const ColorScheme.light(
primary: Color(0xffA2E3F6),
secondary: Color(0xff4FAD85),
tertiary: Color(0xffDE7A60),
scrim: Color(0xffFFABC7),
surface: Color(0xffFDF7F0),
onSecondary: Color(0xff000000),
shadow: Color(0xffAEAEAE),
onPrimary: Color(0xffFFFFFF),
),
useMaterial3: true,
canvasColor: Colors.transparent,
navigationBarTheme: NavigationBarThemeData(
indicatorColor: const Color(0xffA2E3F6),
indicatorShape: CircleBorder(
side: BorderSide.lerp(
const BorderSide(
color: Color(0xff000000),
width: 2,
),
const BorderSide(
color: Color(0xff000000),
width: 2,
),
1),
),
),
);
static const Color primary = Color(0xffA2E3F6);
static const Color scrim = Color(0xffFFABC7);
static const Color tertiary = Color(0xffDE7A60);
static const Color secondary = Color(0xff4FAD85);
static const Color borderColor = Colors.black12;
static const Color focusedBorderColor = Colors.black45;
static const double defaultBorderRadius = 16;
static const double defaultTextSize = 16;
static const Color defaultTextColor = Colors.black87;
static TextStyle get heading1 => theme.textTheme.headlineLarge!.copyWith(
fontWeight: FontWeight.bold,
fontSize: 28,
//height: 36,
color: theme.colorScheme.onSecondary,
);
static TextStyle get heading2 => theme.textTheme.headlineMedium!.copyWith(
fontWeight: FontWeight.bold,
fontSize: 24,
//height: 32,
color: theme.colorScheme.onSecondary,
);
static TextStyle get heading3 => theme.textTheme.headlineSmall!.copyWith(
fontWeight: FontWeight.bold,
fontSize: 18,
//height: 24,
color: theme.colorScheme.onSecondary,
);
static TextStyle get subheading1 => theme.textTheme.bodyLarge!.copyWith(
fontWeight: FontWeight.normal,
fontSize: 18,
//height: 20,
color: theme.colorScheme.onSecondary,
);
static TextStyle get subheading2 => theme.textTheme.bodyMedium!.copyWith(
fontWeight: FontWeight.normal,
fontSize: 14,
//height: 18,
color: theme.colorScheme.onSecondary,
);
static TextStyle get paragraph => theme.textTheme.bodySmall!.copyWith(
fontWeight: FontWeight.normal,
fontSize: 14,
//height: 18,
color: theme.colorScheme.onSecondary,
);
static TextStyle get label => theme.textTheme.labelSmall!.copyWith(
fontWeight: FontWeight.w600,
fontSize: 11,
//height: 16,
color: theme.colorScheme.onSecondary,
);
static TextStyle get dossierParagraph => GoogleFonts.anonymousPro().copyWith(
fontWeight: FontWeight.normal,
fontSize: 14,
//height: 18,
color: theme.colorScheme.onSecondary,
);
static TextStyle get dossierSubheading => GoogleFonts.anonymousPro().copyWith(
fontWeight: FontWeight.normal,
fontSize: 18,
//height: 18,
color: theme.colorScheme.onSecondary,
);
static TextStyle get dossierHeading => GoogleFonts.anonymousPro().copyWith(
fontWeight: FontWeight.bold,
fontSize: 28,
//height: 18,
color: theme.colorScheme.onSecondary,
);
static const double _spacingUnit = 8;
static const double spacing8 = _spacingUnit / 2;
static const double spacing7 = _spacingUnit;
static const double spacing6 = _spacingUnit * 1.5;
static const double spacing5 = _spacingUnit * 2;
static const double spacing4 = _spacingUnit * 2.5;
static const double spacing3 = _spacingUnit * 3;
static const double spacing2 = _spacingUnit * 3.5;
static const double spacing1 = _spacingUnit * 4;
static double lineWidth = 1;
static const Widget verticalSpacer = SizedBox(height: spacing5);
}

@ -0,0 +1,39 @@
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
class DeviceInfo {
static Future<BaseDeviceInfo> initialize(DeviceInfoPlugin plugin) async {
if (kIsWeb) {
return await plugin.webBrowserInfo;
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return await plugin.androidInfo;
case TargetPlatform.iOS:
return await plugin.iosInfo;
case TargetPlatform.macOS:
return plugin.macOsInfo;
case TargetPlatform.windows:
return await plugin.windowsInfo;
case TargetPlatform.linux:
return await plugin.linuxInfo;
default:
throw UnsupportedError(
'Device info not supported for this platform',
);
}
}
static bool isPhysicalDeviceWithCamera(BaseDeviceInfo deviceInfo) {
if (deviceInfo is! IosDeviceInfo && deviceInfo is! AndroidDeviceInfo) {
return false;
}
if (deviceInfo is IosDeviceInfo && deviceInfo.isPhysicalDevice) {
return true;
}
if (deviceInfo is AndroidDeviceInfo && deviceInfo.isPhysicalDevice) {
return true;
}
return false;
}
}

@ -0,0 +1,13 @@
import 'package:flutter/rendering.dart';
extension SliverBreakpointUtils on SliverConstraints {
bool get isTablet => crossAxisExtent > 730 && crossAxisExtent < 1000;
bool get isDesktop => crossAxisExtent > 1000;
bool get isMobile => crossAxisExtent < 730;
}
extension BoxBreakpointUtils on BoxConstraints {
bool get isTablet => maxWidth > 730 && maxWidth < 1000;
bool get isDesktop => maxWidth > 1000;
bool get isMobile => maxWidth < 730;
}

@ -0,0 +1,66 @@
enum CuisineFilter {
italian,
mexican,
american,
french,
japanese,
chinese,
indian,
greek,
moroccan,
ethiopian,
southAfrican,
}
enum BasicIngredientsFilter {
oil,
butter,
flour,
salt,
pepper,
sugar,
milk,
vinegar,
}
enum DietaryRestrictionsFilter {
vegan,
vegetarian,
lactoseIntolerant,
kosher,
// keto,
wheatAllergies,
nutAllergies,
fishAllergies,
soyAllergies,
}
String dietaryRestrictionReadable(DietaryRestrictionsFilter filter) {
return switch (filter) {
DietaryRestrictionsFilter.vegan => 'vegan',
DietaryRestrictionsFilter.vegetarian => 'vegetarian',
DietaryRestrictionsFilter.lactoseIntolerant => 'dairy free',
DietaryRestrictionsFilter.kosher => 'kosher',
// DietaryRestrictionsFilter.keto => 'low carb',
DietaryRestrictionsFilter.wheatAllergies => 'wheat allergy',
DietaryRestrictionsFilter.nutAllergies => 'nut allergy',
DietaryRestrictionsFilter.fishAllergies => 'fish allergy',
DietaryRestrictionsFilter.soyAllergies => 'soy allergy',
};
}
String cuisineReadable(CuisineFilter filter) {
return switch (filter) {
CuisineFilter.italian => 'Italian',
CuisineFilter.mexican => 'Mexican',
CuisineFilter.american => 'American',
CuisineFilter.french => 'French',
CuisineFilter.japanese => 'Japanese',
CuisineFilter.chinese => 'Chinese',
CuisineFilter.indian => 'Indian',
CuisineFilter.ethiopian => 'Ethiopian',
CuisineFilter.moroccan => 'Moroccan',
CuisineFilter.greek => 'Greek',
CuisineFilter.southAfrican => 'South African',
};
}

@ -0,0 +1,8 @@
String cleanJson(String maybeInvalidJson) {
if (maybeInvalidJson.contains('```')) {
final withoutLeading = maybeInvalidJson.split('```json').last;
final withoutTrailing = withoutLeading.split('```').first;
return withoutTrailing;
}
return maybeInvalidJson;
}

@ -0,0 +1,120 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
/// From: https://gist.github.com/creativecreatorormaybenot/cd42b60cb33c9962b19f629ec638d4de
/// This is code that I (https://twitter.com/creativemaybeno) wrote for a
/// StackOverflow answer.
/// You can find it here: https://stackoverflow.com/a/65067655/6509751.
/// List of the taps recorded by [TapRecorder].
///
/// This is only a make-shift solution of course. This will only be viable
/// when using a single [TapRecorder] because it is saved as a top-level
/// variable.
@visibleForTesting
final recordedTaps = <Offset>[];
/// These are the parameters for the visualization of the recorded taps.
const _tapRadius = 15.0,
_tapDuration = Duration(milliseconds: 420),
_tapColor = Colors.white,
_shadowColor = Colors.black,
_shadowElevation = 2.0;
/// Widget that records any taps that hit its child.
///
/// It does not matter to this widget whether the child accepts the hit events.
/// Everything hitting the rect of the child will be recorded.
///
/// It will both visualize them and add them to [recordedTaps].
class TapRecorder extends SingleChildRenderObjectWidget {
const TapRecorder({super.key, required Widget child}) : super(child: child);
@override
RenderObject createRenderObject(BuildContext context) {
return _RenderTapRecorder();
}
}
class _RenderTapRecorder extends RenderProxyBox with _SilentTickerProvider {
final _recordedTaps = <_RecordedTap>[];
@override
void detach() {
for (final recordedTap in _recordedTaps) {
(recordedTap.animation as AnimationController).dispose();
}
_recordedTaps.clear();
super.detach();
}
@override
bool hitTest(BoxHitTestResult result, {required Offset position}) {
if (!size.contains(position)) return false;
// We always want to add a hit test entry for ourselves as we want to react
// to each and every hit event.
result.add(BoxHitTestEntry(this, position));
return hitTestChildren(result, position: position);
}
@override
void handleEvent(PointerEvent event, covariant HitTestEntry entry) {
// We do not want to interfere in the gesture arena, which is why we are not
// using regular tap recognizers. Instead, we handle it ourselves and always
// react to the hit events (ignoring the gesture arena).
if (event is PointerDownEvent) {
// Records the global position.
recordedTaps.add(event.position);
final controller = AnimationController(
vsync: this,
duration: _tapDuration,
),
recordedTap = _RecordedTap(event.localPosition, controller);
_recordedTaps.add(recordedTap);
controller
..addListener(markNeedsPaint)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.dispose();
_recordedTaps.remove(recordedTap);
}
})
..forward();
}
}
@override
void paint(PaintingContext context, Offset offset) {
context.paintChild(child!, offset);
final canvas = context.canvas;
for (final tap in _recordedTaps) {
final path = Path()
..addOval(
Rect.fromCircle(center: tap.localPosition, radius: _tapRadius));
final opacity = 1 - tap.animation.value;
canvas.drawShadow(
path, _shadowColor.withOpacity(opacity), _shadowElevation, true);
canvas.drawPath(path, Paint()..color = _tapColor.withOpacity(opacity));
}
}
}
class _RecordedTap {
_RecordedTap(this.localPosition, this.animation);
final Offset localPosition;
final Animation<double> animation;
}
/// Ticker provider that does not perform any diagnostics.
///
/// We trust that the [_RenderTapRecorder] instance will dispose all tickers
/// by disposing the animation controllers.
mixin _SilentTickerProvider implements TickerProvider {
@override
Ticker createTicker(TickerCallback onTick) => Ticker(onTick);
}

@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../theme.dart';
class AddImage extends StatefulWidget {
const AddImage({
super.key,
required this.onTap,
this.height = 100,
this.width = 100,
});
final VoidCallback onTap;
final double height;
final double width;
@override
State<AddImage> createState() => _AddImageState();
}
class _AddImageState extends State<AddImage> {
bool hovered = false;
bool tappedDown = false;
Color get buttonColor {
var state = (hovered, tappedDown);
return switch (state) {
// tapped down state
(_, true) => MarketplaceTheme.secondary.withOpacity(.7),
// hovered
(true, _) => MarketplaceTheme.secondary.withOpacity(.3),
// base color
(_, _) => MarketplaceTheme.secondary.withOpacity(.3),
};
}
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (event) {
setState(() {
hovered = true;
});
},
onExit: (event) {
setState(() {
hovered = false;
});
},
child: GestureDetector(
onTapDown: (details) {
setState(() {
tappedDown = true;
});
},
onTapUp: (details) {
setState(() {
tappedDown = false;
});
widget.onTap();
},
child: SizedBox(
width: widget.width,
height: widget.height,
child: ClipRRect(
borderRadius:
BorderRadius.circular(MarketplaceTheme.defaultBorderRadius),
child: Container(
decoration: BoxDecoration(
color: buttonColor,
),
child: const Center(
child: Icon(
Symbols.add_photo_alternate_rounded,
size: 32,
),
),
),
),
),
),
);
}
}

@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
class AppBarShapeBorder extends ShapeBorder {
final double radius;
const AppBarShapeBorder(this.radius);
@override
EdgeInsetsGeometry get dimensions => EdgeInsets.zero;
@override
Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
return Path(); // Define inner path if needed
}
@override
Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
// Define your custom shape path here
Path path = Path();
path.moveTo(rect.left, rect.top);
path.lineTo(rect.left, rect.bottom - (radius * 2));
path.quadraticBezierTo(
rect.left,
rect.bottom - radius,
rect.left + radius,
rect.bottom - radius,
);
path.lineTo(rect.right - radius, rect.bottom - radius);
path.quadraticBezierTo(
rect.right, rect.bottom - radius, rect.right, rect.bottom);
path.lineTo(rect.right, rect.top);
path.close();
return path;
}
@override
void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {
// Define your painting logic here
Paint paint = Paint()..color = Colors.transparent;
canvas.drawPath(getOuterPath(rect), paint);
}
@override
ShapeBorder scale(double t) {
// Implement scaling if needed
return this;
}
}

@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
class BottomBarShapeBorder extends ShapeBorder {
final double radius;
const BottomBarShapeBorder(this.radius);
@override
EdgeInsetsGeometry get dimensions => EdgeInsets.zero;
@override
Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
return Path(); // Define inner path if needed
}
@override
Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
// Define your custom shape path here
Path path = Path();
path.moveTo(rect.left, rect.top - radius);
path.quadraticBezierTo(
rect.left,
rect.top,
rect.left + radius,
rect.top,
);
path.lineTo(rect.right - radius, rect.top);
path.quadraticBezierTo(
rect.right,
rect.top,
rect.right,
rect.bottom,
);
path.lineTo(rect.left, rect.bottom);
path.lineTo(rect.left, rect.top + radius);
path.close();
return path;
}
@override
void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {
// Define your painting logic here
Paint paint = Paint()..color = Colors.transparent;
canvas.drawPath(getOuterPath(rect), paint);
}
@override
ShapeBorder scale(double t) {
// Implement scaling if needed
return this;
}
}

@ -0,0 +1,41 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
class CrossImage extends StatelessWidget {
const CrossImage({
super.key,
required this.file,
this.fit = BoxFit.cover,
this.height = 100,
this.width = 100,
});
final XFile file;
final BoxFit fit;
final double width;
final double height;
@override
Widget build(BuildContext context) {
if (kIsWeb) {
return Image.network(
file.path,
fit: fit,
);
} else {
return Image.file(
File(file.path),
height: height,
width: width,
);
}
}
static DecorationImage decoration(XFile file, {BoxFit fit = BoxFit.cover}) {
final image = kIsWeb ? NetworkImage(file.path) : FileImage(File(file.path));
return DecorationImage(image: image as ImageProvider, fit: fit);
}
}

@ -0,0 +1,89 @@
import 'package:ai_recipe_generation/theme.dart';
import 'package:ai_recipe_generation/util/extensions.dart';
import 'package:ai_recipe_generation/util/filter_chip_enums.dart';
import 'package:flutter/material.dart';
class FilterChipSelectionInput<T extends Enum> extends StatefulWidget {
const FilterChipSelectionInput({
super.key,
required this.onChipSelected,
required this.selectedValues,
required this.allValues,
});
final Null Function(Set) onChipSelected;
final Set<T> selectedValues;
final List<T> allValues;
@override
State<FilterChipSelectionInput> createState() =>
_CategorySelectionInputState<T>();
}
class _CategorySelectionInputState<T extends Enum>
extends State<FilterChipSelectionInput> {
bool isExpanded = false;
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
return Theme(
data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
child: Wrap(
spacing: 5.0,
runSpacing: constraints.isMobile ? 5.0 : -5.0,
children: List<Widget>.generate(
widget.allValues.length,
(idx) {
final chipData = widget.allValues[idx];
String label(dynamic chipData) {
if (chipData is CuisineFilter) {
return cuisineReadable(chipData);
} else if (chipData is DietaryRestrictionsFilter) {
return dietaryRestrictionReadable(chipData);
} else if (chipData is BasicIngredientsFilter) {
return chipData.name;
} else {
throw "unknown enum";
}
}
return FilterChip(
color: WidgetStateColor.resolveWith((states) {
if (states.contains(WidgetState.hovered)) {
return MarketplaceTheme.secondary.withOpacity(.5);
}
if (states.contains(WidgetState.selected)) {
return MarketplaceTheme.secondary.withOpacity(.3);
}
return Theme.of(context).splashColor;
}),
surfaceTintColor: Colors.transparent,
shadowColor: Colors.transparent,
backgroundColor: Colors.transparent,
padding: const EdgeInsets.all(4),
label: Text(
label(chipData),
style: MarketplaceTheme.dossierParagraph,
),
selected: widget.selectedValues.contains(chipData),
onSelected: (selected) {
setState(
() {
if (selected) {
widget.selectedValues.add(chipData as T);
} else {
widget.selectedValues.remove(chipData);
}
widget.onChipSelected(widget.selectedValues);
},
);
},
);
},
).toList(),
),
);
});
}
}

@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import '../theme.dart';
class HighlightBorderOnHoverWidget extends StatefulWidget {
const HighlightBorderOnHoverWidget({
super.key,
required this.child,
this.color = MarketplaceTheme.secondary,
required this.borderRadius,
});
final Widget child;
final Color color;
final BorderRadius borderRadius;
@override
State<HighlightBorderOnHoverWidget> createState() =>
_HighlightBorderOnHoverWidgetState();
}
class _HighlightBorderOnHoverWidgetState
extends State<HighlightBorderOnHoverWidget> {
bool hovered = false;
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (event) {
setState(() {
hovered = true;
});
},
onExit: (event) {
setState(() {
hovered = false;
});
},
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).splashColor.withOpacity(.1),
border: Border.all(
color: hovered ? widget.color : MarketplaceTheme.borderColor,
),
borderRadius: widget.borderRadius,
),
child: widget.child,
),
);
}
}

@ -0,0 +1,98 @@
import 'dart:async';
import 'dart:math';
import 'package:ai_recipe_generation/theme.dart';
import 'package:flutter/material.dart';
class IconLoadingAnimator extends StatefulWidget {
IconLoadingAnimator({
super.key,
required this.icons,
this.animationDuration,
this.millisecondsBetweenAnimations,
});
final List<IconData> icons;
final Duration? animationDuration;
final int? millisecondsBetweenAnimations;
final List<Color> colors = [
MarketplaceTheme.primary,
MarketplaceTheme.secondary,
MarketplaceTheme.tertiary,
MarketplaceTheme.scrim,
Colors.black87,
];
@override
State<IconLoadingAnimator> createState() => _IconLoadingAnimatorState();
}
var rand = Random();
class _IconLoadingAnimatorState extends State<IconLoadingAnimator> {
late List<IconData> notYetSeenIcons;
late IconData currentIcon;
late Color currentColor;
late Timer timer;
@override
void initState() {
super.initState();
notYetSeenIcons = widget.icons;
currentIcon =
notYetSeenIcons.removeAt(rand.nextInt(notYetSeenIcons.length));
currentColor = widget.colors[rand.nextInt(widget.colors.length)];
timer = Timer.periodic(
Duration(milliseconds: widget.millisecondsBetweenAnimations ?? 1000),
(timer) {
nextIcon();
},
);
}
void nextIcon() {
if (notYetSeenIcons.length == 1) notYetSeenIcons = widget.icons;
setState(() {
currentIcon =
notYetSeenIcons.removeAt(rand.nextInt(notYetSeenIcons.length));
currentColor = widget.colors[rand.nextInt(widget.colors.length)];
});
}
@override
void dispose() {
timer.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
border: Border.all(
color: MarketplaceTheme.focusedBorderColor,
width: 2,
),
),
child: AnimatedSwitcher(
duration: widget.animationDuration ?? const Duration(milliseconds: 200),
transitionBuilder: (child, animation) {
return ScaleTransition(
scale: animation,
child: child,
);
},
child: Icon(
size: 75,
color: currentColor,
key: Key(currentIcon.hashCode.toString()),
currentIcon,
),
),
);
}
}

@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import '../theme.dart';
class MarketplaceButton extends StatefulWidget {
const MarketplaceButton({
super.key,
required this.onPressed,
required this.buttonText,
required this.icon,
this.iconRotateAngle,
this.iconBackgroundColor,
this.iconColor,
this.buttonBackgroundColor,
this.hoverColor,
});
final VoidCallback? onPressed;
final String buttonText;
final IconData icon;
final double? iconRotateAngle;
final Color? iconBackgroundColor;
final Color? iconColor;
final Color? buttonBackgroundColor;
final Color? hoverColor;
@override
State<MarketplaceButton> createState() => _MarketplaceButtonState();
}
class _MarketplaceButtonState extends State<MarketplaceButton> {
@override
Widget build(BuildContext context) {
return TextButton.icon(
icon: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: widget.iconBackgroundColor ?? Colors.transparent,
),
child: Transform.rotate(
angle: widget.iconRotateAngle ?? 0,
child: Icon(
widget.icon,
color: widget.iconColor ?? Colors.black87,
size: 20.0,
),
),
),
label: Text(
widget.buttonText,
style: MarketplaceTheme.dossierParagraph,
),
onPressed: widget.onPressed,
style: ButtonStyle(
backgroundColor: WidgetStateColor.resolveWith((states) {
if (states.contains(WidgetState.hovered)) {
return widget.hoverColor ??
MarketplaceTheme.secondary.withOpacity(.3);
}
return widget.buttonBackgroundColor ??
Theme.of(context).splashColor.withOpacity(.3);
}),
shape: WidgetStateProperty.resolveWith(
(states) {
if (states.contains(WidgetState.hovered)) {
// TODO: how can I animate between states?
}
return const RoundedRectangleBorder(
side: BorderSide(color: Colors.black26),
borderRadius: BorderRadius.all(
Radius.circular(MarketplaceTheme.defaultBorderRadius),
),
);
},
),
textStyle: WidgetStateTextStyle.resolveWith(
(states) {
return MarketplaceTheme.dossierParagraph.copyWith(
color: Colors.black45,
);
},
),
),
);
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save