diff --git a/commis/.firebaserc b/commis/.firebaserc
new file mode 100644
index 00000000..5116ebbe
--- /dev/null
+++ b/commis/.firebaserc
@@ -0,0 +1,5 @@
+{
+ "projects": {
+ "default": "YOUR_PROJECT_ID"
+ }
+}
diff --git a/commis/README.md b/commis/README.md
new file mode 100644
index 00000000..0e3afc5d
--- /dev/null
+++ b/commis/README.md
@@ -0,0 +1,66 @@
+# Commis Catering Assistant
+
+An AI-powered catering assistant showcasing how to build interactive, stateful **Generative UI (GenUI)** utilizing Flutter, Cloud Firestore, and Dart Cloud Functions.
+
+This repository is organized as a monorepo containing both the mobile/tablet client application and the serverless backend triggers.
+
+```mermaid
+graph TD
+ A[Flutter App] -->|Reads / Writes| B[(Cloud Firestore)]
+ A -->|Direct Conversation| C[GenUI Core/Renderer]
+ C -->|Interactive State| A
+ D[Dart Cloud Functions] <-->|onDocumentWritten trigger| B
+ D -->|Generates UI JSON| B
+```
+
+---
+
+## Project Structure
+
+This project is divided into two primary workspaces:
+
+1. **[Flutter Application (app/)](file:///Users/redbrogdon/source/commis/app/README.md)**: The Flutter frontend application displaying upcoming catering jobs, menus, ingredient listings, and hosting the interactive AI Culinary Assistant chat interface.
+2. **[Cloud Functions (functions/)](file:///Users/redbrogdon/source/commis/functions/README.md)**: The serverless backend codebase written in Dart. This includes Firestore triggers that monitor job updates to automatically generate and cache recommended UI cards using Gemini.
+
+Refer to the individual `README.md` files linked above for specific setup and configuration details for each component.
+
+---
+
+## Quick Start (Local Development)
+
+To run the full stack locally using the Firebase Emulator Suite, perform the following steps:
+
+### 1. Prerequisites
+- [Flutter SDK](https://docs.flutter.dev/get-started/install)
+- [Firebase CLI](https://firebase.google.com/docs/cli) (v15.15.0 or higher is required for Dart support)
+- [FlutterFire CLI](https://firebase.google.com/docs/flutter/setup)
+
+### 2. Enable Dart Functions support
+```bash
+firebase experiments:enable dartfunctions
+```
+
+### 3. Start the Emulators
+From the root of the workspace, start the Firebase emulator:
+```bash
+firebase emulators:start
+```
+
+### 4. Seed the Database
+While the emulator is running, open a new terminal window to seed Firestore with the mock catering dataset:
+```bash
+export FIRESTORE_EMULATOR_HOST=localhost:8080
+cd functions
+dart run bin/seed_ingredients.dart
+dart run bin/seed_recipes.dart
+dart run bin/seed_jobs.dart
+```
+
+### 5. Run the Flutter App
+From the `app/` directory, configure your local Firebase app and run:
+```bash
+cd app
+flutterfire configure
+flutter run
+```
+*(In debug mode, the app automatically detects and routes traffic to the local Firestore and Functions emulators).*
diff --git a/commis/app/.gitignore b/commis/app/.gitignore
new file mode 100644
index 00000000..660568a2
--- /dev/null
+++ b/commis/app/.gitignore
@@ -0,0 +1,49 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.build/
+.buildlog/
+.history
+.svn/
+.swiftpm/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins-dependencies
+.pub-cache/
+.pub/
+/build/
+/coverage/
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release
+
+# Firebase configuration files (generated by FlutterFire CLI)
+google-services.json
+GoogleService-Info.plist
diff --git a/commis/app/.metadata b/commis/app/.metadata
new file mode 100644
index 00000000..51d24577
--- /dev/null
+++ b/commis/app/.metadata
@@ -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: "e1fd963c6f6922bd32afde2e9698a363cd0406d2"
+ channel: "stable"
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: e1fd963c6f6922bd32afde2e9698a363cd0406d2
+ base_revision: e1fd963c6f6922bd32afde2e9698a363cd0406d2
+ - platform: android
+ create_revision: e1fd963c6f6922bd32afde2e9698a363cd0406d2
+ base_revision: e1fd963c6f6922bd32afde2e9698a363cd0406d2
+ - platform: ios
+ create_revision: e1fd963c6f6922bd32afde2e9698a363cd0406d2
+ base_revision: e1fd963c6f6922bd32afde2e9698a363cd0406d2
+ - platform: linux
+ create_revision: e1fd963c6f6922bd32afde2e9698a363cd0406d2
+ base_revision: e1fd963c6f6922bd32afde2e9698a363cd0406d2
+ - platform: macos
+ create_revision: e1fd963c6f6922bd32afde2e9698a363cd0406d2
+ base_revision: e1fd963c6f6922bd32afde2e9698a363cd0406d2
+ - platform: web
+ create_revision: e1fd963c6f6922bd32afde2e9698a363cd0406d2
+ base_revision: e1fd963c6f6922bd32afde2e9698a363cd0406d2
+ - platform: windows
+ create_revision: e1fd963c6f6922bd32afde2e9698a363cd0406d2
+ base_revision: e1fd963c6f6922bd32afde2e9698a363cd0406d2
+
+ # 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'
diff --git a/commis/app/README.md b/commis/app/README.md
new file mode 100644
index 00000000..b1f716fb
--- /dev/null
+++ b/commis/app/README.md
@@ -0,0 +1,54 @@
+# Commis Catering Assistant - Flutter Application
+
+This directory contains the Flutter frontend mobile/tablet client application.
+
+## Features
+- **GenUI Integration**: Instantly renders stateful widgets from AI-generated A2UI JSON payloads.
+- **AI Culinary Assistant Workspace**: Interactive chat interface for planning menus, managing shopping checklists, and requesting ingredient estimations.
+- **Catering Job Pipeline**: Interactive dashboards tracking the statuses of upcoming jobs, with inline warning cards and quick action triggers.
+
+---
+
+## Getting Started
+
+### Prerequisites
+- [Flutter SDK](https://docs.flutter.dev/get-started/install)
+- [FlutterFire CLI](https://firebase.google.com/docs/flutter/setup)
+
+### Firebase Config Setup
+Before running the application, you must link it to your own Firebase project.
+1. Run the FlutterFire CLI from this directory to generate your platform configuration files:
+ ```bash
+ flutterfire configure
+ ```
+ This generates a local [firebase_options.dart](file:///Users/redbrogdon/source/commis/app/lib/firebase_options.dart) file, as well as the platform-specific configuration files (`google-services.json` and `GoogleService-Info.plist`) which are gitignored.
+
+### Running the App
+- **Local Emulator Mode (Default)**: In debug mode, the app automatically routing requests to `localhost:8080` (Firestore) and `localhost:5001` (Functions). Simply start your Firebase Emulator Suite in the root directory and run:
+ ```bash
+ flutter run
+ ```
+- **Production Mode**: To override emulator routing and connect directly to your live production Firebase services, run:
+ ```bash
+ flutter run --dart-define=USE_EMULATORS=false
+ ```
+
+---
+
+## Generating the Catalog Prompt
+
+The application uses a test script to compile the widget catalog specifications into a system prompt format for Gemini. Because this imports Flutter UI dependencies, it must be run within the Flutter test runner environment.
+
+To regenerate the `catalog_prompt.txt` file, run:
+```bash
+flutter test test/generate_prompt_test.dart
+```
+This parses the catalog defined in [catalog.dart](file:///Users/redbrogdon/source/commis/app/lib/ui/catalog/catalog.dart) and saves the output to `catalog_prompt.txt`.
+
+---
+
+## Verification & Testing
+To run the project's test suite, execute:
+```bash
+flutter test
+```
diff --git a/commis/app/analysis_options.yaml b/commis/app/analysis_options.yaml
new file mode 100644
index 00000000..32ec5359
--- /dev/null
+++ b/commis/app/analysis_options.yaml
@@ -0,0 +1,9 @@
+include: package:flutter_lints/flutter.yaml
+
+analyzer:
+ exclude:
+ - build/**
+
+linter:
+ rules:
+ directives_ordering: true
diff --git a/commis/app/android/.gitignore b/commis/app/android/.gitignore
new file mode 100644
index 00000000..be3943c9
--- /dev/null
+++ b/commis/app/android/.gitignore
@@ -0,0 +1,14 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java
+.cxx/
+
+# Remember to never publicly share your keystore.
+# See https://flutter.dev/to/reference-keystore
+key.properties
+**/*.keystore
+**/*.jks
diff --git a/commis/app/android/app/build.gradle.kts b/commis/app/android/app/build.gradle.kts
new file mode 100644
index 00000000..7590f01d
--- /dev/null
+++ b/commis/app/android/app/build.gradle.kts
@@ -0,0 +1,48 @@
+plugins {
+ id("com.android.application")
+ // START: FlutterFire Configuration
+ id("com.google.gms.google-services")
+ // END: FlutterFire Configuration
+ // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
+ id("dev.flutter.flutter-gradle-plugin")
+}
+
+android {
+ namespace = "com.example.commis"
+ compileSdk = flutter.compileSdkVersion
+ ndkVersion = flutter.ndkVersion
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ defaultConfig {
+ // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+ applicationId = "com.example.commis"
+ // You can update the following values to match your application needs.
+ // For more information, see: https://flutter.dev/to/review-gradle-config.
+ minSdk = flutter.minSdkVersion
+ targetSdk = flutter.targetSdkVersion
+ versionCode = flutter.versionCode
+ versionName = flutter.versionName
+ }
+
+ buildTypes {
+ release {
+ // TODO: Add your own signing config for the release build.
+ // Signing with the debug keys for now, so `flutter run --release` works.
+ signingConfig = signingConfigs.getByName("debug")
+ }
+ }
+}
+
+kotlin {
+ compilerOptions {
+ jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
+ }
+}
+
+flutter {
+ source = "../.."
+}
diff --git a/commis/app/android/app/src/debug/AndroidManifest.xml b/commis/app/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 00000000..399f6981
--- /dev/null
+++ b/commis/app/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/commis/app/android/app/src/main/AndroidManifest.xml b/commis/app/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..2d4ee5f8
--- /dev/null
+++ b/commis/app/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/commis/app/android/app/src/main/kotlin/com/example/commis/MainActivity.kt b/commis/app/android/app/src/main/kotlin/com/example/commis/MainActivity.kt
new file mode 100644
index 00000000..7d9fe3d1
--- /dev/null
+++ b/commis/app/android/app/src/main/kotlin/com/example/commis/MainActivity.kt
@@ -0,0 +1,5 @@
+package com.example.commis
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity : FlutterActivity()
diff --git a/commis/app/android/app/src/main/res/drawable-v21/launch_background.xml b/commis/app/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 00000000..f74085f3
--- /dev/null
+++ b/commis/app/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/commis/app/android/app/src/main/res/drawable/launch_background.xml b/commis/app/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 00000000..304732f8
--- /dev/null
+++ b/commis/app/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/commis/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/commis/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..db77bb4b
Binary files /dev/null and b/commis/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/commis/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/commis/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..17987b79
Binary files /dev/null and b/commis/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/commis/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/commis/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..09d43914
Binary files /dev/null and b/commis/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/commis/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/commis/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..d5f1c8d3
Binary files /dev/null and b/commis/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/commis/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/commis/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..4d6372ee
Binary files /dev/null and b/commis/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/commis/app/android/app/src/main/res/values-night/styles.xml b/commis/app/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 00000000..06952be7
--- /dev/null
+++ b/commis/app/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/commis/app/android/app/src/main/res/values/styles.xml b/commis/app/android/app/src/main/res/values/styles.xml
new file mode 100644
index 00000000..cb1ef880
--- /dev/null
+++ b/commis/app/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/commis/app/android/app/src/profile/AndroidManifest.xml b/commis/app/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 00000000..399f6981
--- /dev/null
+++ b/commis/app/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/commis/app/android/build.gradle.kts b/commis/app/android/build.gradle.kts
new file mode 100644
index 00000000..dbee657b
--- /dev/null
+++ b/commis/app/android/build.gradle.kts
@@ -0,0 +1,24 @@
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+val newBuildDir: Directory =
+ rootProject.layout.buildDirectory
+ .dir("../../build")
+ .get()
+rootProject.layout.buildDirectory.value(newBuildDir)
+
+subprojects {
+ val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
+ project.layout.buildDirectory.value(newSubprojectBuildDir)
+}
+subprojects {
+ project.evaluationDependsOn(":app")
+}
+
+tasks.register("clean") {
+ delete(rootProject.layout.buildDirectory)
+}
diff --git a/commis/app/android/gradle.properties b/commis/app/android/gradle.properties
new file mode 100644
index 00000000..e96108cf
--- /dev/null
+++ b/commis/app/android/gradle.properties
@@ -0,0 +1,6 @@
+org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
+android.useAndroidX=true
+# This newDsl flag was added by the Flutter template
+android.newDsl=false
+# This builtInKotlin flag was added by the Flutter template
+android.builtInKotlin=false
diff --git a/commis/app/android/gradle/wrapper/gradle-wrapper.properties b/commis/app/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..2d428bfb
--- /dev/null
+++ b/commis/app/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip
diff --git a/commis/app/android/settings.gradle.kts b/commis/app/android/settings.gradle.kts
new file mode 100644
index 00000000..1fb0fc6e
--- /dev/null
+++ b/commis/app/android/settings.gradle.kts
@@ -0,0 +1,29 @@
+pluginManagement {
+ val flutterSdkPath =
+ run {
+ val properties = java.util.Properties()
+ file("local.properties").inputStream().use { properties.load(it) }
+ val flutterSdkPath = properties.getProperty("flutter.sdk")
+ require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
+ flutterSdkPath
+ }
+
+ includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
+
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
+plugins {
+ id("dev.flutter.flutter-plugin-loader") version "1.0.0"
+ id("com.android.application") version "9.0.1" apply false
+ // START: FlutterFire Configuration
+ id("com.google.gms.google-services") version("4.4.4") apply false
+ // END: FlutterFire Configuration
+ id("org.jetbrains.kotlin.android") version "2.3.20" apply false
+}
+
+include(":app")
diff --git a/commis/app/assets/images/ingredients/200/active_dry_yeast.png b/commis/app/assets/images/ingredients/200/active_dry_yeast.png
new file mode 100644
index 00000000..98a11906
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/active_dry_yeast.png differ
diff --git a/commis/app/assets/images/ingredients/200/all_purpose_flour.png b/commis/app/assets/images/ingredients/200/all_purpose_flour.png
new file mode 100644
index 00000000..bb5a75c1
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/all_purpose_flour.png differ
diff --git a/commis/app/assets/images/ingredients/200/baby_spinach.png b/commis/app/assets/images/ingredients/200/baby_spinach.png
new file mode 100644
index 00000000..05c77ff4
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/baby_spinach.png differ
diff --git a/commis/app/assets/images/ingredients/200/baking_powder.png b/commis/app/assets/images/ingredients/200/baking_powder.png
new file mode 100644
index 00000000..88f34cc9
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/baking_powder.png differ
diff --git a/commis/app/assets/images/ingredients/200/baking_soda.png b/commis/app/assets/images/ingredients/200/baking_soda.png
new file mode 100644
index 00000000..319d5ad3
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/baking_soda.png differ
diff --git a/commis/app/assets/images/ingredients/200/balsamic_vinegar.png b/commis/app/assets/images/ingredients/200/balsamic_vinegar.png
new file mode 100644
index 00000000..8d537e4e
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/balsamic_vinegar.png differ
diff --git a/commis/app/assets/images/ingredients/200/bay_leaves.png b/commis/app/assets/images/ingredients/200/bay_leaves.png
new file mode 100644
index 00000000..e619ca28
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/bay_leaves.png differ
diff --git a/commis/app/assets/images/ingredients/200/beef_stock.png b/commis/app/assets/images/ingredients/200/beef_stock.png
new file mode 100644
index 00000000..d114da7a
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/beef_stock.png differ
diff --git a/commis/app/assets/images/ingredients/200/black_beans.png b/commis/app/assets/images/ingredients/200/black_beans.png
new file mode 100644
index 00000000..0df166ee
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/black_beans.png differ
diff --git a/commis/app/assets/images/ingredients/200/black_peppercorns.png b/commis/app/assets/images/ingredients/200/black_peppercorns.png
new file mode 100644
index 00000000..7216b44c
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/black_peppercorns.png differ
diff --git a/commis/app/assets/images/ingredients/200/brown_sugar_dark.png b/commis/app/assets/images/ingredients/200/brown_sugar_dark.png
new file mode 100644
index 00000000..643cdd07
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/brown_sugar_dark.png differ
diff --git a/commis/app/assets/images/ingredients/200/canned_crushed_tomatoes.png b/commis/app/assets/images/ingredients/200/canned_crushed_tomatoes.png
new file mode 100644
index 00000000..5161b615
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/canned_crushed_tomatoes.png differ
diff --git a/commis/app/assets/images/ingredients/200/canola_oil.png b/commis/app/assets/images/ingredients/200/canola_oil.png
new file mode 100644
index 00000000..ecb5d642
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/canola_oil.png differ
diff --git a/commis/app/assets/images/ingredients/200/canola_oil_spray.png b/commis/app/assets/images/ingredients/200/canola_oil_spray.png
new file mode 100644
index 00000000..4c4c6be6
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/canola_oil_spray.png differ
diff --git a/commis/app/assets/images/ingredients/200/carrots.png b/commis/app/assets/images/ingredients/200/carrots.png
new file mode 100644
index 00000000..4ac081ad
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/carrots.png differ
diff --git a/commis/app/assets/images/ingredients/200/cayenne_pepper.png b/commis/app/assets/images/ingredients/200/cayenne_pepper.png
new file mode 100644
index 00000000..c3fec40a
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/cayenne_pepper.png differ
diff --git a/commis/app/assets/images/ingredients/200/celery_stalks.png b/commis/app/assets/images/ingredients/200/celery_stalks.png
new file mode 100644
index 00000000..b04bdc4d
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/celery_stalks.png differ
diff --git a/commis/app/assets/images/ingredients/200/cheddar_block.png b/commis/app/assets/images/ingredients/200/cheddar_block.png
new file mode 100644
index 00000000..24bf552c
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/cheddar_block.png differ
diff --git a/commis/app/assets/images/ingredients/200/chicken_breast.png b/commis/app/assets/images/ingredients/200/chicken_breast.png
new file mode 100644
index 00000000..627c4d18
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/chicken_breast.png differ
diff --git a/commis/app/assets/images/ingredients/200/chicken_stock.png b/commis/app/assets/images/ingredients/200/chicken_stock.png
new file mode 100644
index 00000000..d1278b97
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/chicken_stock.png differ
diff --git a/commis/app/assets/images/ingredients/200/corn_starch.png b/commis/app/assets/images/ingredients/200/corn_starch.png
new file mode 100644
index 00000000..273e1892
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/corn_starch.png differ
diff --git a/commis/app/assets/images/ingredients/200/corn_tortillas.png b/commis/app/assets/images/ingredients/200/corn_tortillas.png
new file mode 100644
index 00000000..923cdc69
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/corn_tortillas.png differ
diff --git a/commis/app/assets/images/ingredients/200/cream_cheese.png b/commis/app/assets/images/ingredients/200/cream_cheese.png
new file mode 100644
index 00000000..81e3dc88
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/cream_cheese.png differ
diff --git a/commis/app/assets/images/ingredients/200/cremini_mushrooms.png b/commis/app/assets/images/ingredients/200/cremini_mushrooms.png
new file mode 100644
index 00000000..500b8db6
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/cremini_mushrooms.png differ
diff --git a/commis/app/assets/images/ingredients/200/crushed_red_pepper.png b/commis/app/assets/images/ingredients/200/crushed_red_pepper.png
new file mode 100644
index 00000000..bea51e74
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/crushed_red_pepper.png differ
diff --git a/commis/app/assets/images/ingredients/200/dijon_mustard.png b/commis/app/assets/images/ingredients/200/dijon_mustard.png
new file mode 100644
index 00000000..b5eb449d
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/dijon_mustard.png differ
diff --git a/commis/app/assets/images/ingredients/200/dried_oregano.png b/commis/app/assets/images/ingredients/200/dried_oregano.png
new file mode 100644
index 00000000..b91b2d63
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/dried_oregano.png differ
diff --git a/commis/app/assets/images/ingredients/200/dried_rosemary.png b/commis/app/assets/images/ingredients/200/dried_rosemary.png
new file mode 100644
index 00000000..737f08df
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/dried_rosemary.png differ
diff --git a/commis/app/assets/images/ingredients/200/dried_thyme.png b/commis/app/assets/images/ingredients/200/dried_thyme.png
new file mode 100644
index 00000000..8558239e
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/dried_thyme.png differ
diff --git a/commis/app/assets/images/ingredients/200/firm_tofu.png b/commis/app/assets/images/ingredients/200/firm_tofu.png
new file mode 100644
index 00000000..94a01e57
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/firm_tofu.png differ
diff --git a/commis/app/assets/images/ingredients/200/flank_steak.png b/commis/app/assets/images/ingredients/200/flank_steak.png
new file mode 100644
index 00000000..7e768992
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/flank_steak.png differ
diff --git a/commis/app/assets/images/ingredients/200/flour_tortillas.png b/commis/app/assets/images/ingredients/200/flour_tortillas.png
new file mode 100644
index 00000000..d753417d
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/flour_tortillas.png differ
diff --git a/commis/app/assets/images/ingredients/200/fresh_basil.png b/commis/app/assets/images/ingredients/200/fresh_basil.png
new file mode 100644
index 00000000..9e833551
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/fresh_basil.png differ
diff --git a/commis/app/assets/images/ingredients/200/fresh_cilantro.png b/commis/app/assets/images/ingredients/200/fresh_cilantro.png
new file mode 100644
index 00000000..f601114f
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/fresh_cilantro.png differ
diff --git a/commis/app/assets/images/ingredients/200/fresh_lemons.png b/commis/app/assets/images/ingredients/200/fresh_lemons.png
new file mode 100644
index 00000000..f1f0e52e
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/fresh_lemons.png differ
diff --git a/commis/app/assets/images/ingredients/200/fresh_limes.png b/commis/app/assets/images/ingredients/200/fresh_limes.png
new file mode 100644
index 00000000..9dd817a9
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/fresh_limes.png differ
diff --git a/commis/app/assets/images/ingredients/200/fresh_mint.png b/commis/app/assets/images/ingredients/200/fresh_mint.png
new file mode 100644
index 00000000..b3398bbf
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/fresh_mint.png differ
diff --git a/commis/app/assets/images/ingredients/200/fresh_parsley.png b/commis/app/assets/images/ingredients/200/fresh_parsley.png
new file mode 100644
index 00000000..c2609183
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/fresh_parsley.png differ
diff --git a/commis/app/assets/images/ingredients/200/fresh_strawberries.png b/commis/app/assets/images/ingredients/200/fresh_strawberries.png
new file mode 100644
index 00000000..f60bc968
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/fresh_strawberries.png differ
diff --git a/commis/app/assets/images/ingredients/200/fuji_apples.png b/commis/app/assets/images/ingredients/200/fuji_apples.png
new file mode 100644
index 00000000..231e40a9
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/fuji_apples.png differ
diff --git a/commis/app/assets/images/ingredients/200/garlic_bulbs.png b/commis/app/assets/images/ingredients/200/garlic_bulbs.png
new file mode 100644
index 00000000..8c0129c5
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/garlic_bulbs.png differ
diff --git a/commis/app/assets/images/ingredients/200/ginger_root.png b/commis/app/assets/images/ingredients/200/ginger_root.png
new file mode 100644
index 00000000..d20b3bd0
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/ginger_root.png differ
diff --git a/commis/app/assets/images/ingredients/200/goat_cheese_logs.png b/commis/app/assets/images/ingredients/200/goat_cheese_logs.png
new file mode 100644
index 00000000..7b7154c1
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/goat_cheese_logs.png differ
diff --git a/commis/app/assets/images/ingredients/200/granulated_sugar.png b/commis/app/assets/images/ingredients/200/granulated_sugar.png
new file mode 100644
index 00000000..2530cd2b
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/granulated_sugar.png differ
diff --git a/commis/app/assets/images/ingredients/200/greek_yogurt.png b/commis/app/assets/images/ingredients/200/greek_yogurt.png
new file mode 100644
index 00000000..d6fc5f09
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/greek_yogurt.png differ
diff --git a/commis/app/assets/images/ingredients/200/green_bell_peppers.png b/commis/app/assets/images/ingredients/200/green_bell_peppers.png
new file mode 100644
index 00000000..26ac6686
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/green_bell_peppers.png differ
diff --git a/commis/app/assets/images/ingredients/200/ground_beef.png b/commis/app/assets/images/ingredients/200/ground_beef.png
new file mode 100644
index 00000000..317b29ec
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/ground_beef.png differ
diff --git a/commis/app/assets/images/ingredients/200/ground_cinnamon.png b/commis/app/assets/images/ingredients/200/ground_cinnamon.png
new file mode 100644
index 00000000..789a7f09
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/ground_cinnamon.png differ
diff --git a/commis/app/assets/images/ingredients/200/ground_cumin.png b/commis/app/assets/images/ingredients/200/ground_cumin.png
new file mode 100644
index 00000000..94dedc26
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/ground_cumin.png differ
diff --git a/commis/app/assets/images/ingredients/200/ground_nutmeg.png b/commis/app/assets/images/ingredients/200/ground_nutmeg.png
new file mode 100644
index 00000000..35a50abb
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/ground_nutmeg.png differ
diff --git a/commis/app/assets/images/ingredients/200/hass_avocados.png b/commis/app/assets/images/ingredients/200/hass_avocados.png
new file mode 100644
index 00000000..5a9b42ee
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/hass_avocados.png differ
diff --git a/commis/app/assets/images/ingredients/200/heavy_whipping_cream.png b/commis/app/assets/images/ingredients/200/heavy_whipping_cream.png
new file mode 100644
index 00000000..e171cadf
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/heavy_whipping_cream.png differ
diff --git a/commis/app/assets/images/ingredients/200/jasmine_rice.png b/commis/app/assets/images/ingredients/200/jasmine_rice.png
new file mode 100644
index 00000000..4f1047f0
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/jasmine_rice.png differ
diff --git a/commis/app/assets/images/ingredients/200/jumbo_shrimp.png b/commis/app/assets/images/ingredients/200/jumbo_shrimp.png
new file mode 100644
index 00000000..d73d3667
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/jumbo_shrimp.png differ
diff --git a/commis/app/assets/images/ingredients/200/kosher_salt.png b/commis/app/assets/images/ingredients/200/kosher_salt.png
new file mode 100644
index 00000000..c16f01ad
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/kosher_salt.png differ
diff --git a/commis/app/assets/images/ingredients/200/kosher_salt_coarse.png b/commis/app/assets/images/ingredients/200/kosher_salt_coarse.png
new file mode 100644
index 00000000..e7b37066
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/kosher_salt_coarse.png differ
diff --git a/commis/app/assets/images/ingredients/200/large_eggs.png b/commis/app/assets/images/ingredients/200/large_eggs.png
new file mode 100644
index 00000000..1474a63b
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/large_eggs.png differ
diff --git a/commis/app/assets/images/ingredients/200/maple_syrup.png b/commis/app/assets/images/ingredients/200/maple_syrup.png
new file mode 100644
index 00000000..91cd9bb9
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/maple_syrup.png differ
diff --git a/commis/app/assets/images/ingredients/200/mayonnaise.png b/commis/app/assets/images/ingredients/200/mayonnaise.png
new file mode 100644
index 00000000..1cb9b15c
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/mayonnaise.png differ
diff --git a/commis/app/assets/images/ingredients/200/micro_greens.png b/commis/app/assets/images/ingredients/200/micro_greens.png
new file mode 100644
index 00000000..90cfcc1d
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/micro_greens.png differ
diff --git a/commis/app/assets/images/ingredients/200/mozzarella_logs.png b/commis/app/assets/images/ingredients/200/mozzarella_logs.png
new file mode 100644
index 00000000..e532a090
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/mozzarella_logs.png differ
diff --git a/commis/app/assets/images/ingredients/200/olive_oil_extra_virgin.png b/commis/app/assets/images/ingredients/200/olive_oil_extra_virgin.png
new file mode 100644
index 00000000..5bf62f4b
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/olive_oil_extra_virgin.png differ
diff --git a/commis/app/assets/images/ingredients/200/onion_red.png b/commis/app/assets/images/ingredients/200/onion_red.png
new file mode 100644
index 00000000..7c70caca
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/onion_red.png differ
diff --git a/commis/app/assets/images/ingredients/200/panko_breadcrumbs.png b/commis/app/assets/images/ingredients/200/panko_breadcrumbs.png
new file mode 100644
index 00000000..9928e3d3
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/panko_breadcrumbs.png differ
diff --git a/commis/app/assets/images/ingredients/200/parmesan_cheese.png b/commis/app/assets/images/ingredients/200/parmesan_cheese.png
new file mode 100644
index 00000000..6a16a3e0
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/parmesan_cheese.png differ
diff --git a/commis/app/assets/images/ingredients/200/penne_pasta.png b/commis/app/assets/images/ingredients/200/penne_pasta.png
new file mode 100644
index 00000000..1ef50e9c
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/penne_pasta.png differ
diff --git a/commis/app/assets/images/ingredients/200/pinto_beans.png b/commis/app/assets/images/ingredients/200/pinto_beans.png
new file mode 100644
index 00000000..97df7f21
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/pinto_beans.png differ
diff --git a/commis/app/assets/images/ingredients/200/pork_shoulder.png b/commis/app/assets/images/ingredients/200/pork_shoulder.png
new file mode 100644
index 00000000..e59b69be
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/pork_shoulder.png differ
diff --git a/commis/app/assets/images/ingredients/200/red_bell_peppers.png b/commis/app/assets/images/ingredients/200/red_bell_peppers.png
new file mode 100644
index 00000000..6ebcb3b0
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/red_bell_peppers.png differ
diff --git a/commis/app/assets/images/ingredients/200/red_wine_vinegar.png b/commis/app/assets/images/ingredients/200/red_wine_vinegar.png
new file mode 100644
index 00000000..56334596
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/red_wine_vinegar.png differ
diff --git a/commis/app/assets/images/ingredients/200/roma_tomatoes.png b/commis/app/assets/images/ingredients/200/roma_tomatoes.png
new file mode 100644
index 00000000..90def68d
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/roma_tomatoes.png differ
diff --git a/commis/app/assets/images/ingredients/200/russet_potatoes.png b/commis/app/assets/images/ingredients/200/russet_potatoes.png
new file mode 100644
index 00000000..f5bf4c05
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/russet_potatoes.png differ
diff --git a/commis/app/assets/images/ingredients/200/salmon_fillets.png b/commis/app/assets/images/ingredients/200/salmon_fillets.png
new file mode 100644
index 00000000..bf69e05b
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/salmon_fillets.png differ
diff --git a/commis/app/assets/images/ingredients/200/scallions.png b/commis/app/assets/images/ingredients/200/scallions.png
new file mode 100644
index 00000000..7f695a8f
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/scallions.png differ
diff --git a/commis/app/assets/images/ingredients/200/sliced_prosciutto.png b/commis/app/assets/images/ingredients/200/sliced_prosciutto.png
new file mode 100644
index 00000000..769e26ec
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/sliced_prosciutto.png differ
diff --git a/commis/app/assets/images/ingredients/200/sour_cream.png b/commis/app/assets/images/ingredients/200/sour_cream.png
new file mode 100644
index 00000000..ffd31704
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/sour_cream.png differ
diff --git a/commis/app/assets/images/ingredients/200/soy_sauce.png b/commis/app/assets/images/ingredients/200/soy_sauce.png
new file mode 100644
index 00000000..df841131
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/soy_sauce.png differ
diff --git a/commis/app/assets/images/ingredients/200/sriracha_sauce.png b/commis/app/assets/images/ingredients/200/sriracha_sauce.png
new file mode 100644
index 00000000..40c0ba32
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/sriracha_sauce.png differ
diff --git a/commis/app/assets/images/ingredients/200/sweet_paprika.png b/commis/app/assets/images/ingredients/200/sweet_paprika.png
new file mode 100644
index 00000000..b9e4752a
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/sweet_paprika.png differ
diff --git a/commis/app/assets/images/ingredients/200/thick_bacon.png b/commis/app/assets/images/ingredients/200/thick_bacon.png
new file mode 100644
index 00000000..97bd7471
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/thick_bacon.png differ
diff --git a/commis/app/assets/images/ingredients/200/tomato_paste.png b/commis/app/assets/images/ingredients/200/tomato_paste.png
new file mode 100644
index 00000000..ea348948
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/tomato_paste.png differ
diff --git a/commis/app/assets/images/ingredients/200/unsalted_butter.png b/commis/app/assets/images/ingredients/200/unsalted_butter.png
new file mode 100644
index 00000000..08d4fe06
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/unsalted_butter.png differ
diff --git a/commis/app/assets/images/ingredients/200/vanilla_extract.png b/commis/app/assets/images/ingredients/200/vanilla_extract.png
new file mode 100644
index 00000000..83d6c01f
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/vanilla_extract.png differ
diff --git a/commis/app/assets/images/ingredients/200/vegetable_stock.png b/commis/app/assets/images/ingredients/200/vegetable_stock.png
new file mode 100644
index 00000000..fad29264
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/vegetable_stock.png differ
diff --git a/commis/app/assets/images/ingredients/200/white_quinoa.png b/commis/app/assets/images/ingredients/200/white_quinoa.png
new file mode 100644
index 00000000..7660d07b
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/white_quinoa.png differ
diff --git a/commis/app/assets/images/ingredients/200/white_sugar_powder.png b/commis/app/assets/images/ingredients/200/white_sugar_powder.png
new file mode 100644
index 00000000..1a54e4de
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/white_sugar_powder.png differ
diff --git a/commis/app/assets/images/ingredients/200/whole_milk.png b/commis/app/assets/images/ingredients/200/whole_milk.png
new file mode 100644
index 00000000..340622b5
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/whole_milk.png differ
diff --git a/commis/app/assets/images/ingredients/200/wild_arugula.png b/commis/app/assets/images/ingredients/200/wild_arugula.png
new file mode 100644
index 00000000..ab72ed27
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/wild_arugula.png differ
diff --git a/commis/app/assets/images/ingredients/200/wildflower_honey.png b/commis/app/assets/images/ingredients/200/wildflower_honey.png
new file mode 100644
index 00000000..18d7c467
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/wildflower_honey.png differ
diff --git a/commis/app/assets/images/ingredients/200/yellow_onions.png b/commis/app/assets/images/ingredients/200/yellow_onions.png
new file mode 100644
index 00000000..4db1d917
Binary files /dev/null and b/commis/app/assets/images/ingredients/200/yellow_onions.png differ
diff --git a/commis/app/assets/images/recipes/200/arugula_goat_cheese_salad.png b/commis/app/assets/images/recipes/200/arugula_goat_cheese_salad.png
new file mode 100644
index 00000000..095b74c0
Binary files /dev/null and b/commis/app/assets/images/recipes/200/arugula_goat_cheese_salad.png differ
diff --git a/commis/app/assets/images/recipes/200/caprese_skewers.png b/commis/app/assets/images/recipes/200/caprese_skewers.png
new file mode 100644
index 00000000..e95690e7
Binary files /dev/null and b/commis/app/assets/images/recipes/200/caprese_skewers.png differ
diff --git a/commis/app/assets/images/recipes/200/classic_caesar_salad.png b/commis/app/assets/images/recipes/200/classic_caesar_salad.png
new file mode 100644
index 00000000..9a3233c1
Binary files /dev/null and b/commis/app/assets/images/recipes/200/classic_caesar_salad.png differ
diff --git a/commis/app/assets/images/recipes/200/classic_tomato_basil_penne.png b/commis/app/assets/images/recipes/200/classic_tomato_basil_penne.png
new file mode 100644
index 00000000..a18e3382
Binary files /dev/null and b/commis/app/assets/images/recipes/200/classic_tomato_basil_penne.png differ
diff --git a/commis/app/assets/images/recipes/200/garlic_herb_roasted_potatoes.png b/commis/app/assets/images/recipes/200/garlic_herb_roasted_potatoes.png
new file mode 100644
index 00000000..0940c718
Binary files /dev/null and b/commis/app/assets/images/recipes/200/garlic_herb_roasted_potatoes.png differ
diff --git a/commis/app/assets/images/recipes/200/garlic_shrimp_scampi.png b/commis/app/assets/images/recipes/200/garlic_shrimp_scampi.png
new file mode 100644
index 00000000..9d625dbc
Binary files /dev/null and b/commis/app/assets/images/recipes/200/garlic_shrimp_scampi.png differ
diff --git a/commis/app/assets/images/recipes/200/pan_seared_salmon.png b/commis/app/assets/images/recipes/200/pan_seared_salmon.png
new file mode 100644
index 00000000..6bc44e55
Binary files /dev/null and b/commis/app/assets/images/recipes/200/pan_seared_salmon.png differ
diff --git a/commis/app/assets/images/recipes/200/pork_carnitas_tacos.png b/commis/app/assets/images/recipes/200/pork_carnitas_tacos.png
new file mode 100644
index 00000000..fe5beb6f
Binary files /dev/null and b/commis/app/assets/images/recipes/200/pork_carnitas_tacos.png differ
diff --git a/commis/app/assets/images/recipes/200/strawberry_shortcake.png b/commis/app/assets/images/recipes/200/strawberry_shortcake.png
new file mode 100644
index 00000000..6b3ee877
Binary files /dev/null and b/commis/app/assets/images/recipes/200/strawberry_shortcake.png differ
diff --git a/commis/app/assets/images/recipes/200/wild_mushroom_risotto.png b/commis/app/assets/images/recipes/200/wild_mushroom_risotto.png
new file mode 100644
index 00000000..fd61059e
Binary files /dev/null and b/commis/app/assets/images/recipes/200/wild_mushroom_risotto.png differ
diff --git a/commis/app/catalog_prompt.txt b/commis/app/catalog_prompt.txt
new file mode 100644
index 00000000..0136cdb0
--- /dev/null
+++ b/commis/app/catalog_prompt.txt
@@ -0,0 +1,1066 @@
+Use the provided tools to respond to user using rich UI elements.
+
+-------------------------------------
+
+The active catalog ID is: "commis_catalog". You must use this catalog ID when creating surfaces.
+
+-------------------------------------
+
+IMPORTANT: You do not have the ability to execute code. If you need to perform calculations, do them yourself.
+
+-------------------------------------
+
+IMPORTANT: You do not have the ability to use tools for UI generation.
+
+-------------------------------------
+
+IMPORTANT: You do not have the ability to use function calls for UI generation.
+
+-------------------------------------
+
+-----CONTROLLING_THE_UI_START-----
+You can control the UI by outputting valid A2UI JSON messages wrapped in markdown code blocks.
+
+Supported messages are: `createSurface`, `updateComponents`.
+
+- `createSurface`: Creates a new surface.
+- `updateComponents`: Updates components in a surface.
+
+Properties:
+
+- `createSurface`: Requires `surfaceId` (you must always use a unique ID for each created surface),
+`catalogId` (use the active catalog ID if provided in system instructions),
+and `sendDataModel: true`.
+- `updateComponents`: Requires `surfaceId` and a list of `components`.
+One component MUST have `id: "root"`.
+
+To create a new UI:
+1. Output a `createSurface` message with a unique `surfaceId` and `catalogId` (use the active catalog ID if provided in system instructions).
+2. Output an `updateComponents` message with the `surfaceId` and the component definitions.
+
+IMPORTANT: DO NOT update or modify surfaces created in previous turns. If the UI needs to change, you MUST create a NEW surface with a new unique `surfaceId`. You may only use `updateComponents` to populate the components of a freshly created surface.
+-----CONTROLLING_THE_UI_END-----
+
+-------------------------------------
+
+-----OUTPUT_FORMAT_START-----
+When constructing UI, you must output a VALID A2UI JSON object representing one of the A2UI message types (`createSurface`, `updateComponents`).
+- You can treat the A2UI schema as a specification for the JSON you typically output.
+- The JSON block must be valid and complete.
+- Ensure your JSON is fenced with ```json and ```.
+-----OUTPUT_FORMAT_END-----
+
+-------------------------------------
+
+-----COMMON_TYPES_START-----
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "common_types.json",
+ "title": "A2UI Common Types",
+ "description": "Common type definitions used across A2UI schemas.",
+ "$defs": {
+ "ComponentId": {
+ "type": "string",
+ "description": "The unique identifier for a component, used for both definitions and references within the same surface."
+ },
+ "AccessibilityAttributes": {
+ "type": "object",
+ "description": "Attributes to enhance accessibility when using assistive technologies like screen readers.",
+ "properties": {
+ "label": {
+ "$ref": "#/$defs/DynamicString",
+ "description": "A short string, typically 1 to 3 words, used by assistive technologies to convey the purpose or intent of an element. For example, an input field might have an accessible label of 'User ID' or a button might be labeled 'Submit'."
+ },
+ "description": {
+ "$ref": "#/$defs/DynamicString",
+ "description": "Additional information provided by assistive technologies about an element such as instructions, format requirements, or result of an action. For example, a mute button might have a label of 'Mute' and a description of 'Silences notifications about this conversation'."
+ }
+ }
+ },
+ "ComponentCommon": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "$ref": "#/$defs/ComponentId"
+ },
+ "accessibility": {
+ "$ref": "#/$defs/AccessibilityAttributes"
+ }
+ },
+ "required": ["id"]
+ },
+ "ChildList": {
+ "oneOf": [
+ {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/ComponentId"
+ },
+ "description": "A static list of child component IDs."
+ },
+ {
+ "type": "object",
+ "description": "A template for generating a dynamic list of children from a data model list. The `componentId` is the component to use as a template.",
+ "properties": {
+ "componentId": {
+ "$ref": "#/$defs/ComponentId"
+ },
+ "path": {
+ "type": "string",
+ "description": "The path to the list of component property objects in the data model."
+ }
+ },
+ "required": ["componentId", "path"],
+ "additionalProperties": false
+ }
+ ]
+ },
+ "DataBinding": {
+ "type": "object",
+ "properties": {
+ "path": {
+ "type": "string",
+ "description": "A JSON Pointer path to a value in the data model."
+ }
+ },
+ "required": ["path"],
+ "additionalProperties": false
+ },
+ "DynamicValue": {
+ "description": "A value that can be a literal, a path, or a function call returning any type.",
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "array"
+ },
+ {
+ "$ref": "#/$defs/DataBinding"
+ },
+ {
+ "$ref": "#/$defs/FunctionCall"
+ }
+ ]
+ },
+ "DynamicString": {
+ "description": "Represents a string",
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "$ref": "#/$defs/DataBinding"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/FunctionCall"
+ },
+ {
+ "properties": {
+ "returnType": {
+ "const": "string"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "DynamicNumber": {
+ "description": "Represents a value that can be either a literal number, a path to a number in the data model, or a function call returning a number.",
+ "oneOf": [
+ {
+ "type": "number"
+ },
+ {
+ "$ref": "#/$defs/DataBinding"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/FunctionCall"
+ },
+ {
+ "properties": {
+ "returnType": {
+ "const": "number"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "DynamicBoolean": {
+ "description": "A boolean value that can be a literal, a path, or a function call returning a boolean.",
+ "oneOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "$ref": "#/$defs/DataBinding"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/FunctionCall"
+ },
+ {
+ "properties": {
+ "returnType": {
+ "const": "boolean"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "DynamicStringList": {
+ "description": "Represents a value that can be either a literal array of strings, a path to a string array in the data model, or a function call returning a string array.",
+ "oneOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "$ref": "#/$defs/DataBinding"
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "#/$defs/FunctionCall"
+ },
+ {
+ "properties": {
+ "returnType": {
+ "const": "array"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "FunctionCall": {
+ "type": "object",
+ "description": "Invokes a named function on the client.",
+ "properties": {
+ "call": {
+ "type": "string",
+ "description": "The name of the function to call."
+ },
+ "args": {
+ "type": "object",
+ "description": "Arguments passed to the function.",
+ "additionalProperties": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/DynamicValue"
+ },
+ {
+ "type": "object",
+ "description": "A literal object argument (e.g. configuration)."
+ }
+ ]
+ }
+ },
+ "returnType": {
+ "type": "string",
+ "description": "The expected return type of the function call.",
+ "enum": ["string", "number", "boolean", "array", "object", "any", "void"],
+ "default": "boolean"
+ }
+ },
+ "required": ["call"],
+ "oneOf": [{"$ref": "catalog.json#/$defs/anyFunction"}]
+ },
+ "CheckRule": {
+ "type": "object",
+ "description": "A single validation rule applied to an input component.",
+ "properties": {
+ "condition": {
+ "$ref": "#/$defs/DynamicBoolean"
+ },
+ "message": {
+ "type": "string",
+ "description": "The error message to display if the check fails."
+ }
+ },
+ "required": ["condition", "message"],
+ "additionalProperties": false
+ },
+ "Checkable": {
+ "description": "Properties for components that support client-side checks.",
+ "type": "object",
+ "properties": {
+ "checks": {
+ "type": "array",
+ "description": "A list of checks to perform. These are function calls that must return a boolean indicating validity.",
+ "items": {
+ "$ref": "#/$defs/CheckRule"
+ }
+ }
+ }
+ },
+ "Action": {
+ "description": "Defines an interaction handler that can either trigger a server-side event or execute a local client-side function.",
+ "oneOf": [
+ {
+ "type": "object",
+ "description": "Triggers a server-side event.",
+ "properties": {
+ "event": {
+ "type": "object",
+ "description": "The event to dispatch to the server.",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the action to be dispatched to the server."
+ },
+ "context": {
+ "type": "object",
+ "description": "A JSON object containing the key-value pairs for the action context. Values can be literals or paths. Use literal values unless the value must be dynamically bound to the data model. Do NOT use paths for static IDs.",
+ "additionalProperties": {
+ "$ref": "#/$defs/DynamicValue"
+ }
+ }
+ },
+ "required": ["name"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["event"],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "description": "Executes a local client-side function.",
+ "properties": {
+ "functionCall": {
+ "$ref": "#/$defs/FunctionCall"
+ }
+ },
+ "required": ["functionCall"],
+ "additionalProperties": false
+ }
+ ]
+ }
+ }
+}
+-----COMMON_TYPES_END-----
+
+-------------------------------------
+
+-----CATALOG_SCHEMA_START-----
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://a2ui.org/specification/v0_9/catalog.json",
+ "title": "A2UI Catalog",
+ "description": "Custom catalog of A2UI components and functions.",
+ "catalogId": "commis_catalog",
+ "components": {
+ "CateringJob": {
+ "type": "object",
+ "allOf": [
+ {
+ "$ref": "common_types.json#/$defs/ComponentCommon"
+ },
+ {
+ "$ref": "#/$defs/CatalogComponentCommon"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "component": {
+ "type": "string",
+ "enum": [
+ "CateringJob"
+ ]
+ },
+ "jobId": {
+ "type": "string",
+ "description": "The unique ID of the catering job to display."
+ }
+ },
+ "required": [
+ "component",
+ "jobId"
+ ]
+ }
+ ],
+ "unevaluatedProperties": false
+ },
+ "RecipeLine": {
+ "type": "object",
+ "allOf": [
+ {
+ "$ref": "common_types.json#/$defs/ComponentCommon"
+ },
+ {
+ "$ref": "#/$defs/CatalogComponentCommon"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "component": {
+ "type": "string",
+ "enum": [
+ "RecipeLine"
+ ]
+ },
+ "recipeId": {
+ "type": "string",
+ "description": "The unique ID of the recipe to display."
+ }
+ },
+ "required": [
+ "component",
+ "recipeId"
+ ]
+ }
+ ],
+ "unevaluatedProperties": false
+ },
+ "IngredientLine": {
+ "type": "object",
+ "allOf": [
+ {
+ "$ref": "common_types.json#/$defs/ComponentCommon"
+ },
+ {
+ "$ref": "#/$defs/CatalogComponentCommon"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "component": {
+ "type": "string",
+ "enum": [
+ "IngredientLine"
+ ]
+ },
+ "ingredientId": {
+ "type": "string",
+ "description": "The unique ID of the ingredient to display."
+ }
+ },
+ "required": [
+ "component",
+ "ingredientId"
+ ]
+ }
+ ],
+ "unevaluatedProperties": false
+ },
+ "NavigationCard": {
+ "type": "object",
+ "allOf": [
+ {
+ "$ref": "common_types.json#/$defs/ComponentCommon"
+ },
+ {
+ "$ref": "#/$defs/CatalogComponentCommon"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "component": {
+ "type": "string",
+ "enum": [
+ "NavigationCard"
+ ]
+ },
+ "title": {
+ "type": "string",
+ "description": "Title of the job."
+ },
+ "address": {
+ "type": "string",
+ "description": "Address of the job."
+ },
+ "latitude": {
+ "type": "number",
+ "description": "Latitude of the job location."
+ },
+ "longitude": {
+ "type": "number",
+ "description": "Longitude of the job location."
+ }
+ },
+ "required": [
+ "component",
+ "title",
+ "address",
+ "latitude",
+ "longitude"
+ ]
+ }
+ ],
+ "unevaluatedProperties": false
+ },
+ "SimpleCard": {
+ "type": "object",
+ "allOf": [
+ {
+ "$ref": "common_types.json#/$defs/ComponentCommon"
+ },
+ {
+ "$ref": "#/$defs/CatalogComponentCommon"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "component": {
+ "type": "string",
+ "enum": [
+ "SimpleCard"
+ ]
+ },
+ "child": {
+ "type": "string",
+ "description": "The ID of a component."
+ }
+ },
+ "required": [
+ "component",
+ "child"
+ ]
+ }
+ ],
+ "unevaluatedProperties": false
+ },
+ "Column": {
+ "type": "object",
+ "allOf": [
+ {
+ "$ref": "common_types.json#/$defs/ComponentCommon"
+ },
+ {
+ "$ref": "#/$defs/CatalogComponentCommon"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "component": {
+ "type": "string",
+ "enum": [
+ "Column"
+ ]
+ },
+ "justify": {
+ "type": "string",
+ "description": "How children are aligned on the main axis. ",
+ "enum": [
+ "start",
+ "center",
+ "end",
+ "spaceBetween",
+ "spaceAround",
+ "spaceEvenly",
+ "stretch"
+ ]
+ },
+ "align": {
+ "type": "string",
+ "description": "How children are aligned on the cross axis. ",
+ "enum": [
+ "start",
+ "center",
+ "end",
+ "stretch"
+ ]
+ },
+ "children": {
+ "description": "Either an explicit list of widget IDs for the children, or a template with a data binding to the list of children.",
+ "oneOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "description": "Component ID"
+ }
+ },
+ {
+ "type": "object",
+ "properties": {
+ "componentId": {
+ "type": "string",
+ "description": "The ID of a component."
+ },
+ "path": {
+ "type": "string",
+ "description": "A relative or absolute path in the data model."
+ }
+ },
+ "required": [
+ "componentId",
+ "path"
+ ]
+ }
+ ]
+ }
+ },
+ "required": [
+ "component",
+ "children"
+ ]
+ }
+ ],
+ "unevaluatedProperties": false
+ },
+ "Text": {
+ "type": "object",
+ "allOf": [
+ {
+ "$ref": "common_types.json#/$defs/ComponentCommon"
+ },
+ {
+ "$ref": "#/$defs/CatalogComponentCommon"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "component": {
+ "type": "string",
+ "enum": [
+ "Text"
+ ]
+ },
+ "text": {
+ "description": "While simple Markdown is supported (without HTML or image references), utilizing dedicated UI components is generally preferred for a richer and more structured presentation.",
+ "$ref": "common_types.json#/$defs/DynamicString"
+ },
+ "variant": {
+ "type": "string",
+ "description": "A hint for the base text style.",
+ "enum": [
+ "h1",
+ "h2",
+ "h3",
+ "h4",
+ "h5",
+ "caption",
+ "body"
+ ]
+ }
+ },
+ "required": [
+ "component",
+ "text"
+ ]
+ }
+ ],
+ "unevaluatedProperties": false
+ },
+ "Button": {
+ "type": "object",
+ "allOf": [
+ {
+ "$ref": "common_types.json#/$defs/ComponentCommon"
+ },
+ {
+ "$ref": "#/$defs/CatalogComponentCommon"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "component": {
+ "type": "string",
+ "enum": [
+ "Button"
+ ]
+ },
+ "child": {
+ "type": "string",
+ "description": "The ID of a child widget. This should always be set, e.g. to the ID of a `Text` widget."
+ },
+ "action": {
+ "$ref": "common_types.json#/$defs/Action"
+ },
+ "variant": {
+ "type": "string",
+ "description": "A hint for the button style.",
+ "enum": [
+ "primary",
+ "borderless"
+ ]
+ },
+ "checks": {
+ "type": "array",
+ "description": "Validation rules for this component.",
+ "items": {
+ "$ref": "common_types.json#/$defs/CheckRule"
+ }
+ }
+ },
+ "required": [
+ "component",
+ "child",
+ "action"
+ ]
+ }
+ ],
+ "unevaluatedProperties": false
+ },
+ "Row": {
+ "type": "object",
+ "allOf": [
+ {
+ "$ref": "common_types.json#/$defs/ComponentCommon"
+ },
+ {
+ "$ref": "#/$defs/CatalogComponentCommon"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "component": {
+ "type": "string",
+ "enum": [
+ "Row"
+ ]
+ },
+ "children": {
+ "description": "Either an explicit list of widget IDs for the children, or a template with a data binding to the list of children.",
+ "oneOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "description": "Component ID"
+ }
+ },
+ {
+ "type": "object",
+ "properties": {
+ "componentId": {
+ "type": "string",
+ "description": "The ID of a component."
+ },
+ "path": {
+ "type": "string",
+ "description": "A relative or absolute path in the data model."
+ }
+ },
+ "required": [
+ "componentId",
+ "path"
+ ]
+ }
+ ]
+ },
+ "justify": {
+ "type": "string",
+ "enum": [
+ "start",
+ "center",
+ "end",
+ "spaceBetween",
+ "spaceAround",
+ "spaceEvenly",
+ "stretch"
+ ]
+ },
+ "align": {
+ "type": "string",
+ "enum": [
+ "start",
+ "center",
+ "end",
+ "stretch"
+ ]
+ }
+ },
+ "required": [
+ "component",
+ "children"
+ ]
+ }
+ ],
+ "unevaluatedProperties": false
+ },
+ "Divider": {
+ "type": "object",
+ "allOf": [
+ {
+ "$ref": "common_types.json#/$defs/ComponentCommon"
+ },
+ {
+ "$ref": "#/$defs/CatalogComponentCommon"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "component": {
+ "type": "string",
+ "enum": [
+ "Divider"
+ ]
+ },
+ "axis": {
+ "type": "string",
+ "enum": [
+ "horizontal",
+ "vertical"
+ ]
+ }
+ },
+ "required": [
+ "component"
+ ]
+ }
+ ],
+ "unevaluatedProperties": false
+ }
+ },
+ "functions": {
+ "calculateCost": {
+ "description": "Calculates the cost for a certain quantity of an ingredient. Returns a formatted dollar string (i.e. $4.50).",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "ingredient_id": {
+ "type": "string",
+ "description": "The ID of the ingredient."
+ },
+ "quantity": {
+ "type": "number",
+ "description": "The quantity of the ingredient."
+ }
+ },
+ "required": [
+ "ingredient_id",
+ "quantity"
+ ]
+ },
+ "returnType": "string"
+ }
+ },
+ "$defs": {
+ "CatalogComponentCommon": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "A unique identifier for this component instance within the surface. This ID is used to refer to the component in layout children arrays or event handlers."
+ }
+ },
+ "required": [
+ "id"
+ ]
+ },
+ "theme": {
+ "type": "object",
+ "properties": {
+ "primaryColor": {
+ "type": "string",
+ "description": "The primary brand color used for highlights (e.g., primary buttons, active borders). Renderers may generate variants of this color for different contexts. Format: Hexadecimal code (e.g., '#00BFFF').",
+ "pattern": "^#[0-9a-fA-F]{6}$"
+ },
+ "iconUrl": {
+ "type": "string",
+ "format": "uri",
+ "description": "A URL for an image that identifies the agent or tool associated with the surface."
+ },
+ "agentDisplayName": {
+ "type": "string",
+ "description": "Text to be displayed next to the surface to identify the agent or tool that created it."
+ }
+ },
+ "additionalProperties": true
+ },
+ "anyComponent": {
+ "oneOf": [
+ {
+ "$ref": "#/components/CateringJob"
+ },
+ {
+ "$ref": "#/components/RecipeLine"
+ },
+ {
+ "$ref": "#/components/IngredientLine"
+ },
+ {
+ "$ref": "#/components/NavigationCard"
+ },
+ {
+ "$ref": "#/components/SimpleCard"
+ },
+ {
+ "$ref": "#/components/Column"
+ },
+ {
+ "$ref": "#/components/Text"
+ },
+ {
+ "$ref": "#/components/Button"
+ },
+ {
+ "$ref": "#/components/Row"
+ },
+ {
+ "$ref": "#/components/Divider"
+ }
+ ],
+ "discriminator": {
+ "propertyName": "component"
+ }
+ },
+ "anyFunction": {
+ "oneOf": [
+ {
+ "$ref": "#/functions/calculateCost"
+ }
+ ]
+ }
+ }
+}
+-----CATALOG_SCHEMA_END-----
+
+-------------------------------------
+
+-----MESSAGE_SCHEMA_START-----
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://a2ui.org/specification/v0_9/server_to_client.json",
+ "title": "A2UI Message Schema",
+ "description": "Describes a JSON payload for an A2UI (Agent to UI) message, which is used to dynamically construct and update user interfaces.",
+ "type": "object",
+ "oneOf": [
+ {"$ref": "#/$defs/CreateSurfaceMessage"},
+ {"$ref": "#/$defs/UpdateComponentsMessage"},
+ {"$ref": "#/$defs/UpdateDataModelMessage"},
+ {"$ref": "#/$defs/DeleteSurfaceMessage"}
+ ],
+ "$defs": {
+ "CreateSurfaceMessage": {
+ "type": "object",
+ "properties": {
+ "version": {
+ "const": "v0.9"
+ },
+ "createSurface": {
+ "type": "object",
+ "description": "Signals the client to create a new surface and begin rendering it. It is an error to send 'createSurface' for a surfaceId that already exists without first deleting it. When this message is sent, the client will expect 'updateComponents' and/or 'updateDataModel' messages for the same surfaceId that define the component tree.",
+ "properties": {
+ "surfaceId": {
+ "type": "string",
+ "description": "The unique identifier for the UI surface to be rendered."
+ },
+ "catalogId": {
+ "description": "A string that uniquely identifies this catalog. It is recommended to prefix this with an internet domain that you own, to avoid conflicts e.g. mycompany.com:somecatalog'.",
+ "type": "string"
+ },
+ "theme": {
+ "$ref": "catalog.json#/$defs/theme",
+ "description": "Theme parameters for the surface (e.g., {'primaryColor': '#FF0000'}). These must validate against the 'theme' schema defined in the catalog."
+ },
+ "sendDataModel": {
+ "type": "boolean",
+ "description": "If true, the client will send the full data model of this surface in the metadata of every A2A message sent to the server that created the surface. Defaults to false."
+ }
+ },
+ "required": ["surfaceId", "catalogId"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["createSurface", "version"],
+ "additionalProperties": false
+ },
+ "UpdateComponentsMessage": {
+ "type": "object",
+ "properties": {
+ "version": {
+ "const": "v0.9"
+ },
+ "updateComponents": {
+ "type": "object",
+ "description": "Updates a surface with a new set of components. This message can be sent multiple times to update the component tree of an existing surface. One of the components in one of the components lists MUST have an 'id' of 'root' to serve as the root of the component tree. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.",
+ "properties": {
+ "surfaceId": {
+ "type": "string",
+ "description": "The unique identifier for the UI surface to be updated."
+ },
+
+ "components": {
+ "type": "array",
+ "description": "A list containing all UI components for the surface.",
+ "minItems": 1,
+ "items": {
+ "$ref": "catalog.json#/$defs/anyComponent"
+ }
+ }
+ },
+ "required": ["surfaceId", "components"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["updateComponents", "version"],
+ "additionalProperties": false
+ },
+ "UpdateDataModelMessage": {
+ "type": "object",
+ "properties": {
+ "version": {
+ "const": "v0.9"
+ },
+ "updateDataModel": {
+ "type": "object",
+ "description": "Updates the data model for an existing surface. This message can be sent multiple times to update the data model. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.",
+ "properties": {
+ "surfaceId": {
+ "type": "string",
+ "description": "The unique identifier for the UI surface this data model update applies to."
+ },
+ "path": {
+ "type": "string",
+ "description": "An optional path to a location within the data model (e.g., '/user/name'). If omitted, or set to '/', refers to the entire data model."
+ },
+ "value": {
+ "description": "The data to be updated in the data model. If present, the value at 'path' is replaced (or created). If omitted, the key at 'path' is removed.",
+ "additionalProperties": true
+ }
+ },
+ "required": ["surfaceId"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["updateDataModel", "version"],
+ "additionalProperties": false
+ },
+ "DeleteSurfaceMessage": {
+ "type": "object",
+ "properties": {
+ "version": {
+ "const": "v0.9"
+ },
+ "deleteSurface": {
+ "type": "object",
+ "description": "Signals the client to delete the surface identified by 'surfaceId'. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.",
+ "properties": {
+ "surfaceId": {
+ "type": "string",
+ "description": "The unique identifier for the UI surface to be deleted."
+ }
+ },
+ "required": ["surfaceId"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["deleteSurface", "version"],
+ "additionalProperties": false
+ }
+ }
+}
+-----MESSAGE_SCHEMA_END-----
diff --git a/commis/app/firebase.json b/commis/app/firebase.json
new file mode 100644
index 00000000..1a9abe84
--- /dev/null
+++ b/commis/app/firebase.json
@@ -0,0 +1 @@
+{"flutter":{"platforms":{"android":{"default":{"projectId":"YOUR-PROJECT-ID","appId":"YOUR-ANDROID-APP-ID","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"YOUR-PROJECT-ID","appId":"YOUR-IOS-APP-ID","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"macos":{"default":{"projectId":"YOUR-PROJECT-ID","appId":"YOUR-MACOS-APP-ID","uploadDebugSymbols":false,"fileOutput":"macos/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"YOUR-PROJECT-ID","configurations":{"android":"YOUR-ANDROID-APP-ID","ios":"YOUR-IOS-APP-ID","macos":"YOUR-MACOS-APP-ID","web":"YOUR-WEB-APP-ID","windows":"YOUR-WINDOWS-APP-ID"}}}}}}
\ No newline at end of file
diff --git a/commis/app/ios/.gitignore b/commis/app/ios/.gitignore
new file mode 100644
index 00000000..7a7f9873
--- /dev/null
+++ b/commis/app/ios/.gitignore
@@ -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
diff --git a/commis/app/ios/Flutter/AppFrameworkInfo.plist b/commis/app/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 00000000..391a902b
--- /dev/null
+++ b/commis/app/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,24 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleExecutable
+ App
+ CFBundleIdentifier
+ io.flutter.flutter.app
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ App
+ CFBundlePackageType
+ FMWK
+ CFBundleShortVersionString
+ 1.0
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 1.0
+
+
diff --git a/commis/app/ios/Flutter/Debug.xcconfig b/commis/app/ios/Flutter/Debug.xcconfig
new file mode 100644
index 00000000..ec97fc6f
--- /dev/null
+++ b/commis/app/ios/Flutter/Debug.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
+#include "Generated.xcconfig"
diff --git a/commis/app/ios/Flutter/Release.xcconfig b/commis/app/ios/Flutter/Release.xcconfig
new file mode 100644
index 00000000..c4855bfe
--- /dev/null
+++ b/commis/app/ios/Flutter/Release.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
+#include "Generated.xcconfig"
diff --git a/commis/app/ios/Podfile b/commis/app/ios/Podfile
new file mode 100644
index 00000000..c2c1dc54
--- /dev/null
+++ b/commis/app/ios/Podfile
@@ -0,0 +1,43 @@
+# Uncomment this line to define a global platform for your project
+platform :ios, '14.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!
+
+ 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
diff --git a/commis/app/ios/Runner.xcodeproj/project.pbxproj b/commis/app/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 00000000..58d53915
--- /dev/null
+++ b/commis/app/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,765 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 54;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
+ 1A31E7DA470F326451D5F38B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 82057B2F4FD97F44CC12F075 /* Pods_Runner.framework */; };
+ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+ 67441845B536028938FA49FD /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = A58485A03BF5F6FFBF5FFAF1 /* GoogleService-Info.plist */; };
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
+ 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
+ 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
+ 8069338618EE90437E32E92A /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0C5C7A505A5F4DA6901ED15 /* Pods_RunnerTests.framework */; };
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 97C146E61CF9000F007C117D /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 97C146ED1CF9000F007C117D;
+ remoteInfo = Runner;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
+ 23DD40F0B8D3AF461B266789 /* 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 = ""; };
+ 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; };
+ 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
+ 6C041BE8EBEA513B2375CE49 /* 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 = ""; };
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
+ 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
+ 82057B2F4FD97F44CC12F075 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 84E406A647D36139954AC819 /* 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 = ""; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
+ 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 = ""; };
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
+ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ A58485A03BF5F6FFBF5FFAF1 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; };
+ B9F5ED6BBD6EE5B290FDFF96 /* 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 = ""; };
+ BC41AC99CD550B817A5096AE /* 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 = ""; };
+ C0C5C7A505A5F4DA6901ED15 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ E9516313729794B5DBCA8F12 /* 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 = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 97C146EB1CF9000F007C117D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
+ 1A31E7DA470F326451D5F38B /* Pods_Runner.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ C007CFC456D96882AB93561C /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 8069338618EE90437E32E92A /* Pods_RunnerTests.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 331C8082294A63A400263BE5 /* RunnerTests */ = {
+ isa = PBXGroup;
+ children = (
+ 331C807B294A618700263BE5 /* RunnerTests.swift */,
+ );
+ path = RunnerTests;
+ sourceTree = "";
+ };
+ 3CA5961297A545AFA1CC99DE /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ 84E406A647D36139954AC819 /* Pods-Runner.debug.xcconfig */,
+ BC41AC99CD550B817A5096AE /* Pods-Runner.release.xcconfig */,
+ 23DD40F0B8D3AF461B266789 /* Pods-Runner.profile.xcconfig */,
+ E9516313729794B5DBCA8F12 /* Pods-RunnerTests.debug.xcconfig */,
+ B9F5ED6BBD6EE5B290FDFF96 /* Pods-RunnerTests.release.xcconfig */,
+ 6C041BE8EBEA513B2375CE49 /* Pods-RunnerTests.profile.xcconfig */,
+ );
+ path = Pods;
+ sourceTree = "";
+ };
+ 9740EEB11CF90186004384FC /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */,
+ );
+ name = Flutter;
+ sourceTree = "";
+ };
+ 97C146E51CF9000F007C117D = {
+ isa = PBXGroup;
+ children = (
+ 9740EEB11CF90186004384FC /* Flutter */,
+ 97C146F01CF9000F007C117D /* Runner */,
+ 97C146EF1CF9000F007C117D /* Products */,
+ 331C8082294A63A400263BE5 /* RunnerTests */,
+ A58485A03BF5F6FFBF5FFAF1 /* GoogleService-Info.plist */,
+ 3CA5961297A545AFA1CC99DE /* Pods */,
+ E312674DCF8489DBD8CDFA6D /* Frameworks */,
+ );
+ sourceTree = "";
+ };
+ 97C146EF1CF9000F007C117D /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146EE1CF9000F007C117D /* Runner.app */,
+ 331C8081294A63A400263BE5 /* RunnerTests.xctest */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 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 */,
+ 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
+ );
+ path = Runner;
+ sourceTree = "";
+ };
+ E312674DCF8489DBD8CDFA6D /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 82057B2F4FD97F44CC12F075 /* Pods_Runner.framework */,
+ C0C5C7A505A5F4DA6901ED15 /* Pods_RunnerTests.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 331C8080294A63A400263BE5 /* RunnerTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
+ buildPhases = (
+ 0A15E5683C21DDBAA7B57B16 /* [CP] Check Pods Manifest.lock */,
+ 331C807D294A63A400263BE5 /* Sources */,
+ 331C807F294A63A400263BE5 /* Resources */,
+ C007CFC456D96882AB93561C /* 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 = (
+ D730AA90621DB986D68F3D8C /* [CP] Check Pods Manifest.lock */,
+ 9740EEB61CF901F6004384FC /* Run Script */,
+ 97C146EA1CF9000F007C117D /* Sources */,
+ 97C146EB1CF9000F007C117D /* Frameworks */,
+ 97C146EC1CF9000F007C117D /* Resources */,
+ 9705A1C41CF9048500538489 /* Embed Frameworks */,
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+ DE0519414D139EFC53F26758 /* [CP] Copy Pods Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Runner;
+ packageProductDependencies = (
+ 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
+ );
+ 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;
+ packageReferences = (
+ 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
+ );
+ 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 */,
+ 67441845B536028938FA49FD /* GoogleService-Info.plist in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 0A15E5683C21DDBAA7B57B16 /* [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;
+ };
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
+ isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
+ );
+ name = "Thin Binary";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
+ };
+ 9740EEB61CF901F6004384FC /* Run Script */ = {
+ isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Run Script";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
+ };
+ D730AA90621DB986D68F3D8C /* [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;
+ };
+ DE0519414D139EFC53F26758 /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Copy Pods Resources";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\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 */,
+ 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift 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 = "";
+ };
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C147001CF9000F007C117D /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 249021D3217E4FDB00AE95B9 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.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 = 9DTE9G7Q69;
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.6;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.commis;
+ 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 = E9516313729794B5DBCA8F12 /* 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.commis.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 = B9F5ED6BBD6EE5B290FDFF96 /* 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.commis.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 = 6C041BE8EBEA513B2375CE49 /* 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.commis.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
+ };
+ name = Profile;
+ };
+ 97C147031CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 97C147041CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.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 = 9DTE9G7Q69;
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.6;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.commis;
+ 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 = 9DTE9G7Q69;
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.6;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.commis;
+ 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 */
+
+/* Begin XCLocalSwiftPackageReference section */
+ 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
+ isa = XCLocalSwiftPackageReference;
+ relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
+ };
+/* End XCLocalSwiftPackageReference section */
+
+/* Begin XCSwiftPackageProductDependency section */
+ 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
+ isa = XCSwiftPackageProductDependency;
+ productName = FlutterGeneratedPluginSwiftPackage;
+ };
+/* End XCSwiftPackageProductDependency section */
+ };
+ rootObject = 97C146E61CF9000F007C117D /* Project object */;
+}
diff --git a/commis/app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/commis/app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 00000000..919434a6
--- /dev/null
+++ b/commis/app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/commis/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/commis/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 00000000..18d98100
--- /dev/null
+++ b/commis/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/commis/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/commis/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 00000000..f9b0d7c5
--- /dev/null
+++ b/commis/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/commis/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/commis/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
new file mode 100644
index 00000000..bc23812d
--- /dev/null
+++ b/commis/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -0,0 +1,131 @@
+{
+ "pins" : [
+ {
+ "identity" : "abseil-cpp-binary",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/abseil-cpp-binary.git",
+ "state" : {
+ "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5",
+ "version" : "1.2024072200.0"
+ }
+ },
+ {
+ "identity" : "app-check",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/app-check.git",
+ "state" : {
+ "revision" : "3e33dd27dd4c69bd81c7c81fe61d8ccf58846902",
+ "version" : "11.3.1"
+ }
+ },
+ {
+ "identity" : "firebase-ios-sdk",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/firebase/firebase-ios-sdk",
+ "state" : {
+ "revision" : "42e81d245e30e49ea6a5830cf2842d44a1591270",
+ "version" : "12.15.0"
+ }
+ },
+ {
+ "identity" : "google-ads-on-device-conversion-ios-sdk",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk",
+ "state" : {
+ "revision" : "dc39082d8881109d35b94b1c122164c0e8d08a55",
+ "version" : "3.6.1"
+ }
+ },
+ {
+ "identity" : "googleappmeasurement",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/GoogleAppMeasurement.git",
+ "state" : {
+ "revision" : "144855f40d8668927f256a3045f7fdc4c3f4338b",
+ "version" : "12.15.0"
+ }
+ },
+ {
+ "identity" : "googledatatransport",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/GoogleDataTransport.git",
+ "state" : {
+ "revision" : "617af071af9aa1d6a091d59a202910ac482128f9",
+ "version" : "10.1.0"
+ }
+ },
+ {
+ "identity" : "googleutilities",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/GoogleUtilities.git",
+ "state" : {
+ "revision" : "9f183ae842be978784f2963a343682e0c46d8fb3",
+ "version" : "8.1.2"
+ }
+ },
+ {
+ "identity" : "grpc-binary",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/grpc-binary.git",
+ "state" : {
+ "revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6",
+ "version" : "1.69.1"
+ }
+ },
+ {
+ "identity" : "gtm-session-fetcher",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/gtm-session-fetcher.git",
+ "state" : {
+ "revision" : "c0ac7575d70050c2973ba2318bd5af47f8e8153a",
+ "version" : "5.3.0"
+ }
+ },
+ {
+ "identity" : "interop-ios-for-google-sdks",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/interop-ios-for-google-sdks.git",
+ "state" : {
+ "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe",
+ "version" : "101.0.0"
+ }
+ },
+ {
+ "identity" : "leveldb",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/firebase/leveldb.git",
+ "state" : {
+ "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1",
+ "version" : "1.22.5"
+ }
+ },
+ {
+ "identity" : "nanopb",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/firebase/nanopb.git",
+ "state" : {
+ "revision" : "3851d94a41890dea16dc3db34caf60e585cb4163",
+ "version" : "2.30910.1"
+ }
+ },
+ {
+ "identity" : "promises",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/promises.git",
+ "state" : {
+ "revision" : "f4a19a3c313dc2616c70bb49d29a799fb16be837",
+ "version" : "2.4.1"
+ }
+ },
+ {
+ "identity" : "recaptcha-enterprise-mobile-sdk",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/GoogleCloudPlatform/recaptcha-enterprise-mobile-sdk.git",
+ "state" : {
+ "revision" : "85588690041e63784be7bcb4b631c32565127ba2",
+ "version" : "18.9.1"
+ }
+ }
+ ],
+ "version" : 2
+}
diff --git a/commis/app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/commis/app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 00000000..c3fedb29
--- /dev/null
+++ b/commis/app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,119 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/commis/app/ios/Runner.xcworkspace/contents.xcworkspacedata b/commis/app/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 00000000..21a3cc14
--- /dev/null
+++ b/commis/app/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/commis/app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/commis/app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 00000000..18d98100
--- /dev/null
+++ b/commis/app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/commis/app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/commis/app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 00000000..f9b0d7c5
--- /dev/null
+++ b/commis/app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/commis/app/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/commis/app/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved
new file mode 100644
index 00000000..bc23812d
--- /dev/null
+++ b/commis/app/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -0,0 +1,131 @@
+{
+ "pins" : [
+ {
+ "identity" : "abseil-cpp-binary",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/abseil-cpp-binary.git",
+ "state" : {
+ "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5",
+ "version" : "1.2024072200.0"
+ }
+ },
+ {
+ "identity" : "app-check",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/app-check.git",
+ "state" : {
+ "revision" : "3e33dd27dd4c69bd81c7c81fe61d8ccf58846902",
+ "version" : "11.3.1"
+ }
+ },
+ {
+ "identity" : "firebase-ios-sdk",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/firebase/firebase-ios-sdk",
+ "state" : {
+ "revision" : "42e81d245e30e49ea6a5830cf2842d44a1591270",
+ "version" : "12.15.0"
+ }
+ },
+ {
+ "identity" : "google-ads-on-device-conversion-ios-sdk",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk",
+ "state" : {
+ "revision" : "dc39082d8881109d35b94b1c122164c0e8d08a55",
+ "version" : "3.6.1"
+ }
+ },
+ {
+ "identity" : "googleappmeasurement",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/GoogleAppMeasurement.git",
+ "state" : {
+ "revision" : "144855f40d8668927f256a3045f7fdc4c3f4338b",
+ "version" : "12.15.0"
+ }
+ },
+ {
+ "identity" : "googledatatransport",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/GoogleDataTransport.git",
+ "state" : {
+ "revision" : "617af071af9aa1d6a091d59a202910ac482128f9",
+ "version" : "10.1.0"
+ }
+ },
+ {
+ "identity" : "googleutilities",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/GoogleUtilities.git",
+ "state" : {
+ "revision" : "9f183ae842be978784f2963a343682e0c46d8fb3",
+ "version" : "8.1.2"
+ }
+ },
+ {
+ "identity" : "grpc-binary",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/grpc-binary.git",
+ "state" : {
+ "revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6",
+ "version" : "1.69.1"
+ }
+ },
+ {
+ "identity" : "gtm-session-fetcher",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/gtm-session-fetcher.git",
+ "state" : {
+ "revision" : "c0ac7575d70050c2973ba2318bd5af47f8e8153a",
+ "version" : "5.3.0"
+ }
+ },
+ {
+ "identity" : "interop-ios-for-google-sdks",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/interop-ios-for-google-sdks.git",
+ "state" : {
+ "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe",
+ "version" : "101.0.0"
+ }
+ },
+ {
+ "identity" : "leveldb",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/firebase/leveldb.git",
+ "state" : {
+ "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1",
+ "version" : "1.22.5"
+ }
+ },
+ {
+ "identity" : "nanopb",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/firebase/nanopb.git",
+ "state" : {
+ "revision" : "3851d94a41890dea16dc3db34caf60e585cb4163",
+ "version" : "2.30910.1"
+ }
+ },
+ {
+ "identity" : "promises",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/promises.git",
+ "state" : {
+ "revision" : "f4a19a3c313dc2616c70bb49d29a799fb16be837",
+ "version" : "2.4.1"
+ }
+ },
+ {
+ "identity" : "recaptcha-enterprise-mobile-sdk",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/GoogleCloudPlatform/recaptcha-enterprise-mobile-sdk.git",
+ "state" : {
+ "revision" : "85588690041e63784be7bcb4b631c32565127ba2",
+ "version" : "18.9.1"
+ }
+ }
+ ],
+ "version" : 2
+}
diff --git a/commis/app/ios/Runner/AppDelegate.swift b/commis/app/ios/Runner/AppDelegate.swift
new file mode 100644
index 00000000..aed313de
--- /dev/null
+++ b/commis/app/ios/Runner/AppDelegate.swift
@@ -0,0 +1,19 @@
+import Flutter
+import UIKit
+import GoogleMaps
+
+@main
+@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
+ override func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+ ) -> Bool {
+ // TODO: Replace with your Google Maps API Key
+ GMSServices.provideAPIKey("YOUR_GOOGLE_MAPS_API_KEY")
+ return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+ }
+
+ func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
+ GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
+ }
+}
diff --git a/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 00000000..d36b1fab
--- /dev/null
+++ b/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -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"
+ }
+}
diff --git a/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
new file mode 100644
index 00000000..dc9ada47
Binary files /dev/null and b/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ
diff --git a/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100644
index 00000000..7353c41e
Binary files /dev/null and b/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100644
index 00000000..797d452e
Binary files /dev/null and b/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100644
index 00000000..6ed2d933
Binary files /dev/null and b/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
new file mode 100644
index 00000000..4cd7b009
Binary files /dev/null and b/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100644
index 00000000..fe730945
Binary files /dev/null and b/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100644
index 00000000..321773cd
Binary files /dev/null and b/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100644
index 00000000..797d452e
Binary files /dev/null and b/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100644
index 00000000..502f463a
Binary files /dev/null and b/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100644
index 00000000..0ec30343
Binary files /dev/null and b/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 00000000..0ec30343
Binary files /dev/null and b/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100644
index 00000000..e9f5fea2
Binary files /dev/null and b/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100644
index 00000000..84ac32ae
Binary files /dev/null and b/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100644
index 00000000..8953cba0
Binary files /dev/null and b/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100644
index 00000000..0467bf12
Binary files /dev/null and b/commis/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/commis/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/commis/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 00000000..0bedcf2f
--- /dev/null
+++ b/commis/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -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"
+ }
+}
diff --git a/commis/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/commis/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
new file mode 100644
index 00000000..9da19eac
Binary files /dev/null and b/commis/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ
diff --git a/commis/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/commis/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
new file mode 100644
index 00000000..9da19eac
Binary files /dev/null and b/commis/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ
diff --git a/commis/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/commis/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
new file mode 100644
index 00000000..9da19eac
Binary files /dev/null and b/commis/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ
diff --git a/commis/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/commis/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 00000000..89c2725b
--- /dev/null
+++ b/commis/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
@@ -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.
\ No newline at end of file
diff --git a/commis/app/ios/Runner/Base.lproj/LaunchScreen.storyboard b/commis/app/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 00000000..f2e259c7
--- /dev/null
+++ b/commis/app/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/commis/app/ios/Runner/Base.lproj/Main.storyboard b/commis/app/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 00000000..f3c28516
--- /dev/null
+++ b/commis/app/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/commis/app/ios/Runner/Info.plist b/commis/app/ios/Runner/Info.plist
new file mode 100644
index 00000000..a3883d6c
--- /dev/null
+++ b/commis/app/ios/Runner/Info.plist
@@ -0,0 +1,77 @@
+
+
+
+
+ CADisableMinimumFrameDurationOnPhone
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ Commis
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ commis
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSRequiresIPhoneOS
+
+ UIApplicationSceneManifest
+
+ UIApplicationSupportsMultipleScenes
+
+ UISceneConfigurations
+
+ UIWindowSceneSessionRoleApplication
+
+
+ UISceneClassName
+ UIWindowScene
+ UISceneConfigurationName
+ flutter
+ UISceneDelegateClassName
+ $(PRODUCT_MODULE_NAME).SceneDelegate
+ UISceneStoryboardFile
+ Main
+
+
+
+
+ UIApplicationSupportsIndirectInputEvents
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ NSAppTransportSecurity
+
+ NSAllowsArbitraryLoads
+
+
+ NSLocationWhenInUseUsageDescription
+ We need your location to show navigation estimates.
+
+
diff --git a/commis/app/ios/Runner/Runner-Bridging-Header.h b/commis/app/ios/Runner/Runner-Bridging-Header.h
new file mode 100644
index 00000000..308a2a56
--- /dev/null
+++ b/commis/app/ios/Runner/Runner-Bridging-Header.h
@@ -0,0 +1 @@
+#import "GeneratedPluginRegistrant.h"
diff --git a/commis/app/ios/Runner/SceneDelegate.swift b/commis/app/ios/Runner/SceneDelegate.swift
new file mode 100644
index 00000000..b9ce8ea2
--- /dev/null
+++ b/commis/app/ios/Runner/SceneDelegate.swift
@@ -0,0 +1,6 @@
+import Flutter
+import UIKit
+
+class SceneDelegate: FlutterSceneDelegate {
+
+}
diff --git a/commis/app/ios/RunnerTests/RunnerTests.swift b/commis/app/ios/RunnerTests/RunnerTests.swift
new file mode 100644
index 00000000..86a7c3b1
--- /dev/null
+++ b/commis/app/ios/RunnerTests/RunnerTests.swift
@@ -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.
+ }
+
+}
diff --git a/commis/app/lib/data/firestore_repository.dart b/commis/app/lib/data/firestore_repository.dart
new file mode 100644
index 00000000..bd53d36c
--- /dev/null
+++ b/commis/app/lib/data/firestore_repository.dart
@@ -0,0 +1,160 @@
+// Copyright 2026 The Flutter Authors.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:cloud_firestore/cloud_firestore.dart';
+import 'models.dart';
+
+class FirestoreRepository {
+ final FirebaseFirestore _firestore;
+
+ FirestoreRepository({FirebaseFirestore? firestore})
+ : _firestore = firestore ?? FirebaseFirestore.instance;
+
+ // --- Catering Jobs ---
+
+ /// Stream all catering jobs sorted by date
+ Stream> watchJobs() {
+ return _firestore
+ .collection('jobs')
+ .orderBy('date', descending: false)
+ .snapshots()
+ .map(
+ (snapshot) => snapshot.docs
+ .map((doc) => CateringJob.fromMap(doc.id, doc.data()))
+ .toList(),
+ );
+ }
+
+ /// Fetch all catering jobs once (useful for tool calls/prompts)
+ Future> getJobs() async {
+ final snapshot = await _firestore
+ .collection('jobs')
+ .orderBy('date', descending: false)
+ .get();
+ return snapshot.docs
+ .map((doc) => CateringJob.fromMap(doc.id, doc.data()))
+ .toList();
+ }
+
+ /// Stream a single catering job by ID
+ Stream watchJob(String id) {
+ return _firestore.collection('jobs').doc(id).snapshots().map((doc) {
+ final data = doc.data();
+ if (data == null || !doc.exists) return null;
+ return CateringJob.fromMap(id, data);
+ });
+ }
+
+ /// Get a single catering job by ID once
+ Future getJob(String id) async {
+ final doc = await _firestore.collection('jobs').doc(id).get();
+ final data = doc.data();
+ if (data == null || !doc.exists) return null;
+ return CateringJob.fromMap(id, data);
+ }
+
+ /// Update or save a catering job
+ Future updateJob(CateringJob job) async {
+ await _firestore.collection('jobs').doc(job.id).set(job.toMap());
+ }
+
+ // --- Recipes ---
+
+ /// Stream all recipes
+ Stream> watchRecipes() {
+ return _firestore
+ .collection('recipes')
+ .snapshots()
+ .map(
+ (snapshot) => snapshot.docs
+ .map((doc) => Recipe.fromMap(doc.id, doc.data()))
+ .toList(),
+ );
+ }
+
+ /// Stream a single recipe by ID
+ Stream watchRecipe(String id) {
+ return _firestore.collection('recipes').doc(id).snapshots().map((doc) {
+ final data = doc.data();
+ if (data == null || !doc.exists) return null;
+ return Recipe.fromMap(id, data);
+ });
+ }
+
+ /// Fetch all recipes once
+ Future> getRecipes() async {
+ final snapshot = await _firestore.collection('recipes').get();
+ return snapshot.docs
+ .map((doc) => Recipe.fromMap(doc.id, doc.data()))
+ .toList();
+ }
+
+ /// Get a single recipe by ID once
+ Future getRecipe(String id) async {
+ final doc = await _firestore.collection('recipes').doc(id).get();
+ final data = doc.data();
+ if (data == null || !doc.exists) return null;
+ return Recipe.fromMap(id, data);
+ }
+
+ /// Fetch recipes associated with a given job ID
+ Future> getRecipesForJob(String jobId) async {
+ final job = await getJob(jobId);
+ if (job == null || job.recipeIds.isEmpty) return const [];
+ final recipes = await getRecipes();
+ return recipes.where((r) => job.recipeIds.contains(r.id)).toList();
+ }
+
+ // --- Ingredients ---
+
+ /// Stream all ingredients
+ Stream> watchIngredients() {
+ return _firestore
+ .collection('ingredients')
+ .snapshots()
+ .map(
+ (snapshot) => snapshot.docs
+ .map((doc) => Ingredient.fromMap(doc.id, doc.data()))
+ .toList(),
+ );
+ }
+
+ /// Stream a single ingredient by ID
+ Stream watchIngredient(String id) {
+ return _firestore.collection('ingredients').doc(id).snapshots().map((doc) {
+ final data = doc.data();
+ if (data == null || !doc.exists) return null;
+ return Ingredient.fromMap(id, data);
+ });
+ }
+
+ /// Fetch all ingredients once
+ Future> getIngredients() async {
+ final snapshot = await _firestore.collection('ingredients').get();
+ return snapshot.docs
+ .map((doc) => Ingredient.fromMap(doc.id, doc.data()))
+ .toList();
+ }
+
+ /// Fetch ingredients used in a recipe
+ Future> getIngredientsForRecipe(String recipeId) async {
+ final recipes = await getRecipes();
+ final recipe = recipes.cast().firstWhere(
+ (r) => r?.id == recipeId,
+ orElse: () => null,
+ );
+ if (recipe == null || recipe.ingredients.isEmpty) return const [];
+
+ final ingredientIds = recipe.ingredients.map((ri) => ri.itemId).toSet();
+ final ingredients = await getIngredients();
+ return ingredients.where((ii) => ingredientIds.contains(ii.id)).toList();
+ }
+
+ /// Fetch the message from the feed collection for a given JobId
+ Future getFeedMessage(String jobId) async {
+ final doc = await _firestore.collection('feed').doc(jobId).get();
+ if (!doc.exists) return null;
+ return doc.data()?['message'] as String?;
+ }
+}
diff --git a/commis/app/lib/data/models.dart b/commis/app/lib/data/models.dart
new file mode 100644
index 00000000..99ca799a
--- /dev/null
+++ b/commis/app/lib/data/models.dart
@@ -0,0 +1,204 @@
+import 'package:cloud_firestore/cloud_firestore.dart';
+
+// Copyright 2026 The Flutter Authors.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Defines database models serializable to and from Firestore.
+// These models represent the core data schemas stored in the backend database.
+
+class CateringJob {
+ final String id;
+ final String title;
+ final String address;
+ final int guestCount;
+ final DateTime date;
+ final List recipeIds;
+ final double latitude;
+ final double longitude;
+
+ const CateringJob({
+ required this.id,
+ required this.title,
+ required this.address,
+ required this.guestCount,
+ required this.date,
+ required this.recipeIds,
+ required this.latitude,
+ required this.longitude,
+ });
+
+ factory CateringJob.fromMap(String id, Map map) {
+ return CateringJob(
+ id: id,
+ title: map['title'] as String? ?? '',
+ address: map['address'] as String? ?? '',
+ guestCount: map['guestCount'] as int? ?? 0,
+ date: _parseDateTime(map['date']),
+ recipeIds:
+ (map['recipeIds'] as List?)?.map((x) => x as String).toList() ??
+ const [],
+ latitude: (map['latitude'] as num?)?.toDouble() ?? 0.0,
+ longitude: (map['longitude'] as num?)?.toDouble() ?? 0.0,
+ );
+ }
+
+ Map toMap() {
+ return {
+ 'id': id,
+ 'title': title,
+ 'address': address,
+ 'guestCount': guestCount,
+ 'date': Timestamp.fromDate(date),
+ 'recipeIds': recipeIds,
+ 'latitude': latitude,
+ 'longitude': longitude,
+ };
+ }
+}
+
+DateTime _parseDateTime(dynamic value) {
+ if (value is Timestamp) {
+ return value.toDate();
+ }
+ if (value is String) {
+ return DateTime.tryParse(value) ?? DateTime.now();
+ }
+ if (value is int) {
+ return DateTime.fromMillisecondsSinceEpoch(value);
+ }
+ return DateTime.now();
+}
+
+class Ingredient {
+ final String id;
+ final String name;
+ final String description;
+ final String unit;
+
+ const Ingredient({
+ required this.id,
+ required this.name,
+ required this.description,
+ required this.unit,
+ });
+
+ factory Ingredient.fromMap(String id, Map map) {
+ return Ingredient(
+ id: id,
+ name: map['name'] as String? ?? '',
+ description: map['description'] as String? ?? '',
+ unit: map['unit'] as String? ?? '',
+ );
+ }
+
+ Map toMap() {
+ return {'id': id, 'name': name, 'description': description, 'unit': unit};
+ }
+}
+
+class InventoryStock {
+ final String itemId;
+ final double count;
+
+ const InventoryStock({required this.itemId, required this.count});
+
+ factory InventoryStock.fromMap(Map map) {
+ return InventoryStock(
+ itemId: map['itemId'] as String? ?? '',
+ count: (map['count'] as num?)?.toDouble() ?? 0.0,
+ );
+ }
+
+ Map toMap() {
+ return {'itemId': itemId, 'count': count};
+ }
+}
+
+class Inventory {
+ final String id;
+ final List items;
+
+ const Inventory({required this.id, required this.items});
+
+ factory Inventory.fromMap(String id, Map map) {
+ final itemsList = map['items'] as List?;
+ return Inventory(
+ id: id,
+ items:
+ itemsList
+ ?.map(
+ (x) =>
+ InventoryStock.fromMap(Map.from(x as Map)),
+ )
+ .toList() ??
+ const [],
+ );
+ }
+
+ Map toMap() {
+ return {'items': items.map((x) => x.toMap()).toList()};
+ }
+}
+
+class RecipeIngredient {
+ final String itemId;
+ final double amountPer10People;
+
+ const RecipeIngredient({
+ required this.itemId,
+ required this.amountPer10People,
+ });
+
+ factory RecipeIngredient.fromMap(Map map) {
+ return RecipeIngredient(
+ itemId: map['itemId'] as String? ?? '',
+ amountPer10People: (map['amountPer10People'] as num?)?.toDouble() ?? 0.0,
+ );
+ }
+
+ Map toMap() {
+ return {'itemId': itemId, 'amountPer10People': amountPer10People};
+ }
+}
+
+class Recipe {
+ final String id;
+ final String name;
+ final String description;
+ final List ingredients;
+
+ const Recipe({
+ required this.id,
+ required this.name,
+ required this.description,
+ required this.ingredients,
+ });
+
+ factory Recipe.fromMap(String id, Map map) {
+ final ingredientsList = map['ingredients'] as List?;
+ return Recipe(
+ id: id,
+ name: map['name'] as String? ?? '',
+ description: map['description'] as String? ?? '',
+ ingredients:
+ ingredientsList
+ ?.map(
+ (x) => RecipeIngredient.fromMap(
+ Map.from(x as Map),
+ ),
+ )
+ .toList() ??
+ const [],
+ );
+ }
+
+ Map toMap() {
+ return {
+ 'id': id,
+ 'name': name,
+ 'description': description,
+ 'ingredients': ingredients.map((x) => x.toMap()).toList(),
+ };
+ }
+}
diff --git a/commis/app/lib/firebase_options.dart b/commis/app/lib/firebase_options.dart
new file mode 100644
index 00000000..2559d4d4
--- /dev/null
+++ b/commis/app/lib/firebase_options.dart
@@ -0,0 +1,91 @@
+// File generated by FlutterFire CLI.
+// ignore_for_file: type=lint
+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:
+ return windows;
+ 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.',
+ );
+ }
+ }
+
+ // NOTE: These are placeholder configurations. To generate a valid configuration
+ // for your own Firebase project, run the FlutterFire CLI:
+ //
+ // flutterfire configure
+ //
+ static const FirebaseOptions web = FirebaseOptions(
+ apiKey: 'YOUR-WEB-API-KEY',
+ appId: 'YOUR-WEB-APP-ID',
+ messagingSenderId: 'YOUR-MESSAGING-SENDER-ID',
+ projectId: 'YOUR-PROJECT-ID',
+ authDomain: 'YOUR-PROJECT-ID.firebaseapp.com',
+ storageBucket: 'YOUR-PROJECT-ID.firebasestorage.app',
+ );
+
+ static const FirebaseOptions android = FirebaseOptions(
+ apiKey: 'YOUR-ANDROID-API-KEY',
+ appId: 'YOUR-ANDROID-APP-ID',
+ messagingSenderId: 'YOUR-MESSAGING-SENDER-ID',
+ projectId: 'YOUR-PROJECT-ID',
+ storageBucket: 'YOUR-PROJECT-ID.firebasestorage.app',
+ );
+
+ static const FirebaseOptions ios = FirebaseOptions(
+ apiKey: 'YOUR-IOS-API-KEY',
+ appId: 'YOUR-IOS-APP-ID',
+ messagingSenderId: 'YOUR-MESSAGING-SENDER-ID',
+ projectId: 'YOUR-PROJECT-ID',
+ storageBucket: 'YOUR-PROJECT-ID.firebasestorage.app',
+ iosBundleId: 'com.example.commis',
+ );
+
+ static const FirebaseOptions macos = FirebaseOptions(
+ apiKey: 'YOUR-MACOS-API-KEY',
+ appId: 'YOUR-MACOS-APP-ID',
+ messagingSenderId: 'YOUR-MESSAGING-SENDER-ID',
+ projectId: 'YOUR-PROJECT-ID',
+ storageBucket: 'YOUR-PROJECT-ID.firebasestorage.app',
+ iosBundleId: 'com.example.commis',
+ );
+
+ static const FirebaseOptions windows = FirebaseOptions(
+ apiKey: 'YOUR-WINDOWS-API-KEY',
+ appId: 'YOUR-WINDOWS-APP-ID',
+ messagingSenderId: 'YOUR-MESSAGING-SENDER-ID',
+ projectId: 'YOUR-PROJECT-ID',
+ authDomain: 'YOUR-PROJECT-ID.firebaseapp.com',
+ storageBucket: 'YOUR-PROJECT-ID.firebasestorage.app',
+ );
+}
diff --git a/commis/app/lib/main.dart b/commis/app/lib/main.dart
new file mode 100644
index 00000000..d8c088b6
--- /dev/null
+++ b/commis/app/lib/main.dart
@@ -0,0 +1,137 @@
+// Copyright 2026 The Flutter Authors.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:cloud_firestore/cloud_firestore.dart';
+import 'package:cloud_functions/cloud_functions.dart';
+import 'package:firebase_core/firebase_core.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:genui/genui.dart';
+import 'package:go_router/go_router.dart';
+import 'package:provider/provider.dart';
+
+import 'data/firestore_repository.dart';
+import 'firebase_options.dart';
+import 'ui/screens/agent_screen.dart';
+import 'ui/screens/ingredients_list_screen.dart';
+import 'ui/screens/job_edit_screen.dart';
+import 'ui/screens/jobs_list_screen.dart';
+import 'ui/screens/navigation_shell.dart';
+import 'ui/screens/recipes_list_screen.dart';
+import 'ui/theme.dart';
+
+void main() async {
+ WidgetsFlutterBinding.ensureInitialized();
+
+ // Configure basic GenUI logging
+ configureLogging(
+ logCallback: (level, msg) => debugPrint('GenUI $level: $msg'),
+ );
+
+ try {
+ const bool useEmulators = bool.fromEnvironment(
+ 'USE_EMULATORS',
+ defaultValue: kDebugMode,
+ );
+
+ if (useEmulators) {
+ await Firebase.initializeApp(
+ options: DefaultFirebaseOptions.currentPlatform,
+ );
+ FirebaseFirestore.instance.useFirestoreEmulator('localhost', 8080);
+ FirebaseFunctions.instance.useFunctionsEmulator('localhost', 5001);
+ } else {
+ await Firebase.initializeApp(
+ options: DefaultFirebaseOptions.currentPlatform,
+ );
+ }
+
+ // Sign in anonymously for Firebase AI Logic
+ // await FirebaseAuth.instance.signInAnonymously();
+ } catch (e) {
+ debugPrint('Firebase initialization warning: $e');
+ }
+ runApp(
+ Provider(
+ create: (context) => FirestoreRepository(),
+ child: const MyApp(),
+ ),
+ );
+}
+
+final GoRouter _router = GoRouter(
+ initialLocation: '/jobs',
+ routes: [
+ StatefulShellRoute.indexedStack(
+ builder: (context, state, navigationShell) {
+ return ScaffoldWithNavBar(navigationShell: navigationShell);
+ },
+ branches: [
+ StatefulShellBranch(
+ routes: [
+ GoRoute(
+ path: '/jobs',
+ builder: (context, state) => const JobsListScreen(),
+ ),
+ ],
+ ),
+ StatefulShellBranch(
+ routes: [
+ GoRoute(
+ path: '/recipes',
+ builder: (context, state) => const RecipesListScreen(),
+ ),
+ ],
+ ),
+ StatefulShellBranch(
+ routes: [
+ GoRoute(
+ path: '/ingredients',
+ builder: (context, state) => const IngredientsListScreen(),
+ ),
+ ],
+ ),
+ StatefulShellBranch(
+ routes: [
+ GoRoute(
+ path: '/agent',
+ builder: (context, state) => const AgentScreen(),
+ ),
+ ],
+ ),
+ ],
+ ),
+ // Full-screen job edit screen outside of bottom bar shell
+ GoRoute(
+ path: '/jobs/edit/:jobId',
+ builder: (context, state) {
+ final jobId = state.pathParameters['jobId']!;
+ return JobEditScreen(jobId: jobId);
+ },
+ ),
+ ],
+);
+
+class MyApp extends StatelessWidget {
+ const MyApp({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp.router(
+ routerConfig: _router,
+ title: 'Commis Catering Assistant',
+ theme: ThemeData(
+ brightness: Brightness.dark,
+ scaffoldBackgroundColor: CommisColors.background,
+ colorScheme: const ColorScheme.dark(
+ primary: CommisColors.goldAccent,
+ surface: CommisColors.surfaceLevel1,
+ error: Colors.redAccent,
+ ),
+ useMaterial3: true,
+ ),
+ debugShowCheckedModeBanner: false,
+ );
+ }
+}
diff --git a/commis/app/lib/services/agent_service.dart b/commis/app/lib/services/agent_service.dart
new file mode 100644
index 00000000..2341a7e6
--- /dev/null
+++ b/commis/app/lib/services/agent_service.dart
@@ -0,0 +1,295 @@
+// Copyright 2026 The Flutter Authors.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:convert';
+
+import 'package:firebase_ai/firebase_ai.dart';
+import 'package:flutter/foundation.dart';
+import 'package:genui/genui.dart' hide TextPart;
+
+import '../../data/firestore_repository.dart';
+import '../../data/models.dart';
+
+abstract class AgentService {
+ Stream generateResponse(String prompt);
+}
+
+class FirebaseAILogicService implements AgentService {
+ late final ChatSession _chatSession;
+ final FirestoreRepository repository;
+
+ FirebaseAILogicService({
+ required this.repository,
+ required Catalog catalog,
+ String? cachedMessages,
+ }) {
+ final promptBuilder = PromptBuilder.chat(
+ catalog: catalog,
+ systemPromptFragments: [
+ systemInstruction,
+ if (cachedMessages != null)
+ 'These A2UI messages are already in place on the client:\n$cachedMessages\n\n',
+ ],
+ );
+
+ final prompt = promptBuilder.systemPromptJoined();
+
+ final model = FirebaseAI.googleAI().generativeModel(
+ model: 'gemini-flash-latest',
+ systemInstruction: Content.system(prompt),
+ tools: [
+ Tool.functionDeclarations([
+ FunctionDeclaration(
+ 'fetchCateringJobs',
+ 'Fetches the list of active catering jobs in the system, returning their IDs and titles.',
+ parameters: {},
+ ),
+ FunctionDeclaration(
+ 'fetchRecipesForJob',
+ 'Fetches the list of recipes (with all fields) associated with a given jobId.',
+ parameters: {
+ 'jobId': Schema.string(
+ description:
+ 'The unique ID of the catering job to fetch recipes for.',
+ ),
+ },
+ ),
+ FunctionDeclaration(
+ 'fetchIngredientsForRecipe',
+ 'Fetches the list of ingredients (with all fields) associated with a given recipeId.',
+ parameters: {
+ 'recipeId': Schema.string(
+ description:
+ 'The unique ID of the recipe to fetch ingredients for.',
+ ),
+ },
+ ),
+ FunctionDeclaration(
+ 'updateCateringJob',
+ 'Updates an existing catering job with the provided fields.',
+ parameters: {
+ 'id': Schema.string(
+ description:
+ 'The unique ID of the catering job to update (required).',
+ ),
+ 'title': Schema.string(
+ description:
+ 'The updated title of the catering job (optional).',
+ ),
+ 'address': Schema.string(
+ description:
+ 'The updated address of the catering event (optional).',
+ ),
+ 'guestCount': Schema.integer(
+ description: 'The updated guest count of the event (optional).',
+ ),
+ 'date': Schema.string(
+ description:
+ 'The updated scheduled ISO 8601 date string of the event (optional).',
+ ),
+ 'recipeIds': Schema.array(
+ items: Schema.string(),
+ description:
+ 'The updated list of recipe IDs associated with this job (optional).',
+ ),
+ 'latitude': Schema.number(
+ description:
+ 'The updated latitude coordinate of the event (optional).',
+ ),
+ 'longitude': Schema.number(
+ description:
+ 'The updated longitude coordinate of the event (optional).',
+ ),
+ },
+ optionalParameters: [
+ 'title',
+ 'address',
+ 'guestCount',
+ 'date',
+ 'recipeIds',
+ 'latitude',
+ 'longitude',
+ ],
+ ),
+ ]),
+ ],
+ );
+ _chatSession = model.startChat();
+ }
+
+ Future _fetchCateringJobs() async {
+ debugPrint('_fetchCateringJobs called');
+ final jobs = await repository.getJobs();
+
+ final jobsList = jobs.map((job) {
+ return {
+ 'id': job.id,
+ 'title': job.title,
+ 'address': job.address,
+ 'guestCount': job.guestCount,
+ 'date': job.date.toIso8601String(),
+ 'recipeIds': job.recipeIds,
+ };
+ }).toList();
+
+ return jsonEncode(jobsList);
+ }
+
+ Future _fetchRecipesForJob(String jobId) async {
+ debugPrint('_fetchRecipesForJob called with jobId: $jobId');
+ final recipes = await repository.getRecipesForJob(jobId);
+ final result = recipes.map((r) => r.toMap()).toList();
+ return jsonEncode(result);
+ }
+
+ Future _fetchIngredientsForRecipe(String recipeId) async {
+ debugPrint('_fetchIngredientsForRecipe called with recipeId: $recipeId');
+ final recipe = await repository.getRecipe(recipeId);
+ if (recipe == null) return '[]';
+
+ final ingredients = await repository.getIngredientsForRecipe(recipeId);
+
+ final result = recipe.ingredients.map((ri) {
+ final item = ingredients.cast().firstWhere(
+ (ii) => ii?.id == ri.itemId,
+ orElse: () => null,
+ );
+ return {
+ 'itemId': ri.itemId,
+ 'amountPer10People': ri.amountPer10People,
+ 'name': item?.name ?? '',
+ 'description': item?.description ?? '',
+ 'unit': item?.unit ?? '',
+ };
+ }).toList();
+
+ return jsonEncode(result);
+ }
+
+ Future _updateCateringJob(Map args) async {
+ debugPrint('_updateCateringJob called with args: $args');
+ try {
+ final id = args['id'] as String;
+ final existingJob = await repository.getJob(id);
+ if (existingJob == null) {
+ return 'Error: Catering Job with ID "$id" not found.';
+ }
+
+ final updatedJob = CateringJob(
+ id: id,
+ title: (args['title'] as String?)?.trim() ?? existingJob.title,
+ address: (args['address'] as String?)?.trim() ?? existingJob.address,
+ guestCount:
+ (args['guestCount'] as num?)?.toInt() ?? existingJob.guestCount,
+ date: args['date'] != null
+ ? DateTime.parse(args['date'] as String)
+ : existingJob.date,
+ recipeIds: args['recipeIds'] != null
+ ? List.from(args['recipeIds'] as List)
+ : existingJob.recipeIds,
+ latitude:
+ (args['latitude'] as num?)?.toDouble() ?? existingJob.latitude,
+ longitude:
+ (args['longitude'] as num?)?.toDouble() ?? existingJob.longitude,
+ );
+
+ await repository.updateJob(updatedJob);
+ return 'Success: Catering Job "$id" updated successfully.';
+ } catch (e) {
+ return 'Error: Failed to update catering job: $e';
+ }
+ }
+
+ @override
+ Stream generateResponse(String prompt) async* {
+ if (prompt.isEmpty) return;
+
+ var response = await _chatSession.sendMessage(
+ Content('user', [TextPart(prompt)]),
+ );
+
+ while (response.functionCalls.isNotEmpty) {
+ for (final call in response.functionCalls) {
+ if (call.name == 'fetchCateringJobs') {
+ final result = await _fetchCateringJobs();
+ response = await _chatSession.sendMessage(
+ Content.functionResponse(call.name, {'jobs': result}),
+ );
+ } else if (call.name == 'fetchRecipesForJob') {
+ final jobId = call.args['jobId'] as String;
+ final result = await _fetchRecipesForJob(jobId);
+ response = await _chatSession.sendMessage(
+ Content.functionResponse(call.name, {'recipes': result}),
+ );
+ } else if (call.name == 'fetchIngredientsForRecipe') {
+ final recipeId = call.args['recipeId'] as String;
+ final result = await _fetchIngredientsForRecipe(recipeId);
+ response = await _chatSession.sendMessage(
+ Content.functionResponse(call.name, {'ingredients': result}),
+ );
+ } else if (call.name == 'updateCateringJob') {
+ final result = await _updateCateringJob(call.args);
+ response = await _chatSession.sendMessage(
+ Content.functionResponse(call.name, {'result': result}),
+ );
+ }
+ }
+ }
+
+ yield response.text ?? '';
+ }
+}
+
+const systemInstruction = '''
+You are Commis, the intelligent kitchen and catering assistant.
+Your job is to help plan catering jobs, including recipes and ingredients.
+You are friendly, but very concise. Speak like a subordinate in a commercial kitchen.
+
+Specifically, you can help with the following things:
+* Look up data on upcoming jobs.
+* Modify the details of a catering job, including the recipes assigned to it.
+* Solve problems by suggesting changes to job details, such as recipe assignments, guest counts, dates, and addresses.
+
+**YOU CANNOT DO ANY OF THE FOLLOWING THREE THINGS:**
+* Suggest modifications to individual recipes.
+* Suggest modifications to individual ingredients.
+* Suggest new recipes, ingredients, or catering jobs.
+
+You are equipped with the following tools:
+
+1. "fetchCateringJobs": Call this to get the list of active catering jobs
+ in the system.
+2. "fetchRecipesForJob(jobId)": Call this to get the details of recipes
+ associated with a specific catering job.
+3. "fetchIngredientsForRecipe(recipeId)": Call this to get the ingredients
+ list (with all fields) for a specific recipe.
+4. "updateCateringJob(id, title, address, guestCount, date,
+ recipeIds, latitude, longitude)": Call this to update an existing
+ catering job with the provided fields.
+
+When I send you a message, use A2UI to create new UI components, which will be displayed chronologically.
+Feel free to compose multiple components together, such as a SimpleCard
+containing a Column that contains multiple RecipeLine components. If
+you are suggesting a course of action and requesting my approval, include
+a button that will send an event to you indicating that the action is approved.
+When you are composing UI using Row, Column, Text, Divider, and Button, try
+to use a SimpleCard to hold the Row or Column with the other components inside it.
+
+If you can't find a way to express yourself correctly using A2UI, you may
+use short, direct text responses.
+
+** PREFER TO UPDATE EXISTING UI COMPONENTS RATHER THAN CREATE NEW ONES. **
+
+**DO NOT REPEAT YOURSELF**
+If you create a UI component to convey information, do not also provide commentary
+in the form of plain text outside the A2UI messages in your response.
+
+**BE BRIEF! DO NOT USE MORE UI COMPONENTS THAN NECESSARY TO TELL ME WHAT YOU RECOMMEND AND GET MY APPROVAL.**
+
+If you are asked about the cost of ingredients, use the "calculateCost" function
+to display the cost.
+
+When you generate UI components, use "commis_catalog" as the name of your
+catalog.
+''';
diff --git a/commis/app/lib/services/cost_service.dart b/commis/app/lib/services/cost_service.dart
new file mode 100644
index 00000000..b843defb
--- /dev/null
+++ b/commis/app/lib/services/cost_service.dart
@@ -0,0 +1,103 @@
+// Copyright 2026 The Flutter Authors.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+class CostService {
+ static const Map ingredientPrices = {
+ 'active_dry_yeast': 0.75,
+ 'all_purpose_flour': 0.50,
+ 'baby_spinach': 1.99,
+ 'baking_powder': 0.40,
+ 'baking_soda': 0.30,
+ 'balsamic_vinegar': 2.50,
+ 'bay_leaves': 0.15,
+ 'beef_stock': 1.80,
+ 'black_beans': 0.99,
+ 'black_peppercorns': 0.05,
+ 'brown_sugar_dark': 0.65,
+ 'canned_crushed_tomatoes': 1.25,
+ 'canola_oil': 0.80,
+ 'canola_oil_spray': 1.50,
+ 'carrots': 0.45,
+ 'cayenne_pepper': 0.10,
+ 'celery_stalks': 0.55,
+ 'cheddar_block': 3.20,
+ 'chicken_breast': 4.50,
+ 'chicken_stock': 1.50,
+ 'corn_starch': 0.35,
+ 'corn_tortillas': 0.08,
+ 'cream_cheese': 2.10,
+ 'cremini_mushrooms': 2.25,
+ 'crushed_red_pepper': 0.08,
+ 'dijon_mustard': 1.20,
+ 'dried_oregano': 0.12,
+ 'dried_rosemary': 0.15,
+ 'dried_thyme': 0.14,
+ 'firm_tofu': 1.75,
+ 'flank_steak': 8.99,
+ 'flour_tortillas': 0.12,
+ 'fresh_basil': 1.50,
+ 'fresh_cilantro': 0.99,
+ 'fresh_lemons': 0.60,
+ 'fresh_limes': 0.50,
+ 'fresh_mint': 1.25,
+ 'fresh_parsley': 0.85,
+ 'fresh_strawberries': 2.99,
+ 'fuji_apples': 0.95,
+ 'garlic_bulbs': 0.50,
+ 'ginger_root': 1.10,
+ 'goat_cheese_logs': 3.75,
+ 'granulated_sugar': 0.45,
+ 'greek_yogurt': 1.80,
+ 'green_bell_peppers': 0.85,
+ 'ground_beef': 5.50,
+ 'ground_cinnamon': 0.12,
+ 'ground_cumin': 0.10,
+ 'ground_nutmeg': 0.15,
+ 'hass_avocados': 1.25,
+ 'heavy_whipping_cream': 2.40,
+ 'jasmine_rice': 0.70,
+ 'jumbo_shrimp': 12.99,
+ 'kosher_salt': 0.05,
+ 'kosher_salt_coarse': 0.06,
+ 'large_eggs': 0.30,
+ 'maple_syrup': 4.50,
+ 'mayonnaise': 1.50,
+ 'micro_greens': 2.99,
+ 'mozzarella_logs': 3.99,
+ 'olive_oil_extra_virgin': 3.50,
+ 'onion_red': 0.65,
+ 'panko_breadcrumbs': 1.10,
+ 'parmesan_cheese': 4.25,
+ 'penne_pasta': 1.20,
+ 'pinto_beans': 0.89,
+ 'pork_shoulder': 4.99,
+ 'red_bell_peppers': 1.10,
+ 'red_wine_vinegar': 1.99,
+ 'roma_tomatoes': 0.75,
+ 'russet_potatoes': 0.50,
+ 'salmon_fillets': 10.99,
+ 'scallions': 0.60,
+ 'sliced_prosciutto': 5.99,
+ 'sour_cream': 1.25,
+ 'soy_sauce': 0.95,
+ 'sriracha_sauce': 1.75,
+ 'sweet_paprika': 0.12,
+ 'thick_bacon': 6.50,
+ 'tomato_paste': 0.80,
+ 'unsalted_butter': 2.20,
+ 'vanilla_extract': 8.50,
+ 'vegetable_stock': 1.60,
+ 'white_quinoa': 1.50,
+ 'white_sugar_powder': 0.60,
+ 'whole_milk': 1.20,
+ 'wild_arugula': 2.10,
+ 'wildflower_honey': 3.99,
+ 'yellow_onions': 0.50,
+ };
+
+ double fetchPrice(String ingredientId, double quantity) {
+ final pricePerUnit = ingredientPrices[ingredientId] ?? 0.0;
+ return pricePerUnit * quantity;
+ }
+}
diff --git a/commis/app/lib/ui/catalog/catalog.dart b/commis/app/lib/ui/catalog/catalog.dart
new file mode 100644
index 00000000..6cf8bd75
--- /dev/null
+++ b/commis/app/lib/ui/catalog/catalog.dart
@@ -0,0 +1,35 @@
+// Copyright 2026 The Flutter Authors.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:commis/ui/catalog/client_functions.dart';
+import 'package:commis/ui/catalog/simple_card.dart';
+import 'package:genui/genui.dart';
+
+import 'catering_job.dart';
+import 'ingredient_line.dart';
+import 'navigation_card.dart';
+import 'recipe_line.dart';
+
+// --- The Commis Catalog ---
+
+final commisCatalog =
+ Catalog(
+ [
+ cateringJobItem,
+ recipeLineCatalogItem,
+ ingredientLineCatalogItem,
+ navigationCardCatalogItem,
+ simpleCardCatalogItem,
+ ],
+ functions: [CalculateCostFunction()],
+ catalogId: 'commis_catalog',
+ ).copyWith(
+ newItems: [
+ BasicCatalogItems.column,
+ BasicCatalogItems.text,
+ BasicCatalogItems.button,
+ BasicCatalogItems.row,
+ BasicCatalogItems.divider,
+ ],
+ );
diff --git a/commis/app/lib/ui/catalog/catering_job.dart b/commis/app/lib/ui/catalog/catering_job.dart
new file mode 100644
index 00000000..589f33d9
--- /dev/null
+++ b/commis/app/lib/ui/catalog/catering_job.dart
@@ -0,0 +1,114 @@
+// Copyright 2026 The Flutter Authors.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:genui/genui.dart';
+import 'package:google_fonts/google_fonts.dart';
+import 'package:json_schema_builder/json_schema_builder.dart';
+import 'package:provider/provider.dart';
+
+import '../../data/firestore_repository.dart';
+import '../../data/models.dart';
+import '../theme.dart';
+import '../widgets/job_card.dart';
+
+extension type CateringJobItemData.fromMap(JsonMap json) {
+ String get jobId => json['jobId'] as String;
+}
+
+final cateringJobItem = CatalogItem(
+ name: 'CateringJob',
+ dataSchema: S.object(
+ description:
+ 'Displays a single catering job card with its date, guest count, and associated recipes.',
+ properties: {
+ 'jobId': S.string(
+ description: 'The unique ID of the catering job to display.',
+ ),
+ },
+ required: ['jobId'],
+ ),
+ widgetBuilder: (itemContext) {
+ final data = CateringJobItemData.fromMap(itemContext.data as JsonMap);
+ return SurfaceJobCardLoader(jobId: data.jobId);
+ },
+);
+
+class SurfaceJobCardLoader extends StatelessWidget {
+ final String jobId;
+
+ const SurfaceJobCardLoader({required this.jobId, super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final repository = context.read();
+
+ return StreamBuilder>(
+ stream: repository.watchRecipes(),
+ builder: (context, recipesSnapshot) {
+ if (recipesSnapshot.connectionState == ConnectionState.waiting) {
+ return const Center(
+ child: Padding(
+ padding: EdgeInsets.symmetric(vertical: 24.0),
+ child: CircularProgressIndicator(color: CommisColors.goldAccent),
+ ),
+ );
+ }
+
+ if (recipesSnapshot.hasError) {
+ return Center(
+ child: Text(
+ 'Error loading recipes: ${recipesSnapshot.error}',
+ style: GoogleFonts.sourceSans3(color: Colors.redAccent),
+ ),
+ );
+ }
+
+ final recipes = recipesSnapshot.data ?? const [];
+
+ return StreamBuilder(
+ stream: repository.watchJob(jobId),
+ builder: (context, jobSnapshot) {
+ if (jobSnapshot.connectionState == ConnectionState.waiting) {
+ return const Center(
+ child: Padding(
+ padding: EdgeInsets.symmetric(vertical: 24.0),
+ child: CircularProgressIndicator(
+ color: CommisColors.goldAccent,
+ ),
+ ),
+ );
+ }
+
+ if (jobSnapshot.hasError) {
+ return Center(
+ child: Text(
+ 'Error loading job: ${jobSnapshot.error}',
+ style: GoogleFonts.sourceSans3(color: Colors.redAccent),
+ ),
+ );
+ }
+
+ final job = jobSnapshot.data;
+ if (job == null) {
+ return Center(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 24.0),
+ child: Text(
+ 'Job "$jobId" not found.',
+ style: GoogleFonts.sourceSans3(
+ color: CommisColors.textSecondary,
+ ),
+ ),
+ ),
+ );
+ }
+
+ return JobCard(job: job, allRecipes: recipes);
+ },
+ );
+ },
+ );
+ }
+}
diff --git a/commis/app/lib/ui/catalog/client_functions.dart b/commis/app/lib/ui/catalog/client_functions.dart
new file mode 100644
index 00000000..7217cca7
--- /dev/null
+++ b/commis/app/lib/ui/catalog/client_functions.dart
@@ -0,0 +1,44 @@
+// Copyright 2026 The Flutter Authors.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:commis/services/cost_service.dart';
+import 'package:genui/genui.dart';
+import 'package:json_schema_builder/json_schema_builder.dart';
+
+class CalculateCostFunction extends SynchronousClientFunction {
+ const CalculateCostFunction();
+
+ @override
+ String get name => 'calculateCost';
+
+ @override
+ String get description =>
+ 'Calculates the cost for a certain quantity '
+ 'of an ingredient. Returns a formatted dollar string (i.e. \$4.50).';
+
+ @override
+ ClientFunctionReturnType get returnType => .string;
+
+ @override
+ Schema get argumentSchema => S.object(
+ properties: {
+ 'ingredient_id': S.string(description: 'The ID of the ingredient.'),
+ 'quantity': S.number(description: 'The quantity of the ingredient.'),
+ },
+ required: ['ingredient_id', 'quantity'],
+ );
+
+ @override
+ Object? executeSync(JsonMap args, ExecutionContext context) {
+ final ingredientId = args['ingredient_id'].toString();
+ final quantity = num.tryParse(args['quantity'].toString())?.toDouble();
+
+ if (quantity == null || quantity < 1) {
+ return '\$0.00';
+ }
+
+ final cost = CostService().fetchPrice(ingredientId, quantity);
+ return '\$${cost.toStringAsFixed(2)}';
+ }
+}
diff --git a/commis/app/lib/ui/catalog/ingredient_line.dart b/commis/app/lib/ui/catalog/ingredient_line.dart
new file mode 100644
index 00000000..3d2201e2
--- /dev/null
+++ b/commis/app/lib/ui/catalog/ingredient_line.dart
@@ -0,0 +1,191 @@
+// Copyright 2026 The Flutter Authors.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:genui/genui.dart';
+import 'package:google_fonts/google_fonts.dart';
+import 'package:json_schema_builder/json_schema_builder.dart';
+import 'package:provider/provider.dart';
+
+import '../../data/firestore_repository.dart';
+import '../../data/models.dart';
+import '../theme.dart';
+
+// --- Extension Types ---
+
+extension type IngredientLineItemData.fromMap(JsonMap json) {
+ String get ingredientId => json['ingredientId'] as String;
+}
+
+// --- GenUI Catalog Item ---
+
+final ingredientLineCatalogItem = CatalogItem(
+ name: 'IngredientLine',
+ dataSchema: S.object(
+ description:
+ 'Displays a single ingredient with its name, description, unit, cost, and image.',
+ properties: {
+ 'ingredientId': S.string(
+ description: 'The unique ID of the ingredient to display.',
+ ),
+ },
+ required: ['ingredientId'],
+ ),
+ widgetBuilder: (itemContext) {
+ final data = IngredientLineItemData.fromMap(itemContext.data as JsonMap);
+ return SurfaceIngredientLoader(ingredientId: data.ingredientId);
+ },
+);
+
+// --- Custom Loader Widget ---
+
+class SurfaceIngredientLoader extends StatelessWidget {
+ final String ingredientId;
+
+ const SurfaceIngredientLoader({required this.ingredientId, super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final repository = context.read();
+
+ return StreamBuilder(
+ stream: repository.watchIngredient(ingredientId),
+ builder: (context, snapshot) {
+ if (snapshot.connectionState == ConnectionState.waiting) {
+ return const Center(
+ child: Padding(
+ padding: EdgeInsets.symmetric(vertical: 16.0),
+ child: CircularProgressIndicator(color: CommisColors.goldAccent),
+ ),
+ );
+ }
+
+ if (snapshot.hasError) {
+ return Center(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 16.0),
+ child: Text(
+ 'Error loading ingredient: ${snapshot.error}',
+ style: GoogleFonts.sourceSans3(color: Colors.redAccent),
+ ),
+ ),
+ );
+ }
+
+ final ingredient = snapshot.data;
+ if (ingredient == null) {
+ return Center(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 16.0),
+ child: Text(
+ 'Ingredient "$ingredientId" not found.',
+ style: GoogleFonts.sourceSans3(
+ color: CommisColors.textSecondary,
+ ),
+ ),
+ ),
+ );
+ }
+
+ return IngredientLine(ingredient: ingredient);
+ },
+ );
+ }
+}
+
+// --- Mini Ingredient Card Widget ---
+
+class IngredientLine extends StatelessWidget {
+ final Ingredient ingredient;
+
+ const IngredientLine({required this.ingredient, super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final assetPath = 'assets/images/ingredients/200/${ingredient.id}.png';
+
+ return Card(
+ color: CommisColors.surfaceLevel1,
+ elevation: 0.0,
+ margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
+ child: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Left: Ingredient image thumbnail
+ Container(
+ width: 60.0,
+ height: 60.0,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(12.0),
+ border: Border.all(color: CommisColors.borderLowContrast),
+ ),
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(11.0),
+ child: Image.asset(
+ assetPath,
+ fit: BoxFit.cover,
+ errorBuilder: (context, error, stackTrace) {
+ return Container(
+ color: CommisColors.surfaceLevel2,
+ child: const Center(
+ child: Icon(
+ Icons.kitchen,
+ color: CommisColors.goldAccentDim,
+ size: 22.0,
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+ ),
+ const SizedBox(width: 16.0),
+ // Right: Name, Description, Unit, and Cost column
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ ingredient.name,
+ style: GoogleFonts.outfit(
+ color: CommisColors.textPrimary,
+ fontSize: 18.0,
+ fontWeight: FontWeight.w700,
+ ),
+ ),
+ const SizedBox(height: 4.0),
+ Text(
+ ingredient.description,
+ style: GoogleFonts.sourceSans3(
+ color: CommisColors.textSecondary,
+ fontSize: 13.0,
+ height: 1.3,
+ ),
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
+ ),
+ const SizedBox(height: 8.0),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ 'Unit: ${ingredient.unit}',
+ style: GoogleFonts.sourceSans3(
+ color: CommisColors.textSecondary,
+ fontSize: 12.0,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/commis/app/lib/ui/catalog/navigation_card.dart b/commis/app/lib/ui/catalog/navigation_card.dart
new file mode 100644
index 00000000..dca9d239
--- /dev/null
+++ b/commis/app/lib/ui/catalog/navigation_card.dart
@@ -0,0 +1,133 @@
+// Copyright 2026 The Flutter Authors.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:genui/genui.dart';
+import 'package:google_fonts/google_fonts.dart';
+import 'package:google_maps_flutter/google_maps_flutter.dart';
+import 'package:json_schema_builder/json_schema_builder.dart';
+
+import '../theme.dart';
+
+// --- Extension Types ---
+
+extension type NavigationCardItemData.fromMap(JsonMap json) {
+ String get title => json['title'] as String;
+ String get address => json['address'] as String;
+ double get latitude => (json['latitude'] as num).toDouble();
+ double get longitude => (json['longitude'] as num).toDouble();
+}
+
+// --- GenUI Catalog Item ---
+
+final navigationCardCatalogItem = CatalogItem(
+ name: 'NavigationCard',
+ dataSchema: S.object(
+ description:
+ 'Displays a navigation card showing job location, title, and a Google Map widget.',
+ properties: {
+ 'title': S.string(description: 'Title of the job.'),
+ 'address': S.string(description: 'Address of the job.'),
+ 'latitude': S.number(description: 'Latitude of the job location.'),
+ 'longitude': S.number(description: 'Longitude of the job location.'),
+ },
+ required: ['title', 'address', 'latitude', 'longitude'],
+ ),
+ widgetBuilder: (itemContext) {
+ final data = NavigationCardItemData.fromMap(itemContext.data as JsonMap);
+ return NavigationCard(data: data);
+ },
+);
+
+// --- Navigation Card Widget ---
+
+class NavigationCard extends StatelessWidget {
+ final NavigationCardItemData data;
+
+ const NavigationCard({required this.data, super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final position = LatLng(data.latitude, data.longitude);
+
+ return Card(
+ color: CommisColors.surfaceLevel1,
+ elevation: 0.0,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(16.0),
+ side: const BorderSide(
+ color: CommisColors.borderLowContrast,
+ width: 1.0,
+ ),
+ ),
+ margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
+ child: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ data.title,
+ style: GoogleFonts.outfit(
+ color: CommisColors.textPrimary,
+ fontSize: 18.0,
+ fontWeight: FontWeight.w700,
+ ),
+ ),
+ const SizedBox(height: 8.0),
+ Row(
+ children: [
+ const Icon(
+ Icons.location_on_outlined,
+ size: 16.0,
+ color: CommisColors.goldAccent,
+ ),
+ const SizedBox(width: 6.0),
+ Expanded(
+ child: Text(
+ data.address,
+ style: GoogleFonts.sourceSans3(
+ color: CommisColors.textSecondary,
+ fontSize: 14.0,
+ ),
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 16.0),
+ Container(
+ height: 180.0,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(12.0),
+ border: Border.all(
+ color: CommisColors.borderLowContrast,
+ width: 1.0,
+ ),
+ ),
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(11.0),
+ child: GoogleMap(
+ initialCameraPosition: CameraPosition(
+ target: position,
+ zoom: 14.0,
+ ),
+ markers: {
+ Marker(
+ markerId: const MarkerId('job_location'),
+ position: position,
+ ),
+ },
+ zoomControlsEnabled: false,
+ mapToolbarEnabled: false,
+ myLocationButtonEnabled: false,
+ compassEnabled: false,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/commis/app/lib/ui/catalog/recipe_line.dart b/commis/app/lib/ui/catalog/recipe_line.dart
new file mode 100644
index 00000000..82ea5b0b
--- /dev/null
+++ b/commis/app/lib/ui/catalog/recipe_line.dart
@@ -0,0 +1,178 @@
+// Copyright 2026 The Flutter Authors.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:genui/genui.dart';
+import 'package:google_fonts/google_fonts.dart';
+import 'package:json_schema_builder/json_schema_builder.dart';
+import 'package:provider/provider.dart';
+
+import '../../data/firestore_repository.dart';
+import '../../data/models.dart';
+import '../theme.dart';
+
+// --- Extension Types ---
+
+extension type RecipeLineItemData.fromMap(JsonMap json) {
+ String get recipeId => json['recipeId'] as String;
+}
+
+// --- GenUI Catalog Item ---
+
+final recipeLineCatalogItem = CatalogItem(
+ name: 'RecipeLine',
+ dataSchema: S.object(
+ description:
+ 'Displays a single recipe with its name, description, and image.',
+ properties: {
+ 'recipeId': S.string(
+ description: 'The unique ID of the recipe to display.',
+ ),
+ },
+ required: ['recipeId'],
+ ),
+ widgetBuilder: (itemContext) {
+ final data = RecipeLineItemData.fromMap(itemContext.data as JsonMap);
+ return SurfaceRecipeLoader(recipeId: data.recipeId);
+ },
+);
+
+// --- Custom Loader Widget ---
+
+class SurfaceRecipeLoader extends StatelessWidget {
+ final String recipeId;
+
+ const SurfaceRecipeLoader({required this.recipeId, super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final repository = context.read();
+
+ return StreamBuilder(
+ stream: repository.watchRecipe(recipeId),
+ builder: (context, snapshot) {
+ if (snapshot.connectionState == ConnectionState.waiting) {
+ return const Center(
+ child: Padding(
+ padding: EdgeInsets.symmetric(vertical: 16.0),
+ child: CircularProgressIndicator(color: CommisColors.goldAccent),
+ ),
+ );
+ }
+
+ if (snapshot.hasError) {
+ return Center(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 16.0),
+ child: Text(
+ 'Error loading recipe: ${snapshot.error}',
+ style: GoogleFonts.sourceSans3(color: Colors.redAccent),
+ ),
+ ),
+ );
+ }
+
+ final recipe = snapshot.data;
+ if (recipe == null) {
+ return Center(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 16.0),
+ child: Text(
+ 'Recipe "$recipeId" not found.',
+ style: GoogleFonts.sourceSans3(
+ color: CommisColors.textSecondary,
+ ),
+ ),
+ ),
+ );
+ }
+
+ return RecipeLine(recipe: recipe);
+ },
+ );
+ }
+}
+
+// --- Mini Recipe Card Widget ---
+
+class RecipeLine extends StatelessWidget {
+ final Recipe recipe;
+
+ const RecipeLine({required this.recipe, super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final assetPath = 'assets/images/recipes/200/${recipe.id}.png';
+
+ return Card(
+ color: CommisColors.surfaceLevel1,
+ elevation: 0.0,
+ margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
+ child: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Left: Recipe image thumbnail
+ Container(
+ width: 60.0,
+ height: 60.0,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(12.0),
+ border: Border.all(color: CommisColors.borderLowContrast),
+ ),
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(11.0),
+ child: Image.asset(
+ assetPath,
+ fit: BoxFit.cover,
+ errorBuilder: (context, error, stackTrace) {
+ return Container(
+ color: CommisColors.surfaceLevel2,
+ child: const Center(
+ child: Icon(
+ Icons.restaurant,
+ color: CommisColors.goldAccentDim,
+ size: 22.0,
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+ ),
+ const SizedBox(width: 16.0),
+ // Right: Name & Description column
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ recipe.name,
+ style: GoogleFonts.outfit(
+ color: CommisColors.textPrimary,
+ fontSize: 18.0,
+ fontWeight: FontWeight.w700,
+ ),
+ ),
+ const SizedBox(height: 4.0),
+ Text(
+ recipe.description,
+ style: GoogleFonts.sourceSans3(
+ color: CommisColors.textSecondary,
+ fontSize: 13.0,
+ height: 1.3,
+ ),
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/commis/app/lib/ui/catalog/simple_card.dart b/commis/app/lib/ui/catalog/simple_card.dart
new file mode 100644
index 00000000..ef3ac32a
--- /dev/null
+++ b/commis/app/lib/ui/catalog/simple_card.dart
@@ -0,0 +1,51 @@
+// Copyright 2025 The Flutter Authors.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:genui/genui.dart';
+import 'package:json_schema_builder/json_schema_builder.dart';
+
+import '../theme.dart';
+
+final _schema = S.object(
+ description:
+ 'A visual container (card) that groups a single child widget. Can be used with a Column as its child.',
+ properties: {'child': A2uiSchemas.componentReference()},
+ required: ['child'],
+);
+
+extension type _SimpleCardData.fromMap(JsonMap _json) {
+ factory _SimpleCardData({required String child}) =>
+ _SimpleCardData.fromMap({'child': child});
+
+ String get child {
+ final Object? val = _json['child'];
+ if (val is String) return val;
+ throw ArgumentError('Invalid child: $val');
+ }
+}
+
+final simpleCardCatalogItem = CatalogItem(
+ name: 'SimpleCard',
+ dataSchema: _schema,
+ widgetBuilder: (itemContext) {
+ final simpleCardData = _SimpleCardData.fromMap(itemContext.data as JsonMap);
+ return Card(
+ color: CommisColors.surfaceLevel1,
+ elevation: 0.0,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(16.0),
+ side: const BorderSide(
+ color: CommisColors.borderLowContrast,
+ width: 1.0,
+ ),
+ ),
+ margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
+ child: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: itemContext.buildChild(simpleCardData.child),
+ ),
+ );
+ },
+);
diff --git a/commis/app/lib/ui/screens/agent_screen.dart b/commis/app/lib/ui/screens/agent_screen.dart
new file mode 100644
index 00000000..44b6e8de
--- /dev/null
+++ b/commis/app/lib/ui/screens/agent_screen.dart
@@ -0,0 +1,336 @@
+// Copyright 2026 The Flutter Authors.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:genui/genui.dart';
+import 'package:google_fonts/google_fonts.dart';
+import 'package:provider/provider.dart';
+
+import '../../data/firestore_repository.dart';
+import '../../services/agent_service.dart';
+import '../catalog/catalog.dart';
+import '../theme.dart';
+import '../widgets/message_bubble.dart';
+
+class AgentScreen extends StatefulWidget {
+ const AgentScreen({super.key});
+
+ @override
+ State createState() => _AgentScreenState();
+}
+
+class _AgentScreenState extends State {
+ late final Catalog _catalog;
+ late final SurfaceController _controller;
+ late final A2uiTransportAdapter _transport;
+ late final Conversation _conversation;
+ AgentService? _agentService;
+
+ final List _chatMessages = [];
+ final TextEditingController _inputController = TextEditingController();
+ final ScrollController _scrollController = ScrollController();
+ bool _isWaiting = true;
+
+ @override
+ void initState() {
+ super.initState();
+
+ // 1. Initialize GenUI Catalog & Controllers
+ _catalog = commisCatalog;
+ _controller = SurfaceController(catalogs: [_catalog]);
+ _transport = A2uiTransportAdapter(onSend: _sendAndReceive);
+ _conversation = Conversation(
+ controller: _controller,
+ transport: _transport,
+ );
+
+ // 4. Listen to Conversation events to drive our chat UI
+ _conversation.events.listen((event) {
+ if (event is ConversationContentReceived) {
+ setState(() {
+ _isWaiting = false;
+ // Append text chunk to the last model message if applicable
+ final latestMessage = _chatMessages.isNotEmpty
+ ? _chatMessages.last
+ : null;
+ if (latestMessage is TextContent && !latestMessage.isUser) {
+ latestMessage.text += ' ${event.text}';
+ } else {
+ _chatMessages.add(TextContent(text: event.text, isUser: false));
+ }
+ });
+ } else if (event is ConversationSurfaceAdded) {
+ setState(() {
+ _isWaiting = false;
+ _chatMessages.add(SurfaceContent(surfaceId: event.surfaceId));
+ });
+ } else if (event is ConversationWaiting) {
+ setState(() {
+ _isWaiting = true;
+ });
+ } else if (event is ConversationError) {
+ setState(() {
+ _isWaiting = false;
+ _chatMessages.add(
+ TextContent(
+ text: 'An error occurred: ${event.error}',
+ isUser: false,
+ ),
+ );
+ });
+ }
+ });
+
+ _initAgent();
+ }
+
+ Future _initAgent() async {
+ final repository = context.read();
+
+ String? feeds;
+
+ try {
+ final jobs = await repository.getJobs();
+ final feedFutures = jobs.map((job) => repository.getFeedMessage(job.id));
+ final results = await Future.wait(feedFutures);
+ feeds = results
+ .map((r) => r?.trim() ?? '')
+ .where((r) => r.isNotEmpty)
+ .join('\n\n');
+ } catch (e) {
+ debugPrint('Error initializing agent: $e');
+ }
+
+ _agentService = FirebaseAILogicService(
+ repository: repository,
+ catalog: _catalog,
+ cachedMessages: feeds,
+ );
+
+ if (feeds != null) {
+ _transport.addChunk(feeds);
+ }
+
+ setState(() => _isWaiting = false);
+ }
+
+ @override
+ void dispose() {
+ _conversation.dispose();
+ _controller.dispose();
+ _transport.dispose();
+ _inputController.dispose();
+ _scrollController.dispose();
+ super.dispose();
+ }
+
+ Future _sendAndReceive(ChatMessage message) async {
+ if (_agentService == null) {
+ return;
+ }
+
+ final buffer = StringBuffer();
+ if (message.text.isNotEmpty) {
+ buffer.write(message.text);
+ } else {
+ for (final part in message.parts) {
+ if (part is TextPart) {
+ buffer.write(part.text);
+ } else if (part.isUiInteractionPart) {
+ final uiInteraction = part.asUiInteractionPart;
+ if (uiInteraction != null) {
+ buffer.write(uiInteraction.interaction);
+ }
+ }
+ }
+ }
+
+ final prompt = buffer.toString();
+ if (prompt.isEmpty) return;
+
+ debugPrint('OUTBOUND:\n$prompt\n');
+
+ // Add user message to local chat logs (if it's not a background error/interaction)
+ // For visual cleanliness, if the message contains user text, display it.
+ if (message.text.isNotEmpty) {
+ setState(() {
+ _chatMessages.add(TextContent(text: message.text, isUser: true));
+ });
+ }
+
+ try {
+ final responseStream = _agentService!.generateResponse(prompt);
+ await for (final chunk in responseStream) {
+ debugPrint('INBOUND:\n$chunk\n');
+ _transport.addChunk(chunk);
+ }
+ } catch (e) {
+ // Feed error into the transport adapter so Conversation catches it
+ debugPrint('Gemini Stream Error: $e');
+ _transport.addChunk('\n[Error occurred: $e]');
+ }
+ }
+
+ void _handleUserSubmit() {
+ final text = _inputController.text.trim();
+ if (text.isEmpty) return;
+ _inputController.clear();
+
+ // Trigger the conversation flow by sending a ChatMessage
+ _conversation.sendRequest(ChatMessage.user(text));
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: CommisColors.background,
+ appBar: AppBar(
+ backgroundColor: CommisColors.surfaceLevel1,
+ elevation: 0.0,
+ scrolledUnderElevation: 0.0,
+ title: Row(
+ children: [
+ const Icon(
+ Icons.auto_awesome,
+ color: CommisColors.goldAccent,
+ size: 24.0,
+ ),
+ const SizedBox(width: 10.0),
+ Text(
+ 'COMMIS ASSISTANT',
+ style: GoogleFonts.outfit(
+ color: CommisColors.textPrimary,
+ fontSize: 20.0,
+ fontWeight: FontWeight.w800,
+ letterSpacing: 1.5,
+ ),
+ ),
+ ],
+ ),
+ bottom: PreferredSize(
+ preferredSize: const Size.fromHeight(1.0),
+ child: Container(color: CommisColors.borderLowContrast, height: 1.0),
+ ),
+ ),
+ body: Stack(
+ children: [
+ Column(
+ children: [
+ // Chat messages list
+ Expanded(
+ child: ListView.builder(
+ controller: _scrollController,
+ padding: const EdgeInsets.all(16.0),
+ itemCount: _chatMessages.length,
+ itemBuilder: (context, i) {
+ final msg = _chatMessages[i];
+ return switch (msg) {
+ TextContent(:final text, :final isUser) => MessageBubble(
+ text: text,
+ isUser: isUser,
+ ),
+ SurfaceContent(:final surfaceId) => Surface(
+ key: ValueKey(surfaceId),
+ surfaceContext: _controller.contextFor(surfaceId),
+ ),
+ };
+ },
+ ),
+ ),
+
+ // Divider
+ const Divider(color: CommisColors.borderLowContrast, height: 1.0),
+
+ // Input bar
+ SafeArea(
+ top: false,
+ child: Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16.0,
+ vertical: 12.0,
+ ),
+ color: CommisColors.surfaceLevel1,
+ child: Row(
+ children: [
+ Expanded(
+ child: Container(
+ decoration: BoxDecoration(
+ color: CommisColors.background,
+ borderRadius: BorderRadius.circular(24.0),
+ border: Border.all(
+ color: CommisColors.borderLowContrast,
+ ),
+ ),
+ padding: const EdgeInsets.symmetric(horizontal: 16.0),
+ child: TextField(
+ controller: _inputController,
+ enabled: !_isWaiting,
+ style: GoogleFonts.sourceSans3(
+ color: _isWaiting
+ ? CommisColors.textSecondary.withAlpha(80)
+ : CommisColors.textPrimary,
+ fontSize: 15.0,
+ ),
+ decoration: InputDecoration(
+ hintText: _isWaiting
+ ? 'Commis is thinking...'
+ : 'Ask Commis anything...',
+ hintStyle: GoogleFonts.sourceSans3(
+ color: CommisColors.textSecondary.withAlpha(
+ 120,
+ ),
+ ),
+ border: InputBorder.none,
+ ),
+ onSubmitted: _isWaiting
+ ? null
+ : (_) => _handleUserSubmit(),
+ ),
+ ),
+ ),
+ const SizedBox(width: 12.0),
+ IconButton(
+ icon: const Icon(Icons.send_rounded),
+ color: _isWaiting
+ ? CommisColors.goldAccent.withAlpha(80)
+ : CommisColors.goldAccent,
+ onPressed: _isWaiting ? null : _handleUserSubmit,
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ if (_isWaiting)
+ const Positioned(
+ top: 0,
+ left: 0,
+ right: 0,
+ child: LinearProgressIndicator(
+ color: CommisColors.goldAccent,
+ backgroundColor: Colors.transparent,
+ minHeight: 2.0,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+sealed class ChatContent {}
+
+class TextContent extends ChatContent {
+ String text;
+ final bool isUser;
+
+ TextContent({required this.text, required this.isUser});
+}
+
+class SurfaceContent extends ChatContent {
+ final String surfaceId;
+
+ SurfaceContent({required this.surfaceId});
+}
diff --git a/commis/app/lib/ui/screens/ingredients_list_screen.dart b/commis/app/lib/ui/screens/ingredients_list_screen.dart
new file mode 100644
index 00000000..5b58e9c9
--- /dev/null
+++ b/commis/app/lib/ui/screens/ingredients_list_screen.dart
@@ -0,0 +1,108 @@
+// Copyright 2026 The Flutter Authors.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:google_fonts/google_fonts.dart';
+import 'package:provider/provider.dart';
+
+import '../../data/firestore_repository.dart';
+import '../../data/models.dart';
+import '../theme.dart';
+import '../widgets/ingredient_card.dart';
+
+class IngredientsListScreen extends StatelessWidget {
+ const IngredientsListScreen({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final repository = context.read();
+
+ return Scaffold(
+ backgroundColor: CommisColors.background,
+ appBar: AppBar(
+ backgroundColor: CommisColors.surfaceLevel1,
+ elevation: 0.0,
+ scrolledUnderElevation: 0.0,
+ title: Text(
+ 'INGREDIENTS CHECKLIST',
+ style: GoogleFonts.outfit(
+ color: CommisColors.textPrimary,
+ fontSize: 22.0,
+ fontWeight: FontWeight.w800,
+ letterSpacing: 1.5,
+ ),
+ ),
+ bottom: PreferredSize(
+ preferredSize: const Size.fromHeight(1.0),
+ child: Container(color: CommisColors.borderLowContrast, height: 1.0),
+ ),
+ ),
+ body: StreamBuilder>(
+ stream: repository.watchIngredients(),
+ builder: (context, snapshot) {
+ if (snapshot.connectionState == ConnectionState.waiting) {
+ return const Center(
+ child: CircularProgressIndicator(color: CommisColors.goldAccent),
+ );
+ }
+
+ if (snapshot.hasError) {
+ return Center(
+ child: Text(
+ 'Error loading ingredients: ${snapshot.error}',
+ style: GoogleFonts.sourceSans3(color: Colors.redAccent),
+ ),
+ );
+ }
+
+ final items = snapshot.data ?? const [];
+
+ if (items.isEmpty) {
+ return Center(
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Icon(
+ Icons.inventory_2_outlined,
+ size: 64.0,
+ color: CommisColors.textSecondary,
+ ),
+ const SizedBox(height: 16.0),
+ Text(
+ 'No Ingredients Found',
+ style: GoogleFonts.outfit(
+ color: CommisColors.textPrimary,
+ fontSize: 20.0,
+ fontWeight: FontWeight.w700,
+ ),
+ ),
+ const SizedBox(height: 8.0),
+ Text(
+ 'Ensure the Firestore emulator is running and you have seeded the database using seed_ingredients.dart.',
+ textAlign: TextAlign.center,
+ style: GoogleFonts.sourceSans3(
+ color: CommisColors.textSecondary,
+ fontSize: 14.0,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ return ListView.builder(
+ padding: const EdgeInsets.symmetric(vertical: 12.0),
+ itemCount: items.length,
+ itemBuilder: (context, index) {
+ return IngredientCard(item: items[index], isEven: index.isEven);
+ },
+ );
+ },
+ ),
+ );
+ }
+}
diff --git a/commis/app/lib/ui/screens/job_edit_screen.dart b/commis/app/lib/ui/screens/job_edit_screen.dart
new file mode 100644
index 00000000..867483d4
--- /dev/null
+++ b/commis/app/lib/ui/screens/job_edit_screen.dart
@@ -0,0 +1,679 @@
+// Copyright 2026 The Flutter Authors.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+import 'package:google_fonts/google_fonts.dart';
+import 'package:provider/provider.dart';
+
+import '../../data/firestore_repository.dart';
+
+import '../../data/models.dart';
+import '../theme.dart';
+
+class JobEditScreen extends StatefulWidget {
+ final String jobId;
+
+ const JobEditScreen({required this.jobId, super.key});
+
+ @override
+ State createState() => _JobEditScreenState();
+}
+
+class _JobEditScreenState extends State {
+ final _formKey = GlobalKey();
+ late final TextEditingController _titleController;
+ late final TextEditingController _addressController;
+ late final TextEditingController _guestCountController;
+ late final TextEditingController _latitudeController;
+ late final TextEditingController _longitudeController;
+
+ DateTime? _selectedDate;
+ List _selectedRecipeIds = [];
+ bool _isInitialized = false;
+ bool _isSaving = false;
+
+ @override
+ void initState() {
+ super.initState();
+ _titleController = TextEditingController();
+ _addressController = TextEditingController();
+ _guestCountController = TextEditingController();
+ _latitudeController = TextEditingController();
+ _longitudeController = TextEditingController();
+ }
+
+ @override
+ void dispose() {
+ _titleController.dispose();
+ _addressController.dispose();
+ _guestCountController.dispose();
+ _latitudeController.dispose();
+ _longitudeController.dispose();
+ super.dispose();
+ }
+
+ String _formatDate(DateTime date) {
+ const months = [
+ 'JAN',
+ 'FEB',
+ 'MAR',
+ 'APR',
+ 'MAY',
+ 'JUN',
+ 'JUL',
+ 'AUG',
+ 'SEP',
+ 'OCT',
+ 'NOV',
+ 'DEC',
+ ];
+ return '${months[date.month - 1]} ${date.day}, ${date.year}';
+ }
+
+ Future _pickDate() async {
+ final now = DateTime.now();
+ final picked = await showDatePicker(
+ context: context,
+ initialDate: _selectedDate ?? now,
+ firstDate: DateTime(now.year - 1),
+ lastDate: DateTime(now.year + 5),
+ builder: (context, child) {
+ return Theme(
+ data: Theme.of(context).copyWith(
+ colorScheme: const ColorScheme.dark(
+ primary: CommisColors.goldAccent,
+ onPrimary: CommisColors.background,
+ surface: CommisColors.surfaceLevel1,
+ onSurface: CommisColors.textPrimary,
+ ),
+ ),
+ child: child!,
+ );
+ },
+ );
+
+ if (picked != null) {
+ setState(() {
+ _selectedDate = picked;
+ });
+ }
+ }
+
+ Future _saveJob() async {
+ if (!_formKey.currentState!.validate() || _selectedDate == null) {
+ return;
+ }
+
+ setState(() {
+ _isSaving = true;
+ });
+
+ try {
+ final updatedJob = CateringJob(
+ id: widget.jobId,
+ title: _titleController.text.trim(),
+ address: _addressController.text.trim(),
+ guestCount: int.tryParse(_guestCountController.text.trim()) ?? 0,
+ date: _selectedDate!,
+ recipeIds: _selectedRecipeIds,
+ latitude: double.tryParse(_latitudeController.text.trim()) ?? 0.0,
+ longitude: double.tryParse(_longitudeController.text.trim()) ?? 0.0,
+ );
+
+ await context.read().updateJob(updatedJob);
+
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ backgroundColor: CommisColors.goldAccent,
+ content: Text(
+ 'Catering Job updated successfully!',
+ style: GoogleFonts.sourceSans3(
+ color: CommisColors.background,
+ fontWeight: FontWeight.w700,
+ ),
+ ),
+ ),
+ );
+ context.pop();
+ }
+ } catch (e) {
+ setState(() {
+ _isSaving = false;
+ });
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ backgroundColor: Colors.redAccent,
+ content: Text('Failed to update job: $e'),
+ ),
+ );
+ }
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final repository = context.read();
+
+ return Scaffold(
+ backgroundColor: CommisColors.background,
+ appBar: AppBar(
+ backgroundColor: CommisColors.surfaceLevel1,
+ elevation: 0.0,
+ scrolledUnderElevation: 0.0,
+ leading: IconButton(
+ icon: const Icon(Icons.arrow_back, color: CommisColors.textPrimary),
+ onPressed: () => context.pop(),
+ ),
+ title: Text(
+ 'EDIT CATERING JOB',
+ style: GoogleFonts.outfit(
+ color: CommisColors.textPrimary,
+ fontSize: 20.0,
+ fontWeight: FontWeight.w800,
+ letterSpacing: 1.5,
+ ),
+ ),
+ bottom: PreferredSize(
+ preferredSize: const Size.fromHeight(1.0),
+ child: Container(color: CommisColors.borderLowContrast, height: 1.0),
+ ),
+ ),
+ body: StreamBuilder(
+ stream: repository.watchJob(widget.jobId),
+ builder: (context, jobSnapshot) {
+ if (jobSnapshot.connectionState == ConnectionState.waiting &&
+ !_isInitialized) {
+ return const Center(
+ child: CircularProgressIndicator(color: CommisColors.goldAccent),
+ );
+ }
+
+ if (jobSnapshot.hasError) {
+ return Center(
+ child: Text(
+ 'Error loading job: ${jobSnapshot.error}',
+ style: GoogleFonts.sourceSans3(color: Colors.redAccent),
+ ),
+ );
+ }
+
+ final job = jobSnapshot.data;
+ if (job == null) {
+ return Center(
+ child: Text(
+ 'Job not found',
+ style: GoogleFonts.sourceSans3(
+ color: CommisColors.textSecondary,
+ ),
+ ),
+ );
+ }
+
+ if (!_isInitialized) {
+ _titleController.text = job.title;
+ _addressController.text = job.address;
+ _guestCountController.text = job.guestCount.toString();
+ _latitudeController.text = job.latitude.toString();
+ _longitudeController.text = job.longitude.toString();
+ _selectedDate = job.date;
+ _selectedRecipeIds = List.from(job.recipeIds);
+ _isInitialized = true;
+ }
+
+ return StreamBuilder>(
+ stream: repository.watchRecipes(),
+ builder: (context, recipesSnapshot) {
+ final recipes = recipesSnapshot.data ?? const [];
+
+ return Form(
+ key: _formKey,
+ child: ListView(
+ padding: const EdgeInsets.all(20.0),
+ children: [
+ // Job Title
+ Text(
+ 'EVENT TITLE',
+ style: GoogleFonts.outfit(
+ color: CommisColors.textSecondary,
+ fontSize: 12.0,
+ fontWeight: FontWeight.w700,
+ letterSpacing: 1.2,
+ ),
+ ),
+ const SizedBox(height: 8.0),
+ TextFormField(
+ controller: _titleController,
+ style: GoogleFonts.sourceSans3(
+ color: CommisColors.textPrimary,
+ ),
+ decoration: const InputDecoration(
+ hintText: 'e.g. Anniversary Party',
+ hintStyle: TextStyle(color: CommisColors.textSecondary),
+ filled: true,
+ fillColor: CommisColors.surfaceLevel1,
+ focusedBorder: OutlineInputBorder(
+ borderSide: BorderSide(
+ color: CommisColors.goldAccent,
+ ),
+ ),
+ enabledBorder: OutlineInputBorder(
+ borderSide: BorderSide(
+ color: CommisColors.borderLowContrast,
+ ),
+ ),
+ ),
+ validator: (value) {
+ if (value == null || value.trim().isEmpty) {
+ return 'Please enter a title';
+ }
+ return null;
+ },
+ ),
+ const SizedBox(height: 20.0),
+
+ // Venue Address
+ Text(
+ 'VENUE ADDRESS',
+ style: GoogleFonts.outfit(
+ color: CommisColors.textSecondary,
+ fontSize: 12.0,
+ fontWeight: FontWeight.w700,
+ letterSpacing: 1.2,
+ ),
+ ),
+ const SizedBox(height: 8.0),
+ TextFormField(
+ controller: _addressController,
+ style: GoogleFonts.sourceSans3(
+ color: CommisColors.textPrimary,
+ ),
+ decoration: const InputDecoration(
+ hintText: 'Street, City, Zip Code',
+ hintStyle: TextStyle(color: CommisColors.textSecondary),
+ filled: true,
+ fillColor: CommisColors.surfaceLevel1,
+ focusedBorder: OutlineInputBorder(
+ borderSide: BorderSide(
+ color: CommisColors.goldAccent,
+ ),
+ ),
+ enabledBorder: OutlineInputBorder(
+ borderSide: BorderSide(
+ color: CommisColors.borderLowContrast,
+ ),
+ ),
+ ),
+ validator: (value) {
+ if (value == null || value.trim().isEmpty) {
+ return 'Please enter a venue address';
+ }
+ return null;
+ },
+ ),
+ const SizedBox(height: 20.0),
+
+ // Row of Guest Count & Status
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'GUEST COUNT',
+ style: GoogleFonts.outfit(
+ color: CommisColors.textSecondary,
+ fontSize: 12.0,
+ fontWeight: FontWeight.w700,
+ letterSpacing: 1.2,
+ ),
+ ),
+ const SizedBox(height: 8.0),
+ TextFormField(
+ controller: _guestCountController,
+ keyboardType: TextInputType.number,
+ style: GoogleFonts.sourceSans3(
+ color: CommisColors.textPrimary,
+ ),
+ decoration: const InputDecoration(
+ filled: true,
+ fillColor: CommisColors.surfaceLevel1,
+ focusedBorder: OutlineInputBorder(
+ borderSide: BorderSide(
+ color: CommisColors.goldAccent,
+ ),
+ ),
+ enabledBorder: OutlineInputBorder(
+ borderSide: BorderSide(
+ color: CommisColors.borderLowContrast,
+ ),
+ ),
+ ),
+ validator: (value) {
+ if (value == null || value.trim().isEmpty) {
+ return 'Required';
+ }
+ if (int.tryParse(value) == null) {
+ return 'Invalid number';
+ }
+ return null;
+ },
+ ),
+ ],
+ ),
+ const SizedBox(height: 24.0),
+
+ // Latitude and Longitude Row
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'LATITUDE',
+ style: GoogleFonts.outfit(
+ color: CommisColors.textSecondary,
+ fontSize: 12.0,
+ fontWeight: FontWeight.w700,
+ letterSpacing: 1.2,
+ ),
+ ),
+ const SizedBox(height: 8.0),
+ TextFormField(
+ controller: _latitudeController,
+ keyboardType:
+ const TextInputType.numberWithOptions(
+ decimal: true,
+ ),
+ style: GoogleFonts.sourceSans3(
+ color: CommisColors.textPrimary,
+ ),
+ decoration: const InputDecoration(
+ hintText: 'e.g. 28.538',
+ hintStyle: TextStyle(
+ color: CommisColors.textSecondary,
+ ),
+ filled: true,
+ fillColor: CommisColors.surfaceLevel1,
+ focusedBorder: OutlineInputBorder(
+ borderSide: BorderSide(
+ color: CommisColors.goldAccent,
+ ),
+ ),
+ enabledBorder: OutlineInputBorder(
+ borderSide: BorderSide(
+ color: CommisColors.borderLowContrast,
+ ),
+ ),
+ ),
+ validator: (value) {
+ if (value == null || value.trim().isEmpty) {
+ return 'Required';
+ }
+ if (double.tryParse(value) == null) {
+ return 'Invalid number';
+ }
+ return null;
+ },
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(width: 16.0),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'LONGITUDE',
+ style: GoogleFonts.outfit(
+ color: CommisColors.textSecondary,
+ fontSize: 12.0,
+ fontWeight: FontWeight.w700,
+ letterSpacing: 1.2,
+ ),
+ ),
+ const SizedBox(height: 8.0),
+ TextFormField(
+ controller: _longitudeController,
+ keyboardType:
+ const TextInputType.numberWithOptions(
+ decimal: true,
+ ),
+ style: GoogleFonts.sourceSans3(
+ color: CommisColors.textPrimary,
+ ),
+ decoration: const InputDecoration(
+ hintText: 'e.g. -81.379',
+ hintStyle: TextStyle(
+ color: CommisColors.textSecondary,
+ ),
+ filled: true,
+ fillColor: CommisColors.surfaceLevel1,
+ focusedBorder: OutlineInputBorder(
+ borderSide: BorderSide(
+ color: CommisColors.goldAccent,
+ ),
+ ),
+ enabledBorder: OutlineInputBorder(
+ borderSide: BorderSide(
+ color: CommisColors.borderLowContrast,
+ ),
+ ),
+ ),
+ validator: (value) {
+ if (value == null || value.trim().isEmpty) {
+ return 'Required';
+ }
+ if (double.tryParse(value) == null) {
+ return 'Invalid number';
+ }
+ return null;
+ },
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 24.0),
+
+ // Date Selection Row
+ Text(
+ 'EVENT DATE',
+ style: GoogleFonts.outfit(
+ color: CommisColors.textSecondary,
+ fontSize: 12.0,
+ fontWeight: FontWeight.w700,
+ letterSpacing: 1.2,
+ ),
+ ),
+ const SizedBox(height: 8.0),
+ InkWell(
+ onTap: _pickDate,
+ borderRadius: BorderRadius.circular(8.0),
+ child: Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16.0,
+ vertical: 16.0,
+ ),
+ decoration: BoxDecoration(
+ color: CommisColors.surfaceLevel1,
+ border: Border.all(
+ color: CommisColors.borderLowContrast,
+ ),
+ borderRadius: BorderRadius.circular(8.0),
+ ),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ _selectedDate == null
+ ? 'Select Date'
+ : _formatDate(_selectedDate!),
+ style: GoogleFonts.jetBrainsMono(
+ color: _selectedDate == null
+ ? CommisColors.textSecondary
+ : CommisColors.goldAccent,
+ fontSize: 14.0,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ const Icon(
+ Icons.calendar_today_outlined,
+ color: CommisColors.goldAccent,
+ size: 20.0,
+ ),
+ ],
+ ),
+ ),
+ ),
+ const SizedBox(height: 24.0),
+
+ // Recipes Checklist
+ Text(
+ 'ASSOCIATE RECIPES',
+ style: GoogleFonts.outfit(
+ color: CommisColors.textSecondary,
+ fontSize: 12.0,
+ fontWeight: FontWeight.w700,
+ letterSpacing: 1.2,
+ ),
+ ),
+ const SizedBox(height: 12.0),
+ if (recipes.isEmpty)
+ Padding(
+ padding: const EdgeInsets.symmetric(vertical: 8.0),
+ child: Text(
+ 'No recipes available to add',
+ style: GoogleFonts.sourceSans3(
+ color: CommisColors.textSecondary.withAlpha(153),
+ fontStyle: FontStyle.italic,
+ ),
+ ),
+ )
+ else
+ Container(
+ decoration: BoxDecoration(
+ border: Border.all(
+ color: CommisColors.borderLowContrast,
+ ),
+ borderRadius: BorderRadius.circular(8.0),
+ ),
+ child: ListView.separated(
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ itemCount: recipes.length,
+ separatorBuilder: (context, index) => const Divider(
+ color: CommisColors.borderLowContrast,
+ height: 1.0,
+ ),
+ itemBuilder: (context, index) {
+ final recipe = recipes[index];
+ final isSelected = _selectedRecipeIds.contains(
+ recipe.id,
+ );
+
+ return CheckboxListTile(
+ value: isSelected,
+ title: Text(
+ recipe.name,
+ style: GoogleFonts.sourceSans3(
+ color: CommisColors.textPrimary,
+ fontWeight: isSelected
+ ? FontWeight.w600
+ : FontWeight.w400,
+ ),
+ ),
+ subtitle: Text(
+ recipe.description,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ style: GoogleFonts.sourceSans3(
+ color: CommisColors.textSecondary,
+ fontSize: 12.5,
+ ),
+ ),
+ activeColor: CommisColors.goldAccent,
+ checkColor: CommisColors.background,
+ onChanged: (checked) {
+ setState(() {
+ if (checked == true) {
+ _selectedRecipeIds.add(recipe.id);
+ } else {
+ _selectedRecipeIds.remove(recipe.id);
+ }
+ });
+ },
+ );
+ },
+ ),
+ ),
+ const SizedBox(height: 32.0),
+
+ // Actions Buttons (Cancel & Save)
+ Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ TextButton(
+ onPressed: _isSaving ? null : () => context.pop(),
+ style: TextButton.styleFrom(
+ foregroundColor: CommisColors.textSecondary,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16.0,
+ vertical: 12.0,
+ ),
+ ),
+ child: Text(
+ 'Cancel',
+ style: GoogleFonts.sourceSans3(
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ const SizedBox(width: 12.0),
+ FilledButton(
+ onPressed: _isSaving ? null : _saveJob,
+ style: FilledButton.styleFrom(
+ backgroundColor: CommisColors.goldAccent,
+ foregroundColor: CommisColors.background,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 24.0,
+ vertical: 12.0,
+ ),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8.0),
+ ),
+ ),
+ child: _isSaving
+ ? const SizedBox(
+ width: 20.0,
+ height: 20.0,
+ child: CircularProgressIndicator(
+ strokeWidth: 2.0,
+ color: CommisColors.background,
+ ),
+ )
+ : Text(
+ 'Save Changes',
+ style: GoogleFonts.sourceSans3(
+ fontWeight: FontWeight.w700,
+ fontSize: 15.0,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ );
+ },
+ );
+ },
+ ),
+ );
+ }
+}
diff --git a/commis/app/lib/ui/screens/jobs_list_screen.dart b/commis/app/lib/ui/screens/jobs_list_screen.dart
new file mode 100644
index 00000000..cd999a47
--- /dev/null
+++ b/commis/app/lib/ui/screens/jobs_list_screen.dart
@@ -0,0 +1,121 @@
+// Copyright 2026 The Flutter Authors.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:google_fonts/google_fonts.dart';
+import 'package:provider/provider.dart';
+
+import '../../data/firestore_repository.dart';
+import '../../data/models.dart';
+import '../theme.dart';
+import '../widgets/job_card.dart';
+
+class JobsListScreen extends StatelessWidget {
+ const JobsListScreen({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final repository = context.read();
+
+ return Scaffold(
+ backgroundColor: CommisColors.background,
+ appBar: AppBar(
+ backgroundColor: CommisColors.surfaceLevel1,
+ elevation: 0.0,
+ scrolledUnderElevation: 0.0,
+ title: Text(
+ 'CATERING JOBS',
+ style: GoogleFonts.outfit(
+ color: CommisColors.textPrimary,
+ fontSize: 22.0,
+ fontWeight: FontWeight.w800,
+ letterSpacing: 1.5,
+ ),
+ ),
+ bottom: PreferredSize(
+ preferredSize: const Size.fromHeight(1.0),
+ child: Container(color: CommisColors.borderLowContrast, height: 1.0),
+ ),
+ ),
+ body: StreamBuilder>(
+ stream: repository.watchRecipes(),
+ builder: (context, recipesSnapshot) {
+ final recipes = recipesSnapshot.data ?? const [];
+
+ return StreamBuilder>(
+ stream: repository.watchJobs(),
+ builder: (context, jobsSnapshot) {
+ if (jobsSnapshot.connectionState == ConnectionState.waiting) {
+ return const Center(
+ child: CircularProgressIndicator(
+ color: CommisColors.goldAccent,
+ ),
+ );
+ }
+
+ if (jobsSnapshot.hasError) {
+ return Center(
+ child: Text(
+ 'Error loading jobs: ${jobsSnapshot.error}',
+ style: GoogleFonts.sourceSans3(color: Colors.redAccent),
+ ),
+ );
+ }
+
+ final jobs = jobsSnapshot.data ?? const [];
+
+ if (jobs.isEmpty) {
+ return Center(
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Icon(
+ Icons.assignment_late_outlined,
+ size: 64.0,
+ color: CommisColors.textSecondary,
+ ),
+ const SizedBox(height: 16.0),
+ Text(
+ 'No Catering Jobs Found',
+ style: GoogleFonts.outfit(
+ color: CommisColors.textPrimary,
+ fontSize: 20.0,
+ fontWeight: FontWeight.w700,
+ ),
+ ),
+ const SizedBox(height: 8.0),
+ Text(
+ 'Ensure the Firestore emulator is running and you have seeded the database using seed_jobs.dart.',
+ textAlign: TextAlign.center,
+ style: GoogleFonts.sourceSans3(
+ color: CommisColors.textSecondary,
+ fontSize: 14.0,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ return ListView.builder(
+ padding: const EdgeInsets.symmetric(vertical: 12.0),
+ itemCount: jobs.length,
+ itemBuilder: (context, index) {
+ return JobCard(
+ job: jobs[index],
+ allRecipes: recipes,
+ isEven: index.isEven,
+ );
+ },
+ );
+ },
+ );
+ },
+ ),
+ );
+ }
+}
diff --git a/commis/app/lib/ui/screens/navigation_shell.dart b/commis/app/lib/ui/screens/navigation_shell.dart
new file mode 100644
index 00000000..b66028d7
--- /dev/null
+++ b/commis/app/lib/ui/screens/navigation_shell.dart
@@ -0,0 +1,92 @@
+// Copyright 2026 The Flutter Authors.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+import 'package:google_fonts/google_fonts.dart';
+import '../theme.dart';
+
+class ScaffoldWithNavBar extends StatelessWidget {
+ final StatefulNavigationShell navigationShell;
+
+ const ScaffoldWithNavBar({required this.navigationShell, super.key});
+
+ void _goBranch(int index) {
+ navigationShell.goBranch(
+ index,
+ initialLocation: index == navigationShell.currentIndex,
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: navigationShell,
+ bottomNavigationBar: NavigationBarTheme(
+ data: NavigationBarThemeData(
+ backgroundColor: CommisColors.surfaceLevel1,
+ indicatorColor: CommisColors.goldAccent.withAlpha(30),
+ labelTextStyle: WidgetStateProperty.resolveWith((states) {
+ if (states.contains(WidgetState.selected)) {
+ return GoogleFonts.sourceSans3(
+ color: CommisColors.goldAccent,
+ fontSize: 12.0,
+ fontWeight: FontWeight.w700,
+ );
+ }
+ return GoogleFonts.sourceSans3(
+ color: CommisColors.textSecondary,
+ fontSize: 12.0,
+ fontWeight: FontWeight.w500,
+ );
+ }),
+ iconTheme: WidgetStateProperty.resolveWith((states) {
+ if (states.contains(WidgetState.selected)) {
+ return const IconThemeData(color: CommisColors.goldAccent);
+ }
+ return const IconThemeData(color: CommisColors.textSecondary);
+ }),
+ ),
+ child: Container(
+ decoration: const BoxDecoration(
+ border: Border(
+ top: BorderSide(
+ color: CommisColors.borderLowContrast,
+ width: 1.0,
+ ),
+ ),
+ ),
+ child: NavigationBar(
+ selectedIndex: navigationShell.currentIndex,
+ onDestinationSelected: _goBranch,
+ height: 70.0,
+ labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
+ destinations: const [
+ NavigationDestination(
+ icon: Icon(Icons.assignment_outlined),
+ selectedIcon: Icon(Icons.assignment),
+ label: 'Jobs',
+ ),
+ NavigationDestination(
+ icon: Icon(Icons.restaurant_menu_outlined),
+ selectedIcon: Icon(Icons.restaurant_menu),
+ label: 'Recipes',
+ ),
+ NavigationDestination(
+ icon: Icon(Icons.inventory_2_outlined),
+ selectedIcon: Icon(Icons.inventory_2),
+ label: 'Ingredients',
+ ),
+ NavigationDestination(
+ icon: Icon(Icons.auto_awesome_outlined),
+ selectedIcon: Icon(Icons.auto_awesome),
+ label: 'Agent',
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/commis/app/lib/ui/screens/recipes_list_screen.dart b/commis/app/lib/ui/screens/recipes_list_screen.dart
new file mode 100644
index 00000000..44f67493
--- /dev/null
+++ b/commis/app/lib/ui/screens/recipes_list_screen.dart
@@ -0,0 +1,121 @@
+// Copyright 2026 The Flutter Authors.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:google_fonts/google_fonts.dart';
+import 'package:provider/provider.dart';
+
+import '../../data/firestore_repository.dart';
+import '../../data/models.dart';
+import '../theme.dart';
+import '../widgets/recipe_card.dart';
+
+class RecipesListScreen extends StatelessWidget {
+ const RecipesListScreen({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final repository = context.read();
+
+ return Scaffold(
+ backgroundColor: CommisColors.background,
+ appBar: AppBar(
+ backgroundColor: CommisColors.surfaceLevel1,
+ elevation: 0.0,
+ scrolledUnderElevation: 0.0,
+ title: Text(
+ 'RECIPES CATALOG',
+ style: GoogleFonts.outfit(
+ color: CommisColors.textPrimary,
+ fontSize: 22.0,
+ fontWeight: FontWeight.w800,
+ letterSpacing: 1.5,
+ ),
+ ),
+ bottom: PreferredSize(
+ preferredSize: const Size.fromHeight(1.0),
+ child: Container(color: CommisColors.borderLowContrast, height: 1.0),
+ ),
+ ),
+ body: StreamBuilder>(
+ stream: repository.watchIngredients(),
+ builder: (context, ingredientsSnapshot) {
+ final ingredients = ingredientsSnapshot.data ?? const [];
+
+ return StreamBuilder>(
+ stream: repository.watchRecipes(),
+ builder: (context, recipesSnapshot) {
+ if (recipesSnapshot.connectionState == ConnectionState.waiting) {
+ return const Center(
+ child: CircularProgressIndicator(
+ color: CommisColors.goldAccent,
+ ),
+ );
+ }
+
+ if (recipesSnapshot.hasError) {
+ return Center(
+ child: Text(
+ 'Error loading recipes: ${recipesSnapshot.error}',
+ style: GoogleFonts.sourceSans3(color: Colors.redAccent),
+ ),
+ );
+ }
+
+ final recipes = recipesSnapshot.data ?? const [];
+
+ if (recipes.isEmpty) {
+ return Center(
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Icon(
+ Icons.restaurant_menu_outlined,
+ size: 64.0,
+ color: CommisColors.textSecondary,
+ ),
+ const SizedBox(height: 16.0),
+ Text(
+ 'No Recipes Found',
+ style: GoogleFonts.outfit(
+ color: CommisColors.textPrimary,
+ fontSize: 20.0,
+ fontWeight: FontWeight.w700,
+ ),
+ ),
+ const SizedBox(height: 8.0),
+ Text(
+ 'Ensure the Firestore emulator is running and you have seeded the database using seed_recipes.dart.',
+ textAlign: TextAlign.center,
+ style: GoogleFonts.sourceSans3(
+ color: CommisColors.textSecondary,
+ fontSize: 14.0,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ return ListView.builder(
+ padding: const EdgeInsets.symmetric(vertical: 12.0),
+ itemCount: recipes.length,
+ itemBuilder: (context, index) {
+ return RecipeCard(
+ recipe: recipes[index],
+ allIngredients: ingredients,
+ isEven: index.isEven,
+ );
+ },
+ );
+ },
+ );
+ },
+ ),
+ );
+ }
+}
diff --git a/commis/app/lib/ui/theme.dart b/commis/app/lib/ui/theme.dart
new file mode 100644
index 00000000..14bf2fbd
--- /dev/null
+++ b/commis/app/lib/ui/theme.dart
@@ -0,0 +1,25 @@
+// Copyright 2026 The Flutter Authors.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+
+// --- Theme Color Tokens ---
+class CommisColors {
+ static const background = Color(0xFF0C141A);
+ static const surfaceLevel1 = Color(0xFF192127);
+ static const surfaceLevel2 = Color(0xFF212529);
+ static const goldAccent = Color(0xFFE5A93C);
+ static const goldAccentDim = Color(0xFFFFC665);
+ static const textPrimary = Color(0xFFDBE3EC);
+ static const textSecondary = Color(0xFFADB5BD);
+ static const borderLowContrast = Color(0xFF343A40);
+ static const double borderOpacityGold = 0.2;
+}
+
+// --- Typography Constants ---
+class CommisFonts {
+ static const title = 'Outfit';
+ static const mono = 'JetBrains Mono';
+ static const body = 'Source Sans 3';
+}
diff --git a/commis/app/lib/ui/widgets/ingredient_card.dart b/commis/app/lib/ui/widgets/ingredient_card.dart
new file mode 100644
index 00000000..4b3a6b5d
--- /dev/null
+++ b/commis/app/lib/ui/widgets/ingredient_card.dart
@@ -0,0 +1,126 @@
+// Copyright 2026 The Flutter Authors.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:google_fonts/google_fonts.dart';
+import '../../data/models.dart';
+import '../theme.dart';
+
+class IngredientCard extends StatelessWidget {
+ final Ingredient item;
+
+ final bool isEven;
+
+ const IngredientCard({required this.item, this.isEven = false, super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final assetPath = 'assets/images/ingredients/200/${item.id}.png';
+ final cardColor = isEven
+ ? Color.alphaBlend(
+ Colors.white.withAlpha(12),
+ CommisColors.surfaceLevel1,
+ )
+ : CommisColors.surfaceLevel1;
+
+ return Card(
+ color: cardColor,
+ elevation: 0.0,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(16.0),
+ side: const BorderSide(
+ color: CommisColors.borderLowContrast,
+ width: 1.0,
+ ),
+ ),
+ margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
+ child: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Thumbnail Image Asset with Fallback
+ Container(
+ width: 80.0,
+ height: 80.0,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(12.0),
+ border: Border.all(color: CommisColors.borderLowContrast),
+ ),
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(11.0),
+ child: Image.asset(
+ assetPath,
+ fit: BoxFit.cover,
+ errorBuilder: (context, error, stackTrace) {
+ return Container(
+ color: CommisColors.surfaceLevel2,
+ child: const Center(
+ child: Icon(
+ Icons.restaurant,
+ color: CommisColors.goldAccentDim,
+ size: 28.0,
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+ ),
+ const SizedBox(width: 16.0),
+ // Text Details
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ item.name,
+ style: GoogleFonts.outfit(
+ color: CommisColors.textPrimary,
+ fontSize: 18.0,
+ fontWeight: FontWeight.w700,
+ ),
+ ),
+ const SizedBox(height: 4.0),
+ Text(
+ item.description,
+ style: GoogleFonts.sourceSans3(
+ color: CommisColors.textSecondary,
+ fontSize: 13.5,
+ height: 1.3,
+ ),
+ ),
+ const SizedBox(height: 12.0),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 8.0,
+ vertical: 2.0,
+ ),
+ decoration: BoxDecoration(
+ color: CommisColors.surfaceLevel2,
+ borderRadius: BorderRadius.circular(6.0),
+ ),
+ child: Text(
+ 'Unit: ${item.unit}',
+ style: GoogleFonts.sourceSans3(
+ color: CommisColors.textSecondary,
+ fontSize: 12.0,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/commis/app/lib/ui/widgets/job_card.dart b/commis/app/lib/ui/widgets/job_card.dart
new file mode 100644
index 00000000..d78df679
--- /dev/null
+++ b/commis/app/lib/ui/widgets/job_card.dart
@@ -0,0 +1,219 @@
+// Copyright 2026 The Flutter Authors.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+import 'package:google_fonts/google_fonts.dart';
+
+import '../../data/models.dart';
+import '../theme.dart';
+
+class JobCard extends StatelessWidget {
+ final CateringJob job;
+ final List allRecipes;
+
+ final bool isEven;
+
+ const JobCard({
+ required this.job,
+ required this.allRecipes,
+ this.isEven = false,
+ super.key,
+ });
+
+ String _formatDate(DateTime date) {
+ const months = [
+ 'JAN',
+ 'FEB',
+ 'MAR',
+ 'APR',
+ 'MAY',
+ 'JUN',
+ 'JUL',
+ 'AUG',
+ 'SEP',
+ 'OCT',
+ 'NOV',
+ 'DEC',
+ ];
+ return '${months[date.month - 1]} ${date.day}, ${date.year}';
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final cardColor = isEven
+ ? Color.alphaBlend(
+ Colors.white.withAlpha(12),
+ CommisColors.surfaceLevel1,
+ )
+ : CommisColors.surfaceLevel1;
+
+ // Find names of associated recipes
+ final associatedRecipes = job.recipeIds.map((id) {
+ final match = allRecipes.cast().firstWhere(
+ (r) => r?.id == id,
+ orElse: () => null,
+ );
+ return match?.name ?? id;
+ }).toList();
+
+ return Card(
+ color: cardColor,
+ elevation: 0.0,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(16.0),
+ side: const BorderSide(
+ color: CommisColors.borderLowContrast,
+ width: 1.0,
+ ),
+ ),
+ margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ job.title,
+ style: GoogleFonts.outfit(
+ color: CommisColors.textPrimary,
+ fontSize: 20.0,
+ fontWeight: FontWeight.w700,
+ ),
+ ),
+ const SizedBox(height: 6.0),
+ Row(
+ children: [
+ const Icon(
+ Icons.calendar_today_outlined,
+ size: 14.0,
+ color: CommisColors.goldAccent,
+ ),
+ const SizedBox(width: 6.0),
+ Text(
+ _formatDate(job.date),
+ style: GoogleFonts.jetBrainsMono(
+ color: CommisColors.goldAccent,
+ fontSize: 13.0,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ const SizedBox(height: 16.0),
+ Row(
+ children: [
+ const Icon(
+ Icons.location_on_outlined,
+ size: 16.0,
+ color: CommisColors.textSecondary,
+ ),
+ const SizedBox(width: 8.0),
+ Expanded(
+ child: Text(
+ job.address,
+ style: GoogleFonts.sourceSans3(
+ color: CommisColors.textSecondary,
+ fontSize: 14.0,
+ ),
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 8.0),
+ Row(
+ children: [
+ const Icon(
+ Icons.people_outline,
+ size: 16.0,
+ color: CommisColors.textSecondary,
+ ),
+ const SizedBox(width: 8.0),
+ Text(
+ '${job.guestCount} guests',
+ style: GoogleFonts.sourceSans3(
+ color: CommisColors.textSecondary,
+ fontSize: 14.0,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 16.0),
+ const Divider(color: CommisColors.borderLowContrast, height: 1.0),
+ const SizedBox(height: 16.0),
+ Text(
+ 'MENU ITEMS',
+ style: GoogleFonts.outfit(
+ color: CommisColors.textSecondary,
+ fontSize: 12.0,
+ fontWeight: FontWeight.w700,
+ letterSpacing: 1.2,
+ ),
+ ),
+ const SizedBox(height: 10.0),
+ if (associatedRecipes.isEmpty)
+ Text(
+ 'No recipes assigned yet',
+ style: GoogleFonts.sourceSans3(
+ color: CommisColors.textSecondary.withAlpha(153),
+ fontSize: 14.0,
+ fontStyle: FontStyle.italic,
+ ),
+ )
+ else
+ Wrap(
+ spacing: 8.0,
+ runSpacing: 8.0,
+ children: associatedRecipes.map((name) {
+ return Chip(
+ backgroundColor: CommisColors.surfaceLevel2,
+ labelStyle: GoogleFonts.sourceSans3(
+ color: CommisColors.textPrimary,
+ fontSize: 13.0,
+ ),
+ side: const BorderSide(
+ color: CommisColors.borderLowContrast,
+ ),
+ padding: const EdgeInsets.symmetric(horizontal: 4.0),
+ label: Text(name),
+ );
+ }).toList(),
+ ),
+ const SizedBox(height: 16.0),
+ Align(
+ alignment: Alignment.centerRight,
+ child: FilledButton.icon(
+ onPressed: () {
+ context.push('/jobs/edit/${job.id}');
+ },
+ style: FilledButton.styleFrom(
+ backgroundColor: CommisColors.goldAccent,
+ foregroundColor: CommisColors.background,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16.0,
+ vertical: 10.0,
+ ),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8.0),
+ ),
+ ),
+ icon: const Icon(Icons.edit_outlined, size: 16.0),
+ label: Text(
+ 'Edit Job',
+ style: GoogleFonts.sourceSans3(fontWeight: FontWeight.w700),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/commis/app/lib/ui/widgets/message_bubble.dart b/commis/app/lib/ui/widgets/message_bubble.dart
new file mode 100644
index 00000000..94229956
--- /dev/null
+++ b/commis/app/lib/ui/widgets/message_bubble.dart
@@ -0,0 +1,88 @@
+// Copyright 2026 The Flutter Authors.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:flutter_markdown/flutter_markdown.dart';
+import '../theme.dart';
+
+class MessageBubble extends StatelessWidget {
+ final String text;
+ final bool isUser;
+
+ const MessageBubble({super.key, required this.text, required this.isUser});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ margin: const EdgeInsets.symmetric(vertical: 8.0),
+ alignment: isUser ? Alignment.centerRight : Alignment.centerLeft,
+ child: Container(
+ constraints: BoxConstraints(
+ maxWidth: MediaQuery.of(context).size.width * 0.75,
+ ),
+ padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
+ decoration: BoxDecoration(
+ color: isUser
+ ? CommisColors.goldAccent.withAlpha(50)
+ : CommisColors.surfaceLevel1,
+ borderRadius: BorderRadius.only(
+ topLeft: const Radius.circular(16.0),
+ topRight: const Radius.circular(16.0),
+ bottomLeft: Radius.circular(isUser ? 16.0 : 0),
+ bottomRight: Radius.circular(isUser ? 0 : 16.0),
+ ),
+ border: Border.all(
+ color: isUser
+ ? CommisColors.goldAccent.withAlpha(100)
+ : CommisColors.borderLowContrast,
+ ),
+ ),
+ child: MarkdownBody(
+ data: text,
+ // styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context)).copyWith(
+ // p: GoogleFonts.sourceSans3(
+ // color: CommisColors.textPrimary,
+ // fontSize: 15.0,
+ // height: 1.4,
+ // ),
+ // strong: GoogleFonts.sourceSans3(
+ // color: CommisColors.textPrimary,
+ // fontWeight: FontWeight.bold,
+ // fontSize: 15.0,
+ // ),
+ // h1: GoogleFonts.outfit(
+ // color: CommisColors.textPrimary,
+ // fontWeight: FontWeight.bold,
+ // fontSize: 20.0,
+ // ),
+ // h2: GoogleFonts.outfit(
+ // color: CommisColors.textPrimary,
+ // fontWeight: FontWeight.bold,
+ // fontSize: 18.0,
+ // ),
+ // h3: GoogleFonts.outfit(
+ // color: CommisColors.textPrimary,
+ // fontWeight: FontWeight.bold,
+ // fontSize: 16.0,
+ // ),
+ // listBullet: GoogleFonts.sourceSans3(
+ // color: CommisColors.textPrimary,
+ // fontSize: 15.0,
+ // ),
+ // code: GoogleFonts.jetBrainsMono(
+ // color: CommisColors.goldAccentDim,
+ // backgroundColor: Colors.transparent,
+ // fontSize: 13.0,
+ // ),
+ // codeblockDecoration: BoxDecoration(
+ // color: CommisColors.surfaceLevel2,
+ // borderRadius: BorderRadius.circular(8.0),
+ // border: Border.all(color: CommisColors.borderLowContrast),
+ // ),
+ // ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/commis/app/lib/ui/widgets/recipe_card.dart b/commis/app/lib/ui/widgets/recipe_card.dart
new file mode 100644
index 00000000..2e01e8d1
--- /dev/null
+++ b/commis/app/lib/ui/widgets/recipe_card.dart
@@ -0,0 +1,197 @@
+// Copyright 2026 The Flutter Authors.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:google_fonts/google_fonts.dart';
+import '../../data/models.dart';
+import '../theme.dart';
+
+class RecipeCard extends StatelessWidget {
+ final Recipe recipe;
+ final List allIngredients;
+
+ final bool isEven;
+
+ const RecipeCard({
+ required this.recipe,
+ required this.allIngredients,
+ this.isEven = false,
+ super.key,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final assetPath = 'assets/images/recipes/200/${recipe.id}.png';
+ final cardColor = isEven
+ ? Color.alphaBlend(
+ Colors.white.withAlpha(12),
+ CommisColors.surfaceLevel1,
+ )
+ : CommisColors.surfaceLevel1;
+
+ return Card(
+ color: cardColor,
+ elevation: 0.0,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(16.0),
+ side: const BorderSide(
+ color: CommisColors.borderLowContrast,
+ width: 1.0,
+ ),
+ ),
+ margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Header Row: Image on the left, Name and Description on the right
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Left: Recipe image thumbnail
+ Container(
+ width: 72.0,
+ height: 72.0,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(12.0),
+ border: Border.all(color: CommisColors.borderLowContrast),
+ ),
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(11.0),
+ child: Image.asset(
+ assetPath,
+ fit: BoxFit.cover,
+ errorBuilder: (context, error, stackTrace) {
+ return Container(
+ color: CommisColors.surfaceLevel2,
+ child: const Center(
+ child: Icon(
+ Icons.restaurant,
+ color: CommisColors.goldAccentDim,
+ size: 26.0,
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+ ),
+ const SizedBox(width: 16.0),
+ // Right: Name & Description column
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ recipe.name,
+ style: GoogleFonts.outfit(
+ color: CommisColors.textPrimary,
+ fontSize: 20.0,
+ fontWeight: FontWeight.w700,
+ ),
+ ),
+ const SizedBox(height: 6.0),
+ Text(
+ recipe.description,
+ style: GoogleFonts.sourceSans3(
+ color: CommisColors.textSecondary,
+ fontSize: 14.0,
+ height: 1.3,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 16.0),
+ const Divider(color: CommisColors.borderLowContrast, height: 1.0),
+ const SizedBox(height: 16.0),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ 'INGREDIENTS',
+ style: GoogleFonts.outfit(
+ color: CommisColors.textSecondary,
+ fontSize: 11.5,
+ fontWeight: FontWeight.w700,
+ letterSpacing: 1.2,
+ ),
+ ),
+ Text(
+ 'Per 10 Guests',
+ style: GoogleFonts.jetBrainsMono(
+ color: CommisColors.goldAccentDim,
+ fontSize: 11.0,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 10.0),
+ if (recipe.ingredients.isEmpty)
+ Text(
+ 'No ingredients defined',
+ style: GoogleFonts.sourceSans3(
+ color: CommisColors.textSecondary.withAlpha(153),
+ fontSize: 13.0,
+ fontStyle: FontStyle.italic,
+ ),
+ )
+ else
+ ListView.separated(
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ itemCount: recipe.ingredients.length,
+ separatorBuilder: (context, index) =>
+ const SizedBox(height: 6.0),
+ itemBuilder: (context, index) {
+ final ing = recipe.ingredients[index];
+ // Look up ingredient name and unit
+ final match = allIngredients
+ .cast()
+ .firstWhere(
+ (i) => i?.id == ing.itemId,
+ orElse: () => null,
+ );
+ final name = match?.name ?? ing.itemId;
+ final unit = match?.unit ?? '';
+
+ return Row(
+ children: [
+ const Icon(
+ Icons.circle,
+ size: 5.0,
+ color: CommisColors.goldAccent,
+ ),
+ const SizedBox(width: 8.0),
+ Expanded(
+ child: Text(
+ name,
+ style: GoogleFonts.sourceSans3(
+ color: CommisColors.textPrimary,
+ fontSize: 13.5,
+ ),
+ ),
+ ),
+ Text(
+ '${ing.amountPer10People.toStringAsFixed(ing.amountPer10People == ing.amountPer10People.toInt() ? 0 : 2)} $unit',
+ style: GoogleFonts.jetBrainsMono(
+ color: CommisColors.textSecondary,
+ fontSize: 12.5,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ],
+ );
+ },
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/commis/app/lib/ui/widgets/surface_card.dart b/commis/app/lib/ui/widgets/surface_card.dart
new file mode 100644
index 00000000..a20e284f
--- /dev/null
+++ b/commis/app/lib/ui/widgets/surface_card.dart
@@ -0,0 +1,40 @@
+// Copyright 2026 The Flutter Authors.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:genui/genui.dart';
+import '../theme.dart';
+
+class SurfaceCard extends StatelessWidget {
+ final SurfaceContext surfaceContext;
+
+ const SurfaceCard({
+ super.key,
+ required this.surfaceContext,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 10.0),
+ child: Card(
+ color: CommisColors.surfaceLevel1,
+ elevation: 0.0,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(16.0),
+ side: const BorderSide(
+ color: CommisColors.borderLowContrast,
+ width: 1.0,
+ ),
+ ),
+ child: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Surface(
+ surfaceContext: surfaceContext,
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/commis/app/linux/.gitignore b/commis/app/linux/.gitignore
new file mode 100644
index 00000000..d3896c98
--- /dev/null
+++ b/commis/app/linux/.gitignore
@@ -0,0 +1 @@
+flutter/ephemeral
diff --git a/commis/app/linux/CMakeLists.txt b/commis/app/linux/CMakeLists.txt
new file mode 100644
index 00000000..a2abcaf8
--- /dev/null
+++ b/commis/app/linux/CMakeLists.txt
@@ -0,0 +1,128 @@
+# Project-level configuration.
+cmake_minimum_required(VERSION 3.13)
+project(runner LANGUAGES CXX)
+
+# The name of the executable created for the application. Change this to change
+# the on-disk name of your application.
+set(BINARY_NAME "commis")
+# The unique GTK application identifier for this application. See:
+# https://wiki.gnome.org/HowDoI/ChooseApplicationID
+set(APPLICATION_ID "com.example.commis")
+
+# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
+# versions of CMake.
+cmake_policy(SET CMP0063 NEW)
+
+# Load bundled libraries from the lib/ directory relative to the binary.
+set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
+
+# Root filesystem for cross-building.
+if(FLUTTER_TARGET_PLATFORM_SYSROOT)
+ set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
+ set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
+ set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
+ set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
+ set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
+ set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
+endif()
+
+# Define build configuration options.
+if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
+ set(CMAKE_BUILD_TYPE "Debug" CACHE
+ STRING "Flutter build mode" FORCE)
+ set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
+ "Debug" "Profile" "Release")
+endif()
+
+# Compilation settings that should be applied to most targets.
+#
+# Be cautious about adding new options here, as plugins use this function by
+# default. In most cases, you should add new options to specific targets instead
+# of modifying this function.
+function(APPLY_STANDARD_SETTINGS TARGET)
+ target_compile_features(${TARGET} PUBLIC cxx_std_14)
+ target_compile_options(${TARGET} PRIVATE -Wall -Werror)
+ target_compile_options(${TARGET} PRIVATE "$<$>:-O3>")
+ target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>")
+endfunction()
+
+# Flutter library and tool build rules.
+set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
+add_subdirectory(${FLUTTER_MANAGED_DIR})
+
+# System-level dependencies.
+find_package(PkgConfig REQUIRED)
+pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
+
+# Application build; see runner/CMakeLists.txt.
+add_subdirectory("runner")
+
+# Run the Flutter tool portions of the build. This must not be removed.
+add_dependencies(${BINARY_NAME} flutter_assemble)
+
+# Only the install-generated bundle's copy of the executable will launch
+# correctly, since the resources must in the right relative locations. To avoid
+# people trying to run the unbundled copy, put it in a subdirectory instead of
+# the default top-level location.
+set_target_properties(${BINARY_NAME}
+ PROPERTIES
+ RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
+)
+
+
+# Generated plugin build rules, which manage building the plugins and adding
+# them to the application.
+include(flutter/generated_plugins.cmake)
+
+
+# === Installation ===
+# By default, "installing" just makes a relocatable bundle in the build
+# directory.
+set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
+if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
+ set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
+endif()
+
+# Start with a clean build bundle directory every time.
+install(CODE "
+ file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
+ " COMPONENT Runtime)
+
+set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
+set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
+
+install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
+ COMPONENT Runtime)
+
+install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
+ COMPONENT Runtime)
+
+install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+
+foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
+ install(FILES "${bundled_library}"
+ DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+endforeach(bundled_library)
+
+# Copy the native assets provided by the build.dart from all packages.
+set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
+install(DIRECTORY "${NATIVE_ASSETS_DIR}"
+ DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+
+# Fully re-copy the assets directory on each build to avoid having stale files
+# from a previous install.
+set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
+install(CODE "
+ file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
+ " COMPONENT Runtime)
+install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
+ DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
+
+# Install the AOT library on non-Debug builds only.
+if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
+ install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+endif()
diff --git a/commis/app/linux/flutter/CMakeLists.txt b/commis/app/linux/flutter/CMakeLists.txt
new file mode 100644
index 00000000..d5bd0164
--- /dev/null
+++ b/commis/app/linux/flutter/CMakeLists.txt
@@ -0,0 +1,88 @@
+# This file controls Flutter-level build steps. It should not be edited.
+cmake_minimum_required(VERSION 3.10)
+
+set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
+
+# Configuration provided via flutter tool.
+include(${EPHEMERAL_DIR}/generated_config.cmake)
+
+# TODO: Move the rest of this into files in ephemeral. See
+# https://github.com/flutter/flutter/issues/57146.
+
+# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
+# which isn't available in 3.10.
+function(list_prepend LIST_NAME PREFIX)
+ set(NEW_LIST "")
+ foreach(element ${${LIST_NAME}})
+ list(APPEND NEW_LIST "${PREFIX}${element}")
+ endforeach(element)
+ set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
+endfunction()
+
+# === Flutter Library ===
+# System-level dependencies.
+find_package(PkgConfig REQUIRED)
+pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
+pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
+pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
+
+set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
+
+# Published to parent scope for install step.
+set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
+set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
+set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
+set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
+
+list(APPEND FLUTTER_LIBRARY_HEADERS
+ "fl_basic_message_channel.h"
+ "fl_binary_codec.h"
+ "fl_binary_messenger.h"
+ "fl_dart_project.h"
+ "fl_engine.h"
+ "fl_json_message_codec.h"
+ "fl_json_method_codec.h"
+ "fl_message_codec.h"
+ "fl_method_call.h"
+ "fl_method_channel.h"
+ "fl_method_codec.h"
+ "fl_method_response.h"
+ "fl_plugin_registrar.h"
+ "fl_plugin_registry.h"
+ "fl_standard_message_codec.h"
+ "fl_standard_method_codec.h"
+ "fl_string_codec.h"
+ "fl_value.h"
+ "fl_view.h"
+ "flutter_linux.h"
+)
+list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
+add_library(flutter INTERFACE)
+target_include_directories(flutter INTERFACE
+ "${EPHEMERAL_DIR}"
+)
+target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
+target_link_libraries(flutter INTERFACE
+ PkgConfig::GTK
+ PkgConfig::GLIB
+ PkgConfig::GIO
+)
+add_dependencies(flutter flutter_assemble)
+
+# === Flutter tool backend ===
+# _phony_ is a non-existent file to force this command to run every time,
+# since currently there's no way to get a full input/output list from the
+# flutter tool.
+add_custom_command(
+ OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
+ ${CMAKE_CURRENT_BINARY_DIR}/_phony_
+ COMMAND ${CMAKE_COMMAND} -E env
+ ${FLUTTER_TOOL_ENVIRONMENT}
+ "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
+ ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
+ VERBATIM
+)
+add_custom_target(flutter_assemble DEPENDS
+ "${FLUTTER_LIBRARY}"
+ ${FLUTTER_LIBRARY_HEADERS}
+)
diff --git a/commis/app/linux/flutter/generated_plugin_registrant.cc b/commis/app/linux/flutter/generated_plugin_registrant.cc
new file mode 100644
index 00000000..cc10c4da
--- /dev/null
+++ b/commis/app/linux/flutter/generated_plugin_registrant.cc
@@ -0,0 +1,19 @@
+//
+// Generated file. Do not edit.
+//
+
+// clang-format off
+
+#include "generated_plugin_registrant.h"
+
+#include
+#include
+
+void fl_register_plugins(FlPluginRegistry* registry) {
+ g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar =
+ fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin");
+ audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar);
+ g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
+ fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
+ url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
+}
diff --git a/commis/app/linux/flutter/generated_plugin_registrant.h b/commis/app/linux/flutter/generated_plugin_registrant.h
new file mode 100644
index 00000000..e0f0a47b
--- /dev/null
+++ b/commis/app/linux/flutter/generated_plugin_registrant.h
@@ -0,0 +1,15 @@
+//
+// Generated file. Do not edit.
+//
+
+// clang-format off
+
+#ifndef GENERATED_PLUGIN_REGISTRANT_
+#define GENERATED_PLUGIN_REGISTRANT_
+
+#include
+
+// Registers Flutter plugins.
+void fl_register_plugins(FlPluginRegistry* registry);
+
+#endif // GENERATED_PLUGIN_REGISTRANT_
diff --git a/commis/app/linux/flutter/generated_plugins.cmake b/commis/app/linux/flutter/generated_plugins.cmake
new file mode 100644
index 00000000..a2eef970
--- /dev/null
+++ b/commis/app/linux/flutter/generated_plugins.cmake
@@ -0,0 +1,26 @@
+#
+# Generated file, do not edit.
+#
+
+list(APPEND FLUTTER_PLUGIN_LIST
+ audioplayers_linux
+ url_launcher_linux
+)
+
+list(APPEND FLUTTER_FFI_PLUGIN_LIST
+ jni
+)
+
+set(PLUGIN_BUNDLED_LIBRARIES)
+
+foreach(plugin ${FLUTTER_PLUGIN_LIST})
+ add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
+ target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES $)
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
+endforeach(plugin)
+
+foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
+ add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
+endforeach(ffi_plugin)
diff --git a/commis/app/linux/runner/CMakeLists.txt b/commis/app/linux/runner/CMakeLists.txt
new file mode 100644
index 00000000..e97dabc7
--- /dev/null
+++ b/commis/app/linux/runner/CMakeLists.txt
@@ -0,0 +1,26 @@
+cmake_minimum_required(VERSION 3.13)
+project(runner LANGUAGES CXX)
+
+# Define the application target. To change its name, change BINARY_NAME in the
+# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
+# work.
+#
+# Any new source files that you add to the application should be added here.
+add_executable(${BINARY_NAME}
+ "main.cc"
+ "my_application.cc"
+ "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
+)
+
+# Apply the standard set of build settings. This can be removed for applications
+# that need different build settings.
+apply_standard_settings(${BINARY_NAME})
+
+# Add preprocessor definitions for the application ID.
+add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
+
+# Add dependency libraries. Add any application-specific dependencies here.
+target_link_libraries(${BINARY_NAME} PRIVATE flutter)
+target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
+
+target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
diff --git a/commis/app/linux/runner/main.cc b/commis/app/linux/runner/main.cc
new file mode 100644
index 00000000..e7c5c543
--- /dev/null
+++ b/commis/app/linux/runner/main.cc
@@ -0,0 +1,6 @@
+#include "my_application.h"
+
+int main(int argc, char** argv) {
+ g_autoptr(MyApplication) app = my_application_new();
+ return g_application_run(G_APPLICATION(app), argc, argv);
+}
diff --git a/commis/app/linux/runner/my_application.cc b/commis/app/linux/runner/my_application.cc
new file mode 100644
index 00000000..16e0662e
--- /dev/null
+++ b/commis/app/linux/runner/my_application.cc
@@ -0,0 +1,148 @@
+#include "my_application.h"
+
+#include
+#ifdef GDK_WINDOWING_X11
+#include
+#endif
+
+#include "flutter/generated_plugin_registrant.h"
+
+struct _MyApplication {
+ GtkApplication parent_instance;
+ char** dart_entrypoint_arguments;
+};
+
+G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
+
+// Called when first Flutter frame received.
+static void first_frame_cb(MyApplication* self, FlView* view) {
+ gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
+}
+
+// Implements GApplication::activate.
+static void my_application_activate(GApplication* application) {
+ MyApplication* self = MY_APPLICATION(application);
+ GtkWindow* window =
+ GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
+
+ // Use a header bar when running in GNOME as this is the common style used
+ // by applications and is the setup most users will be using (e.g. Ubuntu
+ // desktop).
+ // If running on X and not using GNOME then just use a traditional title bar
+ // in case the window manager does more exotic layout, e.g. tiling.
+ // If running on Wayland assume the header bar will work (may need changing
+ // if future cases occur).
+ gboolean use_header_bar = TRUE;
+#ifdef GDK_WINDOWING_X11
+ GdkScreen* screen = gtk_window_get_screen(window);
+ if (GDK_IS_X11_SCREEN(screen)) {
+ const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
+ if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
+ use_header_bar = FALSE;
+ }
+ }
+#endif
+ if (use_header_bar) {
+ GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
+ gtk_widget_show(GTK_WIDGET(header_bar));
+ gtk_header_bar_set_title(header_bar, "commis");
+ gtk_header_bar_set_show_close_button(header_bar, TRUE);
+ gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
+ } else {
+ gtk_window_set_title(window, "commis");
+ }
+
+ gtk_window_set_default_size(window, 1280, 720);
+
+ g_autoptr(FlDartProject) project = fl_dart_project_new();
+ fl_dart_project_set_dart_entrypoint_arguments(
+ project, self->dart_entrypoint_arguments);
+
+ FlView* view = fl_view_new(project);
+ GdkRGBA background_color;
+ // Background defaults to black, override it here if necessary, e.g. #00000000
+ // for transparent.
+ gdk_rgba_parse(&background_color, "#000000");
+ fl_view_set_background_color(view, &background_color);
+ gtk_widget_show(GTK_WIDGET(view));
+ gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
+
+ // Show the window when Flutter renders.
+ // Requires the view to be realized so we can start rendering.
+ g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb),
+ self);
+ gtk_widget_realize(GTK_WIDGET(view));
+
+ fl_register_plugins(FL_PLUGIN_REGISTRY(view));
+
+ gtk_widget_grab_focus(GTK_WIDGET(view));
+}
+
+// Implements GApplication::local_command_line.
+static gboolean my_application_local_command_line(GApplication* application,
+ gchar*** arguments,
+ int* exit_status) {
+ MyApplication* self = MY_APPLICATION(application);
+ // Strip out the first argument as it is the binary name.
+ self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
+
+ g_autoptr(GError) error = nullptr;
+ if (!g_application_register(application, nullptr, &error)) {
+ g_warning("Failed to register: %s", error->message);
+ *exit_status = 1;
+ return TRUE;
+ }
+
+ g_application_activate(application);
+ *exit_status = 0;
+
+ return TRUE;
+}
+
+// Implements GApplication::startup.
+static void my_application_startup(GApplication* application) {
+ // MyApplication* self = MY_APPLICATION(object);
+
+ // Perform any actions required at application startup.
+
+ G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
+}
+
+// Implements GApplication::shutdown.
+static void my_application_shutdown(GApplication* application) {
+ // MyApplication* self = MY_APPLICATION(object);
+
+ // Perform any actions required at application shutdown.
+
+ G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
+}
+
+// Implements GObject::dispose.
+static void my_application_dispose(GObject* object) {
+ MyApplication* self = MY_APPLICATION(object);
+ g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
+ G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
+}
+
+static void my_application_class_init(MyApplicationClass* klass) {
+ G_APPLICATION_CLASS(klass)->activate = my_application_activate;
+ G_APPLICATION_CLASS(klass)->local_command_line =
+ my_application_local_command_line;
+ G_APPLICATION_CLASS(klass)->startup = my_application_startup;
+ G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
+ G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
+}
+
+static void my_application_init(MyApplication* self) {}
+
+MyApplication* my_application_new() {
+ // Set the program name to the application ID, which helps various systems
+ // like GTK and desktop environments map this running application to its
+ // corresponding .desktop file. This ensures better integration by allowing
+ // the application to be recognized beyond its binary name.
+ g_set_prgname(APPLICATION_ID);
+
+ return MY_APPLICATION(g_object_new(my_application_get_type(),
+ "application-id", APPLICATION_ID, "flags",
+ G_APPLICATION_NON_UNIQUE, nullptr));
+}
diff --git a/commis/app/linux/runner/my_application.h b/commis/app/linux/runner/my_application.h
new file mode 100644
index 00000000..db16367a
--- /dev/null
+++ b/commis/app/linux/runner/my_application.h
@@ -0,0 +1,21 @@
+#ifndef FLUTTER_MY_APPLICATION_H_
+#define FLUTTER_MY_APPLICATION_H_
+
+#include
+
+G_DECLARE_FINAL_TYPE(MyApplication,
+ my_application,
+ MY,
+ APPLICATION,
+ GtkApplication)
+
+/**
+ * my_application_new:
+ *
+ * Creates a new Flutter-based application.
+ *
+ * Returns: a new #MyApplication.
+ */
+MyApplication* my_application_new();
+
+#endif // FLUTTER_MY_APPLICATION_H_
diff --git a/commis/app/macos/.gitignore b/commis/app/macos/.gitignore
new file mode 100644
index 00000000..746adbb6
--- /dev/null
+++ b/commis/app/macos/.gitignore
@@ -0,0 +1,7 @@
+# Flutter-related
+**/Flutter/ephemeral/
+**/Pods/
+
+# Xcode-related
+**/dgph
+**/xcuserdata/
diff --git a/commis/app/macos/Flutter/Flutter-Debug.xcconfig b/commis/app/macos/Flutter/Flutter-Debug.xcconfig
new file mode 100644
index 00000000..c2efd0b6
--- /dev/null
+++ b/commis/app/macos/Flutter/Flutter-Debug.xcconfig
@@ -0,0 +1 @@
+#include "ephemeral/Flutter-Generated.xcconfig"
diff --git a/commis/app/macos/Flutter/Flutter-Release.xcconfig b/commis/app/macos/Flutter/Flutter-Release.xcconfig
new file mode 100644
index 00000000..c2efd0b6
--- /dev/null
+++ b/commis/app/macos/Flutter/Flutter-Release.xcconfig
@@ -0,0 +1 @@
+#include "ephemeral/Flutter-Generated.xcconfig"
diff --git a/commis/app/macos/Flutter/GeneratedPluginRegistrant.swift b/commis/app/macos/Flutter/GeneratedPluginRegistrant.swift
new file mode 100644
index 00000000..88a7efaa
--- /dev/null
+++ b/commis/app/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -0,0 +1,28 @@
+//
+// Generated file. Do not edit.
+//
+
+import FlutterMacOS
+import Foundation
+
+import audioplayers_darwin
+import cloud_firestore
+import cloud_functions
+import firebase_ai
+import firebase_app_check
+import firebase_auth
+import firebase_core
+import url_launcher_macos
+import video_player_avfoundation
+
+func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
+ AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
+ FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin"))
+ FirebaseFunctionsPlugin.register(with: registry.registrar(forPlugin: "FirebaseFunctionsPlugin"))
+ FirebaseAIPlugin.register(with: registry.registrar(forPlugin: "FirebaseAIPlugin"))
+ FirebaseAppCheckPlugin.register(with: registry.registrar(forPlugin: "FirebaseAppCheckPlugin"))
+ FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
+ FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
+ UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
+ VideoPlayerPlugin.register(with: registry.registrar(forPlugin: "VideoPlayerPlugin"))
+}
diff --git a/commis/app/macos/Runner.xcodeproj/project.pbxproj b/commis/app/macos/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 00000000..b921935c
--- /dev/null
+++ b/commis/app/macos/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,733 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 54;
+ objects = {
+
+/* Begin PBXAggregateTarget section */
+ 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
+ isa = PBXAggregateTarget;
+ buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
+ buildPhases = (
+ 33CC111E2044C6BF0003C045 /* ShellScript */,
+ );
+ dependencies = (
+ );
+ name = "Flutter Assemble";
+ productName = FLX;
+ };
+/* End PBXAggregateTarget section */
+
+/* Begin PBXBuildFile section */
+ 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
+ 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
+ 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
+ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
+ 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
+ 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
+ 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
+ A60A214A519208CA9373BA0C /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 81698C9C6C449ABB961C8FF2 /* GoogleService-Info.plist */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 33CC10EC2044A3C60003C045;
+ remoteInfo = Runner;
+ };
+ 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 33CC111A2044C6BA0003C045;
+ remoteInfo = FLX;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 33CC110E2044A8840003C045 /* Bundle Framework */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ );
+ name = "Bundle Framework";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; };
+ 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; };
+ 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; };
+ 33CC10ED2044A3C60003C045 /* commis.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = commis.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; };
+ 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; };
+ 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; };
+ 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; };
+ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; };
+ 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; };
+ 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; };
+ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; };
+ 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; };
+ 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; };
+ 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; };
+ 81698C9C6C449ABB961C8FF2 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 331C80D2294CF70F00263BE5 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 33CC10EA2044A3C60003C045 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 331C80D6294CF71000263BE5 /* RunnerTests */ = {
+ isa = PBXGroup;
+ children = (
+ 331C80D7294CF71000263BE5 /* RunnerTests.swift */,
+ );
+ path = RunnerTests;
+ sourceTree = "";
+ };
+ 33BA886A226E78AF003329D5 /* Configs */ = {
+ isa = PBXGroup;
+ children = (
+ 33E5194F232828860026EE4D /* AppInfo.xcconfig */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
+ );
+ path = Configs;
+ sourceTree = "";
+ };
+ 33CC10E42044A3C60003C045 = {
+ isa = PBXGroup;
+ children = (
+ 33FAB671232836740065AC1E /* Runner */,
+ 33CEB47122A05771004F2AC0 /* Flutter */,
+ 331C80D6294CF71000263BE5 /* RunnerTests */,
+ 33CC10EE2044A3C60003C045 /* Products */,
+ D73912EC22F37F3D000D13A0 /* Frameworks */,
+ 81698C9C6C449ABB961C8FF2 /* GoogleService-Info.plist */,
+ );
+ sourceTree = "";
+ };
+ 33CC10EE2044A3C60003C045 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 33CC10ED2044A3C60003C045 /* commis.app */,
+ 331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 33CC11242044D66E0003C045 /* Resources */ = {
+ isa = PBXGroup;
+ children = (
+ 33CC10F22044A3C60003C045 /* Assets.xcassets */,
+ 33CC10F42044A3C60003C045 /* MainMenu.xib */,
+ 33CC10F72044A3C60003C045 /* Info.plist */,
+ );
+ name = Resources;
+ path = ..;
+ sourceTree = "";
+ };
+ 33CEB47122A05771004F2AC0 /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
+ 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
+ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
+ 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
+ 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
+ );
+ path = Flutter;
+ sourceTree = "";
+ };
+ 33FAB671232836740065AC1E /* Runner */ = {
+ isa = PBXGroup;
+ children = (
+ 33CC10F02044A3C60003C045 /* AppDelegate.swift */,
+ 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
+ 33E51913231747F40026EE4D /* DebugProfile.entitlements */,
+ 33E51914231749380026EE4D /* Release.entitlements */,
+ 33CC11242044D66E0003C045 /* Resources */,
+ 33BA886A226E78AF003329D5 /* Configs */,
+ );
+ path = Runner;
+ sourceTree = "";
+ };
+ D73912EC22F37F3D000D13A0 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 331C80D4294CF70F00263BE5 /* RunnerTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
+ buildPhases = (
+ 331C80D1294CF70F00263BE5 /* Sources */,
+ 331C80D2294CF70F00263BE5 /* Frameworks */,
+ 331C80D3294CF70F00263BE5 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 331C80DA294CF71000263BE5 /* PBXTargetDependency */,
+ );
+ name = RunnerTests;
+ productName = RunnerTests;
+ productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ 33CC10EC2044A3C60003C045 /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ 33CC10E92044A3C60003C045 /* Sources */,
+ 33CC10EA2044A3C60003C045 /* Frameworks */,
+ 33CC10EB2044A3C60003C045 /* Resources */,
+ 33CC110E2044A8840003C045 /* Bundle Framework */,
+ 3399D490228B24CF009A79C7 /* ShellScript */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 33CC11202044C79F0003C045 /* PBXTargetDependency */,
+ );
+ name = Runner;
+ packageProductDependencies = (
+ 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
+ );
+ productName = Runner;
+ productReference = 33CC10ED2044A3C60003C045 /* commis.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 33CC10E52044A3C60003C045 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = YES;
+ LastSwiftUpdateCheck = 0920;
+ LastUpgradeCheck = 1510;
+ ORGANIZATIONNAME = "";
+ TargetAttributes = {
+ 331C80D4294CF70F00263BE5 = {
+ CreatedOnToolsVersion = 14.0;
+ TestTargetID = 33CC10EC2044A3C60003C045;
+ };
+ 33CC10EC2044A3C60003C045 = {
+ CreatedOnToolsVersion = 9.2;
+ LastSwiftMigration = 1100;
+ ProvisioningStyle = Automatic;
+ SystemCapabilities = {
+ com.apple.Sandbox = {
+ enabled = 1;
+ };
+ };
+ };
+ 33CC111A2044C6BA0003C045 = {
+ CreatedOnToolsVersion = 9.2;
+ ProvisioningStyle = Manual;
+ };
+ };
+ };
+ buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
+ compatibilityVersion = "Xcode 9.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 33CC10E42044A3C60003C045;
+ packageReferences = (
+ 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
+ );
+ productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 33CC10EC2044A3C60003C045 /* Runner */,
+ 331C80D4294CF70F00263BE5 /* RunnerTests */,
+ 33CC111A2044C6BA0003C045 /* Flutter Assemble */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 331C80D3294CF70F00263BE5 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 33CC10EB2044A3C60003C045 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
+ 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
+ A60A214A519208CA9373BA0C /* GoogleService-Info.plist in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 3399D490228B24CF009A79C7 /* ShellScript */ = {
+ isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ );
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
+ };
+ 33CC111E2044C6BF0003C045 /* ShellScript */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ Flutter/ephemeral/FlutterInputs.xcfilelist,
+ );
+ inputPaths = (
+ Flutter/ephemeral/tripwire,
+ );
+ outputFileListPaths = (
+ Flutter/ephemeral/FlutterOutputs.xcfilelist,
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 331C80D1294CF70F00263BE5 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 33CC10E92044A3C60003C045 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
+ 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
+ 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 33CC10EC2044A3C60003C045 /* Runner */;
+ targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */;
+ };
+ 33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
+ targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+ 33CC10F42044A3C60003C045 /* MainMenu.xib */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 33CC10F52044A3C60003C045 /* Base */,
+ );
+ name = MainMenu.xib;
+ path = Runner;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 331C80DB294CF71000263BE5 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.commis.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/commis.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/commis";
+ };
+ name = Debug;
+ };
+ 331C80DC294CF71000263BE5 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.commis.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/commis.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/commis";
+ };
+ name = Release;
+ };
+ 331C80DD294CF71000263BE5 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.commis.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/commis.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/commis";
+ };
+ name = Profile;
+ };
+ 338D0CE9231458BD00FA5F75 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ 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_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ 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_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CODE_SIGN_IDENTITY = "-";
+ COPY_PHASE_STRIP = NO;
+ DEAD_CODE_STRIPPING = YES;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.15;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = macosx;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ };
+ name = Profile;
+ };
+ 338D0CEA231458BD00FA5F75 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Profile;
+ };
+ 338D0CEB231458BD00FA5F75 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Manual;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Profile;
+ };
+ 33CC10F92044A3C60003C045 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ 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_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ 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_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CODE_SIGN_IDENTITY = "-";
+ COPY_PHASE_STRIP = NO;
+ DEAD_CODE_STRIPPING = YES;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ 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_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.15;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = macosx;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ 33CC10FA2044A3C60003C045 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ 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_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ 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_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CODE_SIGN_IDENTITY = "-";
+ COPY_PHASE_STRIP = NO;
+ DEAD_CODE_STRIPPING = YES;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.15;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = macosx;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ };
+ name = Release;
+ };
+ 33CC10FC2044A3C60003C045 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Debug;
+ };
+ 33CC10FD2044A3C60003C045 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Release;
+ };
+ 33CC111C2044C6BA0003C045 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Manual;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Debug;
+ };
+ 33CC111D2044C6BA0003C045 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Automatic;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 331C80DB294CF71000263BE5 /* Debug */,
+ 331C80DC294CF71000263BE5 /* Release */,
+ 331C80DD294CF71000263BE5 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 33CC10F92044A3C60003C045 /* Debug */,
+ 33CC10FA2044A3C60003C045 /* Release */,
+ 338D0CE9231458BD00FA5F75 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 33CC10FC2044A3C60003C045 /* Debug */,
+ 33CC10FD2044A3C60003C045 /* Release */,
+ 338D0CEA231458BD00FA5F75 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 33CC111C2044C6BA0003C045 /* Debug */,
+ 33CC111D2044C6BA0003C045 /* Release */,
+ 338D0CEB231458BD00FA5F75 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+
+/* Begin XCLocalSwiftPackageReference section */
+ 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
+ isa = XCLocalSwiftPackageReference;
+ relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
+ };
+/* End XCLocalSwiftPackageReference section */
+
+/* Begin XCSwiftPackageProductDependency section */
+ 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
+ isa = XCSwiftPackageProductDependency;
+ productName = FlutterGeneratedPluginSwiftPackage;
+ };
+/* End XCSwiftPackageProductDependency section */
+ };
+ rootObject = 33CC10E52044A3C60003C045 /* Project object */;
+}
diff --git a/commis/app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/commis/app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 00000000..18d98100
--- /dev/null
+++ b/commis/app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/commis/app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/commis/app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
new file mode 100644
index 00000000..bd303bcf
--- /dev/null
+++ b/commis/app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -0,0 +1,122 @@
+{
+ "pins" : [
+ {
+ "identity" : "abseil-cpp-binary",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/abseil-cpp-binary.git",
+ "state" : {
+ "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5",
+ "version" : "1.2024072200.0"
+ }
+ },
+ {
+ "identity" : "app-check",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/app-check.git",
+ "state" : {
+ "revision" : "bb4002485ff867768dec13bf904a2ddb050bd1b1",
+ "version" : "11.3.0"
+ }
+ },
+ {
+ "identity" : "firebase-ios-sdk",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/firebase/firebase-ios-sdk",
+ "state" : {
+ "revision" : "42e81d245e30e49ea6a5830cf2842d44a1591270",
+ "version" : "12.15.0"
+ }
+ },
+ {
+ "identity" : "google-ads-on-device-conversion-ios-sdk",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk",
+ "state" : {
+ "revision" : "dc39082d8881109d35b94b1c122164c0e8d08a55",
+ "version" : "3.6.1"
+ }
+ },
+ {
+ "identity" : "googleappmeasurement",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/GoogleAppMeasurement.git",
+ "state" : {
+ "revision" : "144855f40d8668927f256a3045f7fdc4c3f4338b",
+ "version" : "12.15.0"
+ }
+ },
+ {
+ "identity" : "googledatatransport",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/GoogleDataTransport.git",
+ "state" : {
+ "revision" : "617af071af9aa1d6a091d59a202910ac482128f9",
+ "version" : "10.1.0"
+ }
+ },
+ {
+ "identity" : "googleutilities",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/GoogleUtilities.git",
+ "state" : {
+ "revision" : "9f183ae842be978784f2963a343682e0c46d8fb3",
+ "version" : "8.1.2"
+ }
+ },
+ {
+ "identity" : "grpc-binary",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/grpc-binary.git",
+ "state" : {
+ "revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6",
+ "version" : "1.69.1"
+ }
+ },
+ {
+ "identity" : "gtm-session-fetcher",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/gtm-session-fetcher.git",
+ "state" : {
+ "revision" : "c0ac7575d70050c2973ba2318bd5af47f8e8153a",
+ "version" : "5.3.0"
+ }
+ },
+ {
+ "identity" : "interop-ios-for-google-sdks",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/interop-ios-for-google-sdks.git",
+ "state" : {
+ "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe",
+ "version" : "101.0.0"
+ }
+ },
+ {
+ "identity" : "leveldb",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/firebase/leveldb.git",
+ "state" : {
+ "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1",
+ "version" : "1.22.5"
+ }
+ },
+ {
+ "identity" : "nanopb",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/firebase/nanopb.git",
+ "state" : {
+ "revision" : "3851d94a41890dea16dc3db34caf60e585cb4163",
+ "version" : "2.30910.1"
+ }
+ },
+ {
+ "identity" : "promises",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/promises.git",
+ "state" : {
+ "revision" : "f4a19a3c313dc2616c70bb49d29a799fb16be837",
+ "version" : "2.4.1"
+ }
+ }
+ ],
+ "version" : 2
+}
diff --git a/commis/app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/commis/app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 00000000..80f91f1b
--- /dev/null
+++ b/commis/app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/commis/app/macos/Runner.xcworkspace/contents.xcworkspacedata b/commis/app/macos/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 00000000..1d526a16
--- /dev/null
+++ b/commis/app/macos/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/commis/app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/commis/app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 00000000..18d98100
--- /dev/null
+++ b/commis/app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/commis/app/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/commis/app/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved
new file mode 100644
index 00000000..bd303bcf
--- /dev/null
+++ b/commis/app/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -0,0 +1,122 @@
+{
+ "pins" : [
+ {
+ "identity" : "abseil-cpp-binary",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/abseil-cpp-binary.git",
+ "state" : {
+ "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5",
+ "version" : "1.2024072200.0"
+ }
+ },
+ {
+ "identity" : "app-check",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/app-check.git",
+ "state" : {
+ "revision" : "bb4002485ff867768dec13bf904a2ddb050bd1b1",
+ "version" : "11.3.0"
+ }
+ },
+ {
+ "identity" : "firebase-ios-sdk",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/firebase/firebase-ios-sdk",
+ "state" : {
+ "revision" : "42e81d245e30e49ea6a5830cf2842d44a1591270",
+ "version" : "12.15.0"
+ }
+ },
+ {
+ "identity" : "google-ads-on-device-conversion-ios-sdk",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk",
+ "state" : {
+ "revision" : "dc39082d8881109d35b94b1c122164c0e8d08a55",
+ "version" : "3.6.1"
+ }
+ },
+ {
+ "identity" : "googleappmeasurement",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/GoogleAppMeasurement.git",
+ "state" : {
+ "revision" : "144855f40d8668927f256a3045f7fdc4c3f4338b",
+ "version" : "12.15.0"
+ }
+ },
+ {
+ "identity" : "googledatatransport",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/GoogleDataTransport.git",
+ "state" : {
+ "revision" : "617af071af9aa1d6a091d59a202910ac482128f9",
+ "version" : "10.1.0"
+ }
+ },
+ {
+ "identity" : "googleutilities",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/GoogleUtilities.git",
+ "state" : {
+ "revision" : "9f183ae842be978784f2963a343682e0c46d8fb3",
+ "version" : "8.1.2"
+ }
+ },
+ {
+ "identity" : "grpc-binary",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/grpc-binary.git",
+ "state" : {
+ "revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6",
+ "version" : "1.69.1"
+ }
+ },
+ {
+ "identity" : "gtm-session-fetcher",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/gtm-session-fetcher.git",
+ "state" : {
+ "revision" : "c0ac7575d70050c2973ba2318bd5af47f8e8153a",
+ "version" : "5.3.0"
+ }
+ },
+ {
+ "identity" : "interop-ios-for-google-sdks",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/interop-ios-for-google-sdks.git",
+ "state" : {
+ "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe",
+ "version" : "101.0.0"
+ }
+ },
+ {
+ "identity" : "leveldb",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/firebase/leveldb.git",
+ "state" : {
+ "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1",
+ "version" : "1.22.5"
+ }
+ },
+ {
+ "identity" : "nanopb",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/firebase/nanopb.git",
+ "state" : {
+ "revision" : "3851d94a41890dea16dc3db34caf60e585cb4163",
+ "version" : "2.30910.1"
+ }
+ },
+ {
+ "identity" : "promises",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/promises.git",
+ "state" : {
+ "revision" : "f4a19a3c313dc2616c70bb49d29a799fb16be837",
+ "version" : "2.4.1"
+ }
+ }
+ ],
+ "version" : 2
+}
diff --git a/commis/app/macos/Runner/AppDelegate.swift b/commis/app/macos/Runner/AppDelegate.swift
new file mode 100644
index 00000000..b3c17614
--- /dev/null
+++ b/commis/app/macos/Runner/AppDelegate.swift
@@ -0,0 +1,13 @@
+import Cocoa
+import FlutterMacOS
+
+@main
+class AppDelegate: FlutterAppDelegate {
+ override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
+ return true
+ }
+
+ override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
+ return true
+ }
+}
diff --git a/commis/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/commis/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 00000000..a2ec33f1
--- /dev/null
+++ b/commis/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,68 @@
+{
+ "images" : [
+ {
+ "size" : "16x16",
+ "idiom" : "mac",
+ "filename" : "app_icon_16.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "16x16",
+ "idiom" : "mac",
+ "filename" : "app_icon_32.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "32x32",
+ "idiom" : "mac",
+ "filename" : "app_icon_32.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "32x32",
+ "idiom" : "mac",
+ "filename" : "app_icon_64.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "128x128",
+ "idiom" : "mac",
+ "filename" : "app_icon_128.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "128x128",
+ "idiom" : "mac",
+ "filename" : "app_icon_256.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "256x256",
+ "idiom" : "mac",
+ "filename" : "app_icon_256.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "256x256",
+ "idiom" : "mac",
+ "filename" : "app_icon_512.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "512x512",
+ "idiom" : "mac",
+ "filename" : "app_icon_512.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "512x512",
+ "idiom" : "mac",
+ "filename" : "app_icon_1024.png",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/commis/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/commis/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
new file mode 100644
index 00000000..82b6f9d9
Binary files /dev/null and b/commis/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ
diff --git a/commis/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/commis/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
new file mode 100644
index 00000000..13b35eba
Binary files /dev/null and b/commis/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ
diff --git a/commis/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/commis/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
new file mode 100644
index 00000000..0a3f5fa4
Binary files /dev/null and b/commis/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ
diff --git a/commis/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/commis/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
new file mode 100644
index 00000000..bdb57226
Binary files /dev/null and b/commis/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ
diff --git a/commis/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/commis/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
new file mode 100644
index 00000000..f083318e
Binary files /dev/null and b/commis/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ
diff --git a/commis/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/commis/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
new file mode 100644
index 00000000..326c0e72
Binary files /dev/null and b/commis/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ
diff --git a/commis/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/commis/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
new file mode 100644
index 00000000..2f1632cf
Binary files /dev/null and b/commis/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ
diff --git a/commis/app/macos/Runner/Base.lproj/MainMenu.xib b/commis/app/macos/Runner/Base.lproj/MainMenu.xib
new file mode 100644
index 00000000..80e867a4
--- /dev/null
+++ b/commis/app/macos/Runner/Base.lproj/MainMenu.xib
@@ -0,0 +1,343 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/commis/app/macos/Runner/Configs/AppInfo.xcconfig b/commis/app/macos/Runner/Configs/AppInfo.xcconfig
new file mode 100644
index 00000000..ea731dab
--- /dev/null
+++ b/commis/app/macos/Runner/Configs/AppInfo.xcconfig
@@ -0,0 +1,14 @@
+// Application-level settings for the Runner target.
+//
+// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the
+// future. If not, the values below would default to using the project name when this becomes a
+// 'flutter create' template.
+
+// The application's name. By default this is also the title of the Flutter window.
+PRODUCT_NAME = commis
+
+// The application's bundle identifier
+PRODUCT_BUNDLE_IDENTIFIER = com.example.commis
+
+// The copyright displayed in application information
+PRODUCT_COPYRIGHT = Copyright © 2026 com.example. All rights reserved.
diff --git a/commis/app/macos/Runner/Configs/Debug.xcconfig b/commis/app/macos/Runner/Configs/Debug.xcconfig
new file mode 100644
index 00000000..36b0fd94
--- /dev/null
+++ b/commis/app/macos/Runner/Configs/Debug.xcconfig
@@ -0,0 +1,2 @@
+#include "../../Flutter/Flutter-Debug.xcconfig"
+#include "Warnings.xcconfig"
diff --git a/commis/app/macos/Runner/Configs/Release.xcconfig b/commis/app/macos/Runner/Configs/Release.xcconfig
new file mode 100644
index 00000000..dff4f495
--- /dev/null
+++ b/commis/app/macos/Runner/Configs/Release.xcconfig
@@ -0,0 +1,2 @@
+#include "../../Flutter/Flutter-Release.xcconfig"
+#include "Warnings.xcconfig"
diff --git a/commis/app/macos/Runner/Configs/Warnings.xcconfig b/commis/app/macos/Runner/Configs/Warnings.xcconfig
new file mode 100644
index 00000000..42bcbf47
--- /dev/null
+++ b/commis/app/macos/Runner/Configs/Warnings.xcconfig
@@ -0,0 +1,13 @@
+WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings
+GCC_WARN_UNDECLARED_SELECTOR = YES
+CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES
+CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
+CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
+CLANG_WARN_PRAGMA_PACK = YES
+CLANG_WARN_STRICT_PROTOTYPES = YES
+CLANG_WARN_COMMA = YES
+GCC_WARN_STRICT_SELECTOR_MATCH = YES
+CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES
+CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
+GCC_WARN_SHADOW = YES
+CLANG_WARN_UNREACHABLE_CODE = YES
diff --git a/commis/app/macos/Runner/DebugProfile.entitlements b/commis/app/macos/Runner/DebugProfile.entitlements
new file mode 100644
index 00000000..08c3ab17
--- /dev/null
+++ b/commis/app/macos/Runner/DebugProfile.entitlements
@@ -0,0 +1,14 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+ com.apple.security.cs.allow-jit
+
+ com.apple.security.network.server
+
+ com.apple.security.network.client
+
+
+
diff --git a/commis/app/macos/Runner/Info.plist b/commis/app/macos/Runner/Info.plist
new file mode 100644
index 00000000..4789daa6
--- /dev/null
+++ b/commis/app/macos/Runner/Info.plist
@@ -0,0 +1,32 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIconFile
+
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSMinimumSystemVersion
+ $(MACOSX_DEPLOYMENT_TARGET)
+ NSHumanReadableCopyright
+ $(PRODUCT_COPYRIGHT)
+ NSMainNibFile
+ MainMenu
+ NSPrincipalClass
+ NSApplication
+
+
diff --git a/commis/app/macos/Runner/MainFlutterWindow.swift b/commis/app/macos/Runner/MainFlutterWindow.swift
new file mode 100644
index 00000000..3cc05eb2
--- /dev/null
+++ b/commis/app/macos/Runner/MainFlutterWindow.swift
@@ -0,0 +1,15 @@
+import Cocoa
+import FlutterMacOS
+
+class MainFlutterWindow: NSWindow {
+ override func awakeFromNib() {
+ let flutterViewController = FlutterViewController()
+ let windowFrame = self.frame
+ self.contentViewController = flutterViewController
+ self.setFrame(windowFrame, display: true)
+
+ RegisterGeneratedPlugins(registry: flutterViewController)
+
+ super.awakeFromNib()
+ }
+}
diff --git a/commis/app/macos/Runner/Release.entitlements b/commis/app/macos/Runner/Release.entitlements
new file mode 100644
index 00000000..ee95ab7e
--- /dev/null
+++ b/commis/app/macos/Runner/Release.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+ com.apple.security.network.client
+
+
+
diff --git a/commis/app/macos/RunnerTests/RunnerTests.swift b/commis/app/macos/RunnerTests/RunnerTests.swift
new file mode 100644
index 00000000..61f3bd1f
--- /dev/null
+++ b/commis/app/macos/RunnerTests/RunnerTests.swift
@@ -0,0 +1,12 @@
+import Cocoa
+import FlutterMacOS
+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.
+ }
+
+}
diff --git a/commis/app/pubspec.yaml b/commis/app/pubspec.yaml
new file mode 100644
index 00000000..e42aeca3
--- /dev/null
+++ b/commis/app/pubspec.yaml
@@ -0,0 +1,59 @@
+name: commis
+description: "A new Flutter project."
+publish_to: 'none'
+
+version: 1.0.0+1
+
+environment:
+ sdk: ^3.12.2
+dependencies:
+ flutter:
+ sdk: flutter
+
+ firebase_core: ^4.11.0
+ firebase_auth: ^6.0.0
+ firebase_ai: ^3.13.1
+ cloud_firestore: ^6.0.0
+ cloud_functions: ^6.3.3
+ genui:
+ git:
+ url: https://github.com/flutter/genui.git
+ ref: f2b94cea1876cea42ae9c44c6dcae532d3072d28
+ path: packages/genui
+ json_schema_builder: ^0.1.5
+ google_fonts: ^8.1.0
+ cupertino_icons: ^1.0.8
+ go_router: ^17.3.0
+ provider: ^6.1.5+1
+ flutter_markdown: ^0.7.7+1
+ google_maps_flutter: ^2.17.1
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+ flutter_lints: ^6.0.0
+
+flutter:
+ uses-material-design: true
+ assets:
+ - assets/images/ingredients/200/
+ - assets/images/recipes/200/
+
+# Dependency overrides are required because client-side functions are currently
+# available @HEAD, but not in the published version (they're the new hotness!).
+dependency_overrides:
+ json_schema_builder:
+ git:
+ url: https://github.com/flutter/genui.git
+ ref: f2b94cea1876cea42ae9c44c6dcae532d3072d28
+ path: packages/json_schema_builder
+ a2ui_core:
+ git:
+ url: https://github.com/flutter/genui.git
+ ref: f2b94cea1876cea42ae9c44c6dcae532d3072d28
+ path: packages/a2ui_core
+ genai_primitives:
+ git:
+ url: https://github.com/flutter/genui.git
+ ref: f2b94cea1876cea42ae9c44c6dcae532d3072d28
+ path: packages/genai_primitives
diff --git a/commis/app/test/commis_test.dart b/commis/app/test/commis_test.dart
new file mode 100644
index 00000000..b6c27a8f
--- /dev/null
+++ b/commis/app/test/commis_test.dart
@@ -0,0 +1,61 @@
+// Copyright 2026 The Flutter Authors.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:commis/data/models.dart';
+import 'package:commis/services/cost_service.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+ group('CostService Tests', () {
+ final costService = CostService();
+
+ test('calculates correct cost for known ingredients', () {
+ // Russet Potatoes are 0.50/lb
+ expect(costService.fetchPrice('russet_potatoes', 2.0), closeTo(1.00, 0.001));
+ // Unsalted Butter is 2.20/ea
+ expect(costService.fetchPrice('unsalted_butter', 5.0), closeTo(11.00, 0.001));
+ });
+
+ test('returns 0.0 for unknown ingredients', () {
+ expect(costService.fetchPrice('non_existent_ingredient', 10.0), 0.0);
+ });
+ });
+
+ group('CateringJob Model Tests', () {
+ test('parses from valid firestore map', () {
+ final mockData = {
+ 'title': 'Test catering job',
+ 'address': '123 Test St',
+ 'guestCount': 100,
+ 'date': '2026-07-20T12:00:00.000',
+ 'recipeIds': ['recipe_1', 'recipe_2'],
+ 'latitude': 37.422,
+ 'longitude': -122.084,
+ };
+
+ final job = CateringJob.fromMap('job_test_id', mockData);
+
+ expect(job.id, 'job_test_id');
+ expect(job.title, 'Test catering job');
+ expect(job.address, '123 Test St');
+ expect(job.guestCount, 100);
+ expect(job.date.year, 2026);
+ expect(job.recipeIds, containsAll(['recipe_1', 'recipe_2']));
+ expect(job.latitude, 37.422);
+ expect(job.longitude, -122.084);
+ });
+
+ test('uses defaults for empty map fields', () {
+ final job = CateringJob.fromMap('job_empty_id', {});
+
+ expect(job.id, 'job_empty_id');
+ expect(job.title, '');
+ expect(job.address, '');
+ expect(job.guestCount, 0);
+ expect(job.recipeIds, isEmpty);
+ expect(job.latitude, 0.0);
+ expect(job.longitude, 0.0);
+ });
+ });
+}
diff --git a/commis/app/test/generate_prompt_test.dart b/commis/app/test/generate_prompt_test.dart
new file mode 100644
index 00000000..4d94486c
--- /dev/null
+++ b/commis/app/test/generate_prompt_test.dart
@@ -0,0 +1,22 @@
+// Copyright 2026 The Flutter Authors.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:commis/ui/catalog/catalog.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:genui/genui.dart';
+
+void main() {
+ test('Generate prompt and save to a file.', () {
+ final promptBuilder = PromptBuilder.chat(
+ catalog: commisCatalog,
+ systemPromptFragments: [],
+ );
+
+ File(
+ 'catalog_prompt.txt',
+ ).writeAsStringSync(promptBuilder.systemPromptJoined());
+ });
+}
diff --git a/commis/app/web/favicon.png b/commis/app/web/favicon.png
new file mode 100644
index 00000000..8aaa46ac
Binary files /dev/null and b/commis/app/web/favicon.png differ
diff --git a/commis/app/web/icons/Icon-192.png b/commis/app/web/icons/Icon-192.png
new file mode 100644
index 00000000..b749bfef
Binary files /dev/null and b/commis/app/web/icons/Icon-192.png differ
diff --git a/commis/app/web/icons/Icon-512.png b/commis/app/web/icons/Icon-512.png
new file mode 100644
index 00000000..88cfd48d
Binary files /dev/null and b/commis/app/web/icons/Icon-512.png differ
diff --git a/commis/app/web/icons/Icon-maskable-192.png b/commis/app/web/icons/Icon-maskable-192.png
new file mode 100644
index 00000000..eb9b4d76
Binary files /dev/null and b/commis/app/web/icons/Icon-maskable-192.png differ
diff --git a/commis/app/web/icons/Icon-maskable-512.png b/commis/app/web/icons/Icon-maskable-512.png
new file mode 100644
index 00000000..d69c5669
Binary files /dev/null and b/commis/app/web/icons/Icon-maskable-512.png differ
diff --git a/commis/app/web/index.html b/commis/app/web/index.html
new file mode 100644
index 00000000..ba115c7b
--- /dev/null
+++ b/commis/app/web/index.html
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ commis
+
+
+
+
+
+
+
diff --git a/commis/app/web/manifest.json b/commis/app/web/manifest.json
new file mode 100644
index 00000000..97f1b80f
--- /dev/null
+++ b/commis/app/web/manifest.json
@@ -0,0 +1,35 @@
+{
+ "name": "commis",
+ "short_name": "commis",
+ "start_url": ".",
+ "display": "standalone",
+ "background_color": "#0175C2",
+ "theme_color": "#0175C2",
+ "description": "A new Flutter project.",
+ "orientation": "portrait-primary",
+ "prefer_related_applications": false,
+ "icons": [
+ {
+ "src": "icons/Icon-192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "icons/Icon-512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ },
+ {
+ "src": "icons/Icon-maskable-192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "icons/Icon-maskable-512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "maskable"
+ }
+ ]
+}
diff --git a/commis/app/windows/.gitignore b/commis/app/windows/.gitignore
new file mode 100644
index 00000000..d492d0d9
--- /dev/null
+++ b/commis/app/windows/.gitignore
@@ -0,0 +1,17 @@
+flutter/ephemeral/
+
+# Visual Studio user-specific files.
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# Visual Studio build-related files.
+x64/
+x86/
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
diff --git a/commis/app/windows/CMakeLists.txt b/commis/app/windows/CMakeLists.txt
new file mode 100644
index 00000000..6c0cdea1
--- /dev/null
+++ b/commis/app/windows/CMakeLists.txt
@@ -0,0 +1,108 @@
+# Project-level configuration.
+cmake_minimum_required(VERSION 3.14)
+project(commis LANGUAGES CXX)
+
+# The name of the executable created for the application. Change this to change
+# the on-disk name of your application.
+set(BINARY_NAME "commis")
+
+# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
+# versions of CMake.
+cmake_policy(VERSION 3.14...3.25)
+
+# Define build configuration option.
+get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
+if(IS_MULTICONFIG)
+ set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
+ CACHE STRING "" FORCE)
+else()
+ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
+ set(CMAKE_BUILD_TYPE "Debug" CACHE
+ STRING "Flutter build mode" FORCE)
+ set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
+ "Debug" "Profile" "Release")
+ endif()
+endif()
+# Define settings for the Profile build mode.
+set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
+set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")
+set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}")
+set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
+
+# Use Unicode for all projects.
+add_definitions(-DUNICODE -D_UNICODE)
+
+# Compilation settings that should be applied to most targets.
+#
+# Be cautious about adding new options here, as plugins use this function by
+# default. In most cases, you should add new options to specific targets instead
+# of modifying this function.
+function(APPLY_STANDARD_SETTINGS TARGET)
+ target_compile_features(${TARGET} PUBLIC cxx_std_17)
+ target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100")
+ target_compile_options(${TARGET} PRIVATE /EHsc)
+ target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
+ target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>")
+endfunction()
+
+# Flutter library and tool build rules.
+set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
+add_subdirectory(${FLUTTER_MANAGED_DIR})
+
+# Application build; see runner/CMakeLists.txt.
+add_subdirectory("runner")
+
+
+# Generated plugin build rules, which manage building the plugins and adding
+# them to the application.
+include(flutter/generated_plugins.cmake)
+
+
+# === Installation ===
+# Support files are copied into place next to the executable, so that it can
+# run in place. This is done instead of making a separate bundle (as on Linux)
+# so that building and running from within Visual Studio will work.
+set(BUILD_BUNDLE_DIR "$")
+# Make the "install" step default, as it's required to run.
+set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
+if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
+ set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
+endif()
+
+set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
+set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
+
+install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
+ COMPONENT Runtime)
+
+install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
+ COMPONENT Runtime)
+
+install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+
+if(PLUGIN_BUNDLED_LIBRARIES)
+ install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
+ DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+endif()
+
+# Copy the native assets provided by the build.dart from all packages.
+set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/")
+install(DIRECTORY "${NATIVE_ASSETS_DIR}"
+ DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+
+# Fully re-copy the assets directory on each build to avoid having stale files
+# from a previous install.
+set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
+install(CODE "
+ file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
+ " COMPONENT Runtime)
+install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
+ DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
+
+# Install the AOT library on non-Debug builds only.
+install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
+ CONFIGURATIONS Profile;Release
+ COMPONENT Runtime)
diff --git a/commis/app/windows/flutter/CMakeLists.txt b/commis/app/windows/flutter/CMakeLists.txt
new file mode 100644
index 00000000..903f4899
--- /dev/null
+++ b/commis/app/windows/flutter/CMakeLists.txt
@@ -0,0 +1,109 @@
+# This file controls Flutter-level build steps. It should not be edited.
+cmake_minimum_required(VERSION 3.14)
+
+set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
+
+# Configuration provided via flutter tool.
+include(${EPHEMERAL_DIR}/generated_config.cmake)
+
+# TODO: Move the rest of this into files in ephemeral. See
+# https://github.com/flutter/flutter/issues/57146.
+set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
+
+# Set fallback configurations for older versions of the flutter tool.
+if (NOT DEFINED FLUTTER_TARGET_PLATFORM)
+ set(FLUTTER_TARGET_PLATFORM "windows-x64")
+endif()
+
+# === Flutter Library ===
+set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
+
+# Published to parent scope for install step.
+set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
+set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
+set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
+set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE)
+
+list(APPEND FLUTTER_LIBRARY_HEADERS
+ "flutter_export.h"
+ "flutter_windows.h"
+ "flutter_messenger.h"
+ "flutter_plugin_registrar.h"
+ "flutter_texture_registrar.h"
+)
+list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
+add_library(flutter INTERFACE)
+target_include_directories(flutter INTERFACE
+ "${EPHEMERAL_DIR}"
+)
+target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib")
+add_dependencies(flutter flutter_assemble)
+
+# === Wrapper ===
+list(APPEND CPP_WRAPPER_SOURCES_CORE
+ "core_implementations.cc"
+ "standard_codec.cc"
+)
+list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
+list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
+ "plugin_registrar.cc"
+)
+list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
+list(APPEND CPP_WRAPPER_SOURCES_APP
+ "flutter_engine.cc"
+ "flutter_view_controller.cc"
+)
+list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/")
+
+# Wrapper sources needed for a plugin.
+add_library(flutter_wrapper_plugin STATIC
+ ${CPP_WRAPPER_SOURCES_CORE}
+ ${CPP_WRAPPER_SOURCES_PLUGIN}
+)
+apply_standard_settings(flutter_wrapper_plugin)
+set_target_properties(flutter_wrapper_plugin PROPERTIES
+ POSITION_INDEPENDENT_CODE ON)
+set_target_properties(flutter_wrapper_plugin PROPERTIES
+ CXX_VISIBILITY_PRESET hidden)
+target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
+target_include_directories(flutter_wrapper_plugin PUBLIC
+ "${WRAPPER_ROOT}/include"
+)
+add_dependencies(flutter_wrapper_plugin flutter_assemble)
+
+# Wrapper sources needed for the runner.
+add_library(flutter_wrapper_app STATIC
+ ${CPP_WRAPPER_SOURCES_CORE}
+ ${CPP_WRAPPER_SOURCES_APP}
+)
+apply_standard_settings(flutter_wrapper_app)
+target_link_libraries(flutter_wrapper_app PUBLIC flutter)
+target_include_directories(flutter_wrapper_app PUBLIC
+ "${WRAPPER_ROOT}/include"
+)
+add_dependencies(flutter_wrapper_app flutter_assemble)
+
+# === Flutter tool backend ===
+# _phony_ is a non-existent file to force this command to run every time,
+# since currently there's no way to get a full input/output list from the
+# flutter tool.
+set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")
+set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE)
+add_custom_command(
+ OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
+ ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
+ ${CPP_WRAPPER_SOURCES_APP}
+ ${PHONY_OUTPUT}
+ COMMAND ${CMAKE_COMMAND} -E env
+ ${FLUTTER_TOOL_ENVIRONMENT}
+ "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
+ ${FLUTTER_TARGET_PLATFORM} $
+ VERBATIM
+)
+add_custom_target(flutter_assemble DEPENDS
+ "${FLUTTER_LIBRARY}"
+ ${FLUTTER_LIBRARY_HEADERS}
+ ${CPP_WRAPPER_SOURCES_CORE}
+ ${CPP_WRAPPER_SOURCES_PLUGIN}
+ ${CPP_WRAPPER_SOURCES_APP}
+)
diff --git a/commis/app/windows/flutter/generated_plugin_registrant.cc b/commis/app/windows/flutter/generated_plugin_registrant.cc
new file mode 100644
index 00000000..a3762973
--- /dev/null
+++ b/commis/app/windows/flutter/generated_plugin_registrant.cc
@@ -0,0 +1,32 @@
+//
+// Generated file. Do not edit.
+//
+
+// clang-format off
+
+#include "generated_plugin_registrant.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+void RegisterPlugins(flutter::PluginRegistry* registry) {
+ AudioplayersWindowsPluginRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin"));
+ CloudFirestorePluginCApiRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("CloudFirestorePluginCApi"));
+ FirebaseAppCheckPluginCApiRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("FirebaseAppCheckPluginCApi"));
+ FirebaseAuthPluginCApiRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi"));
+ FirebaseCorePluginCApiRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
+ UrlLauncherWindowsRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("UrlLauncherWindows"));
+ VideoPlayerWinPluginCApiRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("VideoPlayerWinPluginCApi"));
+}
diff --git a/commis/app/windows/flutter/generated_plugin_registrant.h b/commis/app/windows/flutter/generated_plugin_registrant.h
new file mode 100644
index 00000000..dc139d85
--- /dev/null
+++ b/commis/app/windows/flutter/generated_plugin_registrant.h
@@ -0,0 +1,15 @@
+//
+// Generated file. Do not edit.
+//
+
+// clang-format off
+
+#ifndef GENERATED_PLUGIN_REGISTRANT_
+#define GENERATED_PLUGIN_REGISTRANT_
+
+#include
+
+// Registers Flutter plugins.
+void RegisterPlugins(flutter::PluginRegistry* registry);
+
+#endif // GENERATED_PLUGIN_REGISTRANT_
diff --git a/commis/app/windows/flutter/generated_plugins.cmake b/commis/app/windows/flutter/generated_plugins.cmake
new file mode 100644
index 00000000..4de27ac9
--- /dev/null
+++ b/commis/app/windows/flutter/generated_plugins.cmake
@@ -0,0 +1,31 @@
+#
+# Generated file, do not edit.
+#
+
+list(APPEND FLUTTER_PLUGIN_LIST
+ audioplayers_windows
+ cloud_firestore
+ firebase_app_check
+ firebase_auth
+ firebase_core
+ url_launcher_windows
+ video_player_win
+)
+
+list(APPEND FLUTTER_FFI_PLUGIN_LIST
+ jni
+)
+
+set(PLUGIN_BUNDLED_LIBRARIES)
+
+foreach(plugin ${FLUTTER_PLUGIN_LIST})
+ add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
+ target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES $)
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
+endforeach(plugin)
+
+foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
+ add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
+endforeach(ffi_plugin)
diff --git a/commis/app/windows/runner/CMakeLists.txt b/commis/app/windows/runner/CMakeLists.txt
new file mode 100644
index 00000000..394917c0
--- /dev/null
+++ b/commis/app/windows/runner/CMakeLists.txt
@@ -0,0 +1,40 @@
+cmake_minimum_required(VERSION 3.14)
+project(runner LANGUAGES CXX)
+
+# Define the application target. To change its name, change BINARY_NAME in the
+# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
+# work.
+#
+# Any new source files that you add to the application should be added here.
+add_executable(${BINARY_NAME} WIN32
+ "flutter_window.cpp"
+ "main.cpp"
+ "utils.cpp"
+ "win32_window.cpp"
+ "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
+ "Runner.rc"
+ "runner.exe.manifest"
+)
+
+# Apply the standard set of build settings. This can be removed for applications
+# that need different build settings.
+apply_standard_settings(${BINARY_NAME})
+
+# Add preprocessor definitions for the build version.
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
+
+# Disable Windows macros that collide with C++ standard library functions.
+target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
+
+# Add dependency libraries and include directories. Add any application-specific
+# dependencies here.
+target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
+target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
+target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
+
+# Run the Flutter tool portions of the build. This must not be removed.
+add_dependencies(${BINARY_NAME} flutter_assemble)
diff --git a/commis/app/windows/runner/Runner.rc b/commis/app/windows/runner/Runner.rc
new file mode 100644
index 00000000..3ca78284
--- /dev/null
+++ b/commis/app/windows/runner/Runner.rc
@@ -0,0 +1,121 @@
+// Microsoft Visual C++ generated resource script.
+//
+#pragma code_page(65001)
+#include "resource.h"
+
+#define APSTUDIO_READONLY_SYMBOLS
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 2 resource.
+//
+#include "winres.h"
+
+/////////////////////////////////////////////////////////////////////////////
+#undef APSTUDIO_READONLY_SYMBOLS
+
+/////////////////////////////////////////////////////////////////////////////
+// English (United States) resources
+
+#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
+LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
+
+#ifdef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// TEXTINCLUDE
+//
+
+1 TEXTINCLUDE
+BEGIN
+ "resource.h\0"
+END
+
+2 TEXTINCLUDE
+BEGIN
+ "#include ""winres.h""\r\n"
+ "\0"
+END
+
+3 TEXTINCLUDE
+BEGIN
+ "\r\n"
+ "\0"
+END
+
+#endif // APSTUDIO_INVOKED
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Icon
+//
+
+// Icon with lowest ID value placed first to ensure application icon
+// remains consistent on all systems.
+IDI_APP_ICON ICON "resources\\app_icon.ico"
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Version
+//
+
+#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
+#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
+#else
+#define VERSION_AS_NUMBER 1,0,0,0
+#endif
+
+#if defined(FLUTTER_VERSION)
+#define VERSION_AS_STRING FLUTTER_VERSION
+#else
+#define VERSION_AS_STRING "1.0.0"
+#endif
+
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION VERSION_AS_NUMBER
+ PRODUCTVERSION VERSION_AS_NUMBER
+ FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
+#ifdef _DEBUG
+ FILEFLAGS VS_FF_DEBUG
+#else
+ FILEFLAGS 0x0L
+#endif
+ FILEOS VOS__WINDOWS32
+ FILETYPE VFT_APP
+ FILESUBTYPE 0x0L
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040904e4"
+ BEGIN
+ VALUE "CompanyName", "com.example" "\0"
+ VALUE "FileDescription", "commis" "\0"
+ VALUE "FileVersion", VERSION_AS_STRING "\0"
+ VALUE "InternalName", "commis" "\0"
+ VALUE "LegalCopyright", "Copyright (C) 2026 com.example. All rights reserved." "\0"
+ VALUE "OriginalFilename", "commis.exe" "\0"
+ VALUE "ProductName", "commis" "\0"
+ VALUE "ProductVersion", VERSION_AS_STRING "\0"
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x409, 1252
+ END
+END
+
+#endif // English (United States) resources
+/////////////////////////////////////////////////////////////////////////////
+
+
+
+#ifndef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 3 resource.
+//
+
+
+/////////////////////////////////////////////////////////////////////////////
+#endif // not APSTUDIO_INVOKED
diff --git a/commis/app/windows/runner/flutter_window.cpp b/commis/app/windows/runner/flutter_window.cpp
new file mode 100644
index 00000000..955ee303
--- /dev/null
+++ b/commis/app/windows/runner/flutter_window.cpp
@@ -0,0 +1,71 @@
+#include "flutter_window.h"
+
+#include
+
+#include "flutter/generated_plugin_registrant.h"
+
+FlutterWindow::FlutterWindow(const flutter::DartProject& project)
+ : project_(project) {}
+
+FlutterWindow::~FlutterWindow() {}
+
+bool FlutterWindow::OnCreate() {
+ if (!Win32Window::OnCreate()) {
+ return false;
+ }
+
+ RECT frame = GetClientArea();
+
+ // The size here must match the window dimensions to avoid unnecessary surface
+ // creation / destruction in the startup path.
+ flutter_controller_ = std::make_unique(
+ frame.right - frame.left, frame.bottom - frame.top, project_);
+ // Ensure that basic setup of the controller was successful.
+ if (!flutter_controller_->engine() || !flutter_controller_->view()) {
+ return false;
+ }
+ RegisterPlugins(flutter_controller_->engine());
+ SetChildContent(flutter_controller_->view()->GetNativeWindow());
+
+ flutter_controller_->engine()->SetNextFrameCallback([&]() {
+ this->Show();
+ });
+
+ // Flutter can complete the first frame before the "show window" callback is
+ // registered. The following call ensures a frame is pending to ensure the
+ // window is shown. It is a no-op if the first frame hasn't completed yet.
+ flutter_controller_->ForceRedraw();
+
+ return true;
+}
+
+void FlutterWindow::OnDestroy() {
+ if (flutter_controller_) {
+ flutter_controller_ = nullptr;
+ }
+
+ Win32Window::OnDestroy();
+}
+
+LRESULT
+FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept {
+ // Give Flutter, including plugins, an opportunity to handle window messages.
+ if (flutter_controller_) {
+ std::optional result =
+ flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
+ lparam);
+ if (result) {
+ return *result;
+ }
+ }
+
+ switch (message) {
+ case WM_FONTCHANGE:
+ flutter_controller_->engine()->ReloadSystemFonts();
+ break;
+ }
+
+ return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
+}
diff --git a/commis/app/windows/runner/flutter_window.h b/commis/app/windows/runner/flutter_window.h
new file mode 100644
index 00000000..6da0652f
--- /dev/null
+++ b/commis/app/windows/runner/flutter_window.h
@@ -0,0 +1,33 @@
+#ifndef RUNNER_FLUTTER_WINDOW_H_
+#define RUNNER_FLUTTER_WINDOW_H_
+
+#include
+#include
+
+#include
+
+#include "win32_window.h"
+
+// A window that does nothing but host a Flutter view.
+class FlutterWindow : public Win32Window {
+ public:
+ // Creates a new FlutterWindow hosting a Flutter view running |project|.
+ explicit FlutterWindow(const flutter::DartProject& project);
+ virtual ~FlutterWindow();
+
+ protected:
+ // Win32Window:
+ bool OnCreate() override;
+ void OnDestroy() override;
+ LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,
+ LPARAM const lparam) noexcept override;
+
+ private:
+ // The project to run.
+ flutter::DartProject project_;
+
+ // The Flutter instance hosted by this window.
+ std::unique_ptr flutter_controller_;
+};
+
+#endif // RUNNER_FLUTTER_WINDOW_H_
diff --git a/commis/app/windows/runner/main.cpp b/commis/app/windows/runner/main.cpp
new file mode 100644
index 00000000..e4274a57
--- /dev/null
+++ b/commis/app/windows/runner/main.cpp
@@ -0,0 +1,43 @@
+#include
+#include
+#include
+
+#include "flutter_window.h"
+#include "utils.h"
+
+int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
+ _In_ wchar_t *command_line, _In_ int show_command) {
+ // Attach to console when present (e.g., 'flutter run') or create a
+ // new console when running with a debugger.
+ if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
+ CreateAndAttachConsole();
+ }
+
+ // Initialize COM, so that it is available for use in the library and/or
+ // plugins.
+ ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
+
+ flutter::DartProject project(L"data");
+
+ std::vector command_line_arguments =
+ GetCommandLineArguments();
+
+ project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
+
+ FlutterWindow window(project);
+ Win32Window::Point origin(10, 10);
+ Win32Window::Size size(1280, 720);
+ if (!window.Create(L"commis", origin, size)) {
+ return EXIT_FAILURE;
+ }
+ window.SetQuitOnClose(true);
+
+ ::MSG msg;
+ while (::GetMessage(&msg, nullptr, 0, 0)) {
+ ::TranslateMessage(&msg);
+ ::DispatchMessage(&msg);
+ }
+
+ ::CoUninitialize();
+ return EXIT_SUCCESS;
+}
diff --git a/commis/app/windows/runner/resource.h b/commis/app/windows/runner/resource.h
new file mode 100644
index 00000000..66a65d1e
--- /dev/null
+++ b/commis/app/windows/runner/resource.h
@@ -0,0 +1,16 @@
+//{{NO_DEPENDENCIES}}
+// Microsoft Visual C++ generated include file.
+// Used by Runner.rc
+//
+#define IDI_APP_ICON 101
+
+// Next default values for new objects
+//
+#ifdef APSTUDIO_INVOKED
+#ifndef APSTUDIO_READONLY_SYMBOLS
+#define _APS_NEXT_RESOURCE_VALUE 102
+#define _APS_NEXT_COMMAND_VALUE 40001
+#define _APS_NEXT_CONTROL_VALUE 1001
+#define _APS_NEXT_SYMED_VALUE 101
+#endif
+#endif
diff --git a/commis/app/windows/runner/resources/app_icon.ico b/commis/app/windows/runner/resources/app_icon.ico
new file mode 100644
index 00000000..c04e20ca
Binary files /dev/null and b/commis/app/windows/runner/resources/app_icon.ico differ
diff --git a/commis/app/windows/runner/runner.exe.manifest b/commis/app/windows/runner/runner.exe.manifest
new file mode 100644
index 00000000..153653e8
--- /dev/null
+++ b/commis/app/windows/runner/runner.exe.manifest
@@ -0,0 +1,14 @@
+
+
+
+
+ PerMonitorV2
+
+
+
+
+
+
+
+
+
diff --git a/commis/app/windows/runner/utils.cpp b/commis/app/windows/runner/utils.cpp
new file mode 100644
index 00000000..3cb71466
--- /dev/null
+++ b/commis/app/windows/runner/utils.cpp
@@ -0,0 +1,69 @@
+#include "utils.h"
+
+#include
+#include
+#include
+#include
+
+#include
+
+void CreateAndAttachConsole() {
+ if (::AllocConsole()) {
+ FILE *unused;
+ if (freopen_s(&unused, "CONOUT$", "w", stdout)) {
+ _dup2(_fileno(stdout), 1);
+ }
+ if (freopen_s(&unused, "CONOUT$", "w", stderr)) {
+ _dup2(_fileno(stdout), 2);
+ }
+ std::ios::sync_with_stdio();
+ FlutterDesktopResyncOutputStreams();
+ }
+}
+
+std::vector GetCommandLineArguments() {
+ // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.
+ int argc;
+ wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
+ if (argv == nullptr) {
+ return std::vector();
+ }
+
+ std::vector command_line_arguments;
+
+ // Skip the first argument as it's the binary name.
+ for (int i = 1; i < argc; i++) {
+ command_line_arguments.push_back(Utf8FromUtf16(argv[i]));
+ }
+
+ ::LocalFree(argv);
+
+ return command_line_arguments;
+}
+
+std::string Utf8FromUtf16(const wchar_t* utf16_string) {
+ if (utf16_string == nullptr) {
+ return std::string();
+ }
+ // First, find the length of the string with a safe upper bound (CWE-126).
+ // UNICODE_STRING_MAX_CHARS (32767) is the maximum length of a UNICODE_STRING.
+ int input_length = static_cast(wcsnlen(utf16_string, UNICODE_STRING_MAX_CHARS));
+ // Now use that bounded length to determine the required buffer size.
+ // When an explicit length is passed, WideCharToMultiByte does not include
+ // the null terminator in its returned size.
+ int target_length = ::WideCharToMultiByte(
+ CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
+ input_length, nullptr, 0, nullptr, nullptr);
+ std::string utf8_string;
+ if (target_length == 0 || static_cast(target_length) > utf8_string.max_size()) {
+ return utf8_string;
+ }
+ utf8_string.resize(target_length);
+ int converted_length = ::WideCharToMultiByte(
+ CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
+ input_length, utf8_string.data(), target_length, nullptr, nullptr);
+ if (converted_length == 0) {
+ return std::string();
+ }
+ return utf8_string;
+}
diff --git a/commis/app/windows/runner/utils.h b/commis/app/windows/runner/utils.h
new file mode 100644
index 00000000..3879d547
--- /dev/null
+++ b/commis/app/windows/runner/utils.h
@@ -0,0 +1,19 @@
+#ifndef RUNNER_UTILS_H_
+#define RUNNER_UTILS_H_
+
+#include
+#include
+
+// Creates a console for the process, and redirects stdout and stderr to
+// it for both the runner and the Flutter library.
+void CreateAndAttachConsole();
+
+// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string
+// encoded in UTF-8. Returns an empty std::string on failure.
+std::string Utf8FromUtf16(const wchar_t* utf16_string);
+
+// Gets the command line arguments passed in as a std::vector,
+// encoded in UTF-8. Returns an empty std::vector on failure.
+std::vector GetCommandLineArguments();
+
+#endif // RUNNER_UTILS_H_
diff --git a/commis/app/windows/runner/win32_window.cpp b/commis/app/windows/runner/win32_window.cpp
new file mode 100644
index 00000000..60608d0f
--- /dev/null
+++ b/commis/app/windows/runner/win32_window.cpp
@@ -0,0 +1,288 @@
+#include "win32_window.h"
+
+#include
+#include
+
+#include "resource.h"
+
+namespace {
+
+/// Window attribute that enables dark mode window decorations.
+///
+/// Redefined in case the developer's machine has a Windows SDK older than
+/// version 10.0.22000.0.
+/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
+#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
+#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
+#endif
+
+constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
+
+/// Registry key for app theme preference.
+///
+/// A value of 0 indicates apps should use dark mode. A non-zero or missing
+/// value indicates apps should use light mode.
+constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
+ L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
+constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";
+
+// The number of Win32Window objects that currently exist.
+static int g_active_window_count = 0;
+
+using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
+
+// Scale helper to convert logical scaler values to physical using passed in
+// scale factor
+int Scale(int source, double scale_factor) {
+ return static_cast(source * scale_factor);
+}
+
+// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
+// This API is only needed for PerMonitor V1 awareness mode.
+void EnableFullDpiSupportIfAvailable(HWND hwnd) {
+ HMODULE user32_module = LoadLibraryA("User32.dll");
+ if (!user32_module) {
+ return;
+ }
+ auto enable_non_client_dpi_scaling =
+ reinterpret_cast(
+ GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
+ if (enable_non_client_dpi_scaling != nullptr) {
+ enable_non_client_dpi_scaling(hwnd);
+ }
+ FreeLibrary(user32_module);
+}
+
+} // namespace
+
+// Manages the Win32Window's window class registration.
+class WindowClassRegistrar {
+ public:
+ ~WindowClassRegistrar() = default;
+
+ // Returns the singleton registrar instance.
+ static WindowClassRegistrar* GetInstance() {
+ if (!instance_) {
+ instance_ = new WindowClassRegistrar();
+ }
+ return instance_;
+ }
+
+ // Returns the name of the window class, registering the class if it hasn't
+ // previously been registered.
+ const wchar_t* GetWindowClass();
+
+ // Unregisters the window class. Should only be called if there are no
+ // instances of the window.
+ void UnregisterWindowClass();
+
+ private:
+ WindowClassRegistrar() = default;
+
+ static WindowClassRegistrar* instance_;
+
+ bool class_registered_ = false;
+};
+
+WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
+
+const wchar_t* WindowClassRegistrar::GetWindowClass() {
+ if (!class_registered_) {
+ WNDCLASS window_class{};
+ window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
+ window_class.lpszClassName = kWindowClassName;
+ window_class.style = CS_HREDRAW | CS_VREDRAW;
+ window_class.cbClsExtra = 0;
+ window_class.cbWndExtra = 0;
+ window_class.hInstance = GetModuleHandle(nullptr);
+ window_class.hIcon =
+ LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
+ window_class.hbrBackground = 0;
+ window_class.lpszMenuName = nullptr;
+ window_class.lpfnWndProc = Win32Window::WndProc;
+ RegisterClass(&window_class);
+ class_registered_ = true;
+ }
+ return kWindowClassName;
+}
+
+void WindowClassRegistrar::UnregisterWindowClass() {
+ UnregisterClass(kWindowClassName, nullptr);
+ class_registered_ = false;
+}
+
+Win32Window::Win32Window() {
+ ++g_active_window_count;
+}
+
+Win32Window::~Win32Window() {
+ --g_active_window_count;
+ Destroy();
+}
+
+bool Win32Window::Create(const std::wstring& title,
+ const Point& origin,
+ const Size& size) {
+ Destroy();
+
+ const wchar_t* window_class =
+ WindowClassRegistrar::GetInstance()->GetWindowClass();
+
+ const POINT target_point = {static_cast(origin.x),
+ static_cast(origin.y)};
+ HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
+ UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
+ double scale_factor = dpi / 96.0;
+
+ HWND window = CreateWindow(
+ window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
+ Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
+ Scale(size.width, scale_factor), Scale(size.height, scale_factor),
+ nullptr, nullptr, GetModuleHandle(nullptr), this);
+
+ if (!window) {
+ return false;
+ }
+
+ UpdateTheme(window);
+
+ return OnCreate();
+}
+
+bool Win32Window::Show() {
+ return ShowWindow(window_handle_, SW_SHOWNORMAL);
+}
+
+// static
+LRESULT CALLBACK Win32Window::WndProc(HWND const window,
+ UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept {
+ if (message == WM_NCCREATE) {
+ auto window_struct = reinterpret_cast(lparam);
+ SetWindowLongPtr(window, GWLP_USERDATA,
+ reinterpret_cast(window_struct->lpCreateParams));
+
+ auto that = static_cast(window_struct->lpCreateParams);
+ EnableFullDpiSupportIfAvailable(window);
+ that->window_handle_ = window;
+ } else if (Win32Window* that = GetThisFromHandle(window)) {
+ return that->MessageHandler(window, message, wparam, lparam);
+ }
+
+ return DefWindowProc(window, message, wparam, lparam);
+}
+
+LRESULT
+Win32Window::MessageHandler(HWND hwnd,
+ UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept {
+ switch (message) {
+ case WM_DESTROY:
+ window_handle_ = nullptr;
+ Destroy();
+ if (quit_on_close_) {
+ PostQuitMessage(0);
+ }
+ return 0;
+
+ case WM_DPICHANGED: {
+ auto newRectSize = reinterpret_cast(lparam);
+ LONG newWidth = newRectSize->right - newRectSize->left;
+ LONG newHeight = newRectSize->bottom - newRectSize->top;
+
+ SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
+ newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
+
+ return 0;
+ }
+ case WM_SIZE: {
+ RECT rect = GetClientArea();
+ if (child_content_ != nullptr) {
+ // Size and position the child window.
+ MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
+ rect.bottom - rect.top, TRUE);
+ }
+ return 0;
+ }
+
+ case WM_ACTIVATE:
+ if (child_content_ != nullptr) {
+ SetFocus(child_content_);
+ }
+ return 0;
+
+ case WM_DWMCOLORIZATIONCOLORCHANGED:
+ UpdateTheme(hwnd);
+ return 0;
+ }
+
+ return DefWindowProc(window_handle_, message, wparam, lparam);
+}
+
+void Win32Window::Destroy() {
+ OnDestroy();
+
+ if (window_handle_) {
+ DestroyWindow(window_handle_);
+ window_handle_ = nullptr;
+ }
+ if (g_active_window_count == 0) {
+ WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
+ }
+}
+
+Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
+ return reinterpret_cast(
+ GetWindowLongPtr(window, GWLP_USERDATA));
+}
+
+void Win32Window::SetChildContent(HWND content) {
+ child_content_ = content;
+ SetParent(content, window_handle_);
+ RECT frame = GetClientArea();
+
+ MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
+ frame.bottom - frame.top, true);
+
+ SetFocus(child_content_);
+}
+
+RECT Win32Window::GetClientArea() {
+ RECT frame;
+ GetClientRect(window_handle_, &frame);
+ return frame;
+}
+
+HWND Win32Window::GetHandle() {
+ return window_handle_;
+}
+
+void Win32Window::SetQuitOnClose(bool quit_on_close) {
+ quit_on_close_ = quit_on_close;
+}
+
+bool Win32Window::OnCreate() {
+ // No-op; provided for subclasses.
+ return true;
+}
+
+void Win32Window::OnDestroy() {
+ // No-op; provided for subclasses.
+}
+
+void Win32Window::UpdateTheme(HWND const window) {
+ DWORD light_mode;
+ DWORD light_mode_size = sizeof(light_mode);
+ LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,
+ kGetPreferredBrightnessRegValue,
+ RRF_RT_REG_DWORD, nullptr, &light_mode,
+ &light_mode_size);
+
+ if (result == ERROR_SUCCESS) {
+ BOOL enable_dark_mode = light_mode == 0;
+ DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,
+ &enable_dark_mode, sizeof(enable_dark_mode));
+ }
+}
diff --git a/commis/app/windows/runner/win32_window.h b/commis/app/windows/runner/win32_window.h
new file mode 100644
index 00000000..e901dde6
--- /dev/null
+++ b/commis/app/windows/runner/win32_window.h
@@ -0,0 +1,102 @@
+#ifndef RUNNER_WIN32_WINDOW_H_
+#define RUNNER_WIN32_WINDOW_H_
+
+#include
+
+#include
+#include
+#include
+
+// A class abstraction for a high DPI-aware Win32 Window. Intended to be
+// inherited from by classes that wish to specialize with custom
+// rendering and input handling
+class Win32Window {
+ public:
+ struct Point {
+ unsigned int x;
+ unsigned int y;
+ Point(unsigned int x, unsigned int y) : x(x), y(y) {}
+ };
+
+ struct Size {
+ unsigned int width;
+ unsigned int height;
+ Size(unsigned int width, unsigned int height)
+ : width(width), height(height) {}
+ };
+
+ Win32Window();
+ virtual ~Win32Window();
+
+ // Creates a win32 window with |title| that is positioned and sized using
+ // |origin| and |size|. New windows are created on the default monitor. Window
+ // sizes are specified to the OS in physical pixels, hence to ensure a
+ // consistent size this function will scale the inputted width and height as
+ // as appropriate for the default monitor. The window is invisible until
+ // |Show| is called. Returns true if the window was created successfully.
+ bool Create(const std::wstring& title, const Point& origin, const Size& size);
+
+ // Show the current window. Returns true if the window was successfully shown.
+ bool Show();
+
+ // Release OS resources associated with window.
+ void Destroy();
+
+ // Inserts |content| into the window tree.
+ void SetChildContent(HWND content);
+
+ // Returns the backing Window handle to enable clients to set icon and other
+ // window properties. Returns nullptr if the window has been destroyed.
+ HWND GetHandle();
+
+ // If true, closing this window will quit the application.
+ void SetQuitOnClose(bool quit_on_close);
+
+ // Return a RECT representing the bounds of the current client area.
+ RECT GetClientArea();
+
+ protected:
+ // Processes and route salient window messages for mouse handling,
+ // size change and DPI. Delegates handling of these to member overloads that
+ // inheriting classes can handle.
+ virtual LRESULT MessageHandler(HWND window,
+ UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept;
+
+ // Called when CreateAndShow is called, allowing subclass window-related
+ // setup. Subclasses should return false if setup fails.
+ virtual bool OnCreate();
+
+ // Called when Destroy is called.
+ virtual void OnDestroy();
+
+ private:
+ friend class WindowClassRegistrar;
+
+ // OS callback called by message pump. Handles the WM_NCCREATE message which
+ // is passed when the non-client area is being created and enables automatic
+ // non-client DPI scaling so that the non-client area automatically
+ // responds to changes in DPI. All other messages are handled by
+ // MessageHandler.
+ static LRESULT CALLBACK WndProc(HWND const window,
+ UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept;
+
+ // Retrieves a class instance pointer for |window|
+ static Win32Window* GetThisFromHandle(HWND const window) noexcept;
+
+ // Update the window frame's theme to match the system theme.
+ static void UpdateTheme(HWND const window);
+
+ bool quit_on_close_ = false;
+
+ // window handle for top level window.
+ HWND window_handle_ = nullptr;
+
+ // window handle for hosted content.
+ HWND child_content_ = nullptr;
+};
+
+#endif // RUNNER_WIN32_WINDOW_H_
diff --git a/commis/firebase.json b/commis/firebase.json
new file mode 100644
index 00000000..3634be77
--- /dev/null
+++ b/commis/firebase.json
@@ -0,0 +1,17 @@
+{
+ "functions": {
+ "source": "functions",
+ "codebase": "default"
+ },
+ "emulators": {
+ "functions": {
+ "port": 5001
+ },
+ "firestore": {
+ "port": 8080
+ },
+ "ui": {
+ "enabled": true
+ }
+ }
+}
diff --git a/commis/functions/.gitignore b/commis/functions/.gitignore
new file mode 100644
index 00000000..d9cb518b
--- /dev/null
+++ b/commis/functions/.gitignore
@@ -0,0 +1,4 @@
+## Firebase logs
+*-debug.log
+.env.local
+.dart_tool
diff --git a/commis/functions/README.md b/commis/functions/README.md
new file mode 100644
index 00000000..1b52acba
--- /dev/null
+++ b/commis/functions/README.md
@@ -0,0 +1,48 @@
+# Commis Catering Assistant - Cloud Functions
+
+This directory contains the serverless backend code for the Commis Catering Assistant, written in Dart using the experimental `firebase_functions` Dart package.
+
+## Features
+- **Firestore Document Triggers**: Listens to writes on `jobs/{jobId}` documents to run the planning pipeline.
+- **AI Content Generator**: Communicates with Gemini to analyze job changes and generate responsive A2UI JSON payloads cached in Firestore.
+- **Database Seeding Utility**: Pre-populates the database with default ingredients, recipes, and jobs for testing.
+
+---
+
+## Getting Started
+
+### Prerequisites
+- [Firebase CLI](https://firebase.google.com/docs/cli) (v15.15.0 or higher)
+- [Dart SDK](https://dart.dev/get-started) (aligned with the main app project)
+
+Before running the emulators or deploying, ensure the Dart functions experiment is enabled on your machine:
+```bash
+firebase experiments:enable dartfunctions
+```
+
+### Seeding the Emulator Database
+When starting development with a clean Firestore emulator, you can seed the database with mock catering data using the scripts provided in `bin/`:
+
+1. Set the Firestore emulator environment variable in your terminal:
+ ```bash
+ export FIRESTORE_EMULATOR_HOST=localhost:8080
+ ```
+2. Run the seeding commands:
+ ```bash
+ dart run bin/seed_ingredients.dart
+ dart run bin/seed_recipes.dart
+ dart run bin/seed_jobs.dart
+ ```
+
+---
+
+## Configuration
+
+### Gemini API Key
+The functions expect a `GEMINI_API_KEY` parameter to access the Gemini API.
+
+- **Local Development**: Create a `.env.local` file inside the `functions/` directory:
+ ```env
+ GEMINI_API_KEY=YOUR_GEMINI_API_KEY
+ ```
+- **Production**: Configure the parameter in Firebase Secrets or Parameters config.
diff --git a/commis/functions/analysis_options.yaml b/commis/functions/analysis_options.yaml
new file mode 100644
index 00000000..2a5ade29
--- /dev/null
+++ b/commis/functions/analysis_options.yaml
@@ -0,0 +1,9 @@
+include: package:lints/recommended.yaml
+
+analyzer:
+ exclude:
+ - build/**
+
+linter:
+ rules:
+ directives_ordering: true
diff --git a/commis/functions/bin/seed_ingredients.dart b/commis/functions/bin/seed_ingredients.dart
new file mode 100644
index 00000000..0d7ca270
--- /dev/null
+++ b/commis/functions/bin/seed_ingredients.dart
@@ -0,0 +1,679 @@
+// Copyright 2026 The Flutter Authors.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:io';
+import 'package:firebase_admin_sdk/firebase_admin_sdk.dart';
+
+void main() async {
+ // Initialize the Firebase Admin App
+ final String projectId =
+ Platform.environment['GOOGLE_CLOUD_PROJECT'] ?? 'YOUR_PROJECT_ID';
+ final adminApp = FirebaseApp.initializeApp(
+ options: AppOptions(
+ projectId: projectId,
+ ),
+ );
+ final firestore = adminApp.firestore();
+
+ final List