diff --git a/coverage_baseline.yaml b/coverage_baseline.yaml index 7bc26ea10..3c4bcc367 100644 --- a/coverage_baseline.yaml +++ b/coverage_baseline.yaml @@ -5,12 +5,11 @@ # Monorepo Coverage High-Water Marks (Auto-generated by test_and_fix --update-baseline) dev_tools/catalog_gallery: 52.91 dev_tools/composer: 20.49 +examples/express_chat: 35.76 +examples/simple_chat: 37.33 packages/a2ui_core: 76.30 packages/genai_primitives: 100.00 -packages/genui: 79.71 +packages/genui: 79.72 packages/genui_a2a: 91.37 -packages/json_schema_builder: 79.09 -tool/e2e: 100.00 -tool/fix_copyright: 89.83 -tool/release: 78.01 -tool/test_and_fix: 94.37 +packages/genui_express: 63.34 +packages/json_schema_builder: 79.27 diff --git a/coverage_policy.yaml b/coverage_policy.yaml index 714d300ed..83c66fb3a 100644 --- a/coverage_policy.yaml +++ b/coverage_policy.yaml @@ -18,11 +18,14 @@ exclude: - "**/*.freezed.dart" - "**/*.mocks.dart" - "**/generated/**" + - "**/*_web.dart" # Package-specific overrides (paths relative to monorepo root). packages: packages/genui: threshold: 75.0 + packages/genui_express: + threshold: 60.0 packages/a2ui_core: threshold: 75.0 packages/json_schema_builder: @@ -36,6 +39,8 @@ packages: tool/release: threshold: 75.0 examples/simple_chat: - enabled: false + threshold: 30.0 + examples/express_chat: + threshold: 30.0 examples/verdure/client: enabled: false diff --git a/dev_tools/catalog_gallery/linux/flutter/generated_plugins.cmake b/dev_tools/catalog_gallery/linux/flutter/generated_plugins.cmake index 8e2a1900c..a2eef970f 100644 --- a/dev_tools/catalog_gallery/linux/flutter/generated_plugins.cmake +++ b/dev_tools/catalog_gallery/linux/flutter/generated_plugins.cmake @@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/dev_tools/catalog_gallery/macos/Flutter/GeneratedPluginRegistrant.swift b/dev_tools/catalog_gallery/macos/Flutter/GeneratedPluginRegistrant.swift index ad1073ab5..7b265476e 100644 --- a/dev_tools/catalog_gallery/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/dev_tools/catalog_gallery/macos/Flutter/GeneratedPluginRegistrant.swift @@ -12,5 +12,5 @@ import video_player_avfoundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) - FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) + VideoPlayerPlugin.register(with: registry.registrar(forPlugin: "VideoPlayerPlugin")) } diff --git a/dev_tools/composer/linux/flutter/generated_plugins.cmake b/dev_tools/composer/linux/flutter/generated_plugins.cmake index c085ca836..a1bc1781f 100644 --- a/dev_tools/composer/linux/flutter/generated_plugins.cmake +++ b/dev_tools/composer/linux/flutter/generated_plugins.cmake @@ -10,6 +10,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/dev_tools/composer/macos/Flutter/GeneratedPluginRegistrant.swift b/dev_tools/composer/macos/Flutter/GeneratedPluginRegistrant.swift index 802a33c8d..4825c9db2 100644 --- a/dev_tools/composer/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/dev_tools/composer/macos/Flutter/GeneratedPluginRegistrant.swift @@ -15,6 +15,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) - FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) + VideoPlayerPlugin.register(with: registry.registrar(forPlugin: "VideoPlayerPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) } diff --git a/dev_tools/composer/windows/flutter/generated_plugins.cmake b/dev_tools/composer/windows/flutter/generated_plugins.cmake index f74fccabd..79fabcfb8 100644 --- a/dev_tools/composer/windows/flutter/generated_plugins.cmake +++ b/dev_tools/composer/windows/flutter/generated_plugins.cmake @@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/examples/express_chat/.metadata b/examples/express_chat/.metadata new file mode 100644 index 000000000..5492cf186 --- /dev/null +++ b/examples/express_chat/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "adc901062556672b4138e18a4dc62a4be8f4b3c2" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + - platform: linux + create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + + # 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/examples/express_chat/README.md b/examples/express_chat/README.md new file mode 100644 index 000000000..152846d85 --- /dev/null +++ b/examples/express_chat/README.md @@ -0,0 +1,43 @@ +# Simple Chat Example + +This application is a minimal example of how to use the `genui` package to create a simple, conversational chat application. + +## Purpose + +The main goal of this example is to demonstrate the fundamental concepts of `genui` in a straightforward chat context. It shows how to: +1. Initialize and use the `SurfaceController`, the core engine for the package. +2. Provide a simple system prompt to guide the AI's behavior. +3. Send user messages to the AI and receive responses. +4. Handle the creation of new UI "surfaces" generated by the AI. +5. Render these dynamic UI surfaces within a standard chat message list. +6. Manage a conversation history that interleaves user text messages with AI-generated UI responses. + +Unlike more complex examples, this app does not define a custom widget catalog. Instead, it relies on the default `coreCatalog` provided by `genui`, meaning the AI can only respond with basic widgets like `Text`, `Column`, `ElevatedButton`, etc. + +## How it Works + +The application's logic is contained almost entirely within `lib/chat_session.dart`. + +1. **Initialization**: A `SurfaceController` is created to manage the state of UI surfaces. +2. **User Input**: The user types a message into a `TextField` and hits send. +3. **Sending the Message**: + - The user's text is immediately added to the local message list. + - The request is sent to the `AiClient`. +4. **AI Response**: + - The `AiClient` streams `A2uiMessage`s back. + - These messages are piped into the `SurfaceController`. +5. **UI Rendering**: + - The UI listens to `SurfaceController.surfaceUpdates` or `A2uiTransportAdapter` streams. + - When a surface is added, a `Surface` widget is rendered, dynamically building the UI based on the `UiDefinition` managed by `SurfaceController`. + +## Getting Started + +Follow the instructions in [Running the app with a Gemini key](../../docs/usage/run_app_with_gemini_key.md). + +## Video + +https://github.com/user-attachments/assets/469fb2cf-09cf-463c-8c9c-b9c0cb39203b + +This video is recorded on May 11, 2026, for [PR#905](https://github.com/flutter/genui/pull/905). + +Please update the link if you have a fresher version of the demo. diff --git a/examples/express_chat/android/.gitignore b/examples/express_chat/android/.gitignore new file mode 100644 index 000000000..be3943c96 --- /dev/null +++ b/examples/express_chat/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/examples/express_chat/android/app/build.gradle.kts b/examples/express_chat/android/app/build.gradle.kts new file mode 100644 index 000000000..244285367 --- /dev/null +++ b/examples/express_chat/android/app/build.gradle.kts @@ -0,0 +1,47 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") + // START: FlutterFire Configuration + id("com.google.gms.google-services") + // END: FlutterFire Configuration +} + +android { + namespace = "com.example.simple_chat" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.simple_chat" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/examples/express_chat/android/app/src/debug/AndroidManifest.xml b/examples/express_chat/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000..a90346878 --- /dev/null +++ b/examples/express_chat/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/examples/express_chat/android/app/src/main/AndroidManifest.xml b/examples/express_chat/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..bb1441467 --- /dev/null +++ b/examples/express_chat/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/express_chat/android/app/src/main/kotlin/com/example/simple_chat/MainActivity.kt b/examples/express_chat/android/app/src/main/kotlin/com/example/simple_chat/MainActivity.kt new file mode 100644 index 000000000..89498754b --- /dev/null +++ b/examples/express_chat/android/app/src/main/kotlin/com/example/simple_chat/MainActivity.kt @@ -0,0 +1,9 @@ +// 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. + +package com.example.simple_chat + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/examples/express_chat/android/app/src/main/res/drawable-v21/launch_background.xml b/examples/express_chat/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 000000000..3827ad38b --- /dev/null +++ b/examples/express_chat/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/examples/express_chat/android/app/src/main/res/drawable/launch_background.xml b/examples/express_chat/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 000000000..6e3711468 --- /dev/null +++ b/examples/express_chat/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/examples/express_chat/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/examples/express_chat/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..db77bb4b7 Binary files /dev/null and b/examples/express_chat/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/examples/express_chat/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/examples/express_chat/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..17987b79b Binary files /dev/null and b/examples/express_chat/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/examples/express_chat/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/examples/express_chat/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..09d439148 Binary files /dev/null and b/examples/express_chat/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/examples/express_chat/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/examples/express_chat/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..d5f1c8d34 Binary files /dev/null and b/examples/express_chat/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/examples/express_chat/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/examples/express_chat/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..4d6372eeb Binary files /dev/null and b/examples/express_chat/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/examples/express_chat/android/app/src/main/res/values-night/styles.xml b/examples/express_chat/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 000000000..deb01c1c9 --- /dev/null +++ b/examples/express_chat/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/examples/express_chat/android/app/src/main/res/values/styles.xml b/examples/express_chat/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000..45f156a54 --- /dev/null +++ b/examples/express_chat/android/app/src/main/res/values/styles.xml @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/examples/express_chat/android/app/src/profile/AndroidManifest.xml b/examples/express_chat/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 000000000..a90346878 --- /dev/null +++ b/examples/express_chat/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/examples/express_chat/android/build.gradle.kts b/examples/express_chat/android/build.gradle.kts new file mode 100644 index 000000000..dbee657bb --- /dev/null +++ b/examples/express_chat/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/examples/express_chat/android/gradle.properties b/examples/express_chat/android/gradle.properties new file mode 100644 index 000000000..f018a6181 --- /dev/null +++ b/examples/express_chat/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/examples/express_chat/android/gradle/wrapper/gradle-wrapper.properties b/examples/express_chat/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..e4ef43fb9 --- /dev/null +++ b/examples/express_chat/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-8.14-all.zip diff --git a/examples/express_chat/android/settings.gradle.kts b/examples/express_chat/android/settings.gradle.kts new file mode 100644 index 000000000..7712fad2d --- /dev/null +++ b/examples/express_chat/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 "8.9.1" apply false + id("org.jetbrains.kotlin.android") version "2.1.0" apply false + // START: FlutterFire Configuration + id("com.google.gms.google-services") version("4.3.15") apply false + // END: FlutterFire Configuration +} + +include(":app") diff --git a/examples/express_chat/assets/climbing/10x2500x1667.jpg b/examples/express_chat/assets/climbing/10x2500x1667.jpg new file mode 100644 index 000000000..46f6946d1 Binary files /dev/null and b/examples/express_chat/assets/climbing/10x2500x1667.jpg differ diff --git a/examples/express_chat/assets/climbing/121x1600x1067.jpg b/examples/express_chat/assets/climbing/121x1600x1067.jpg new file mode 100644 index 000000000..103e03552 Binary files /dev/null and b/examples/express_chat/assets/climbing/121x1600x1067.jpg differ diff --git a/examples/express_chat/assets/climbing/128x3823x2549.jpg b/examples/express_chat/assets/climbing/128x3823x2549.jpg new file mode 100644 index 000000000..b2983ca7b Binary files /dev/null and b/examples/express_chat/assets/climbing/128x3823x2549.jpg differ diff --git a/examples/express_chat/assets/climbing/134x4928x3264.jpg b/examples/express_chat/assets/climbing/134x4928x3264.jpg new file mode 100644 index 000000000..b645e5ced Binary files /dev/null and b/examples/express_chat/assets/climbing/134x4928x3264.jpg differ diff --git a/examples/express_chat/assets/climbing/136x4032x2272.jpg b/examples/express_chat/assets/climbing/136x4032x2272.jpg new file mode 100644 index 000000000..5a43e125f Binary files /dev/null and b/examples/express_chat/assets/climbing/136x4032x2272.jpg differ diff --git a/examples/express_chat/assets/climbing/15x2500x1667.jpg b/examples/express_chat/assets/climbing/15x2500x1667.jpg new file mode 100644 index 000000000..f87e06869 Binary files /dev/null and b/examples/express_chat/assets/climbing/15x2500x1667.jpg differ diff --git a/examples/express_chat/assets/climbing/166x1280x720.jpg b/examples/express_chat/assets/climbing/166x1280x720.jpg new file mode 100644 index 000000000..36f3eb64f Binary files /dev/null and b/examples/express_chat/assets/climbing/166x1280x720.jpg differ diff --git a/examples/express_chat/assets/climbing/177x2515x1830.jpg b/examples/express_chat/assets/climbing/177x2515x1830.jpg new file mode 100644 index 000000000..511fc06cb Binary files /dev/null and b/examples/express_chat/assets/climbing/177x2515x1830.jpg differ diff --git a/examples/express_chat/assets/climbing/184x4288x2848.jpg b/examples/express_chat/assets/climbing/184x4288x2848.jpg new file mode 100644 index 000000000..70182daba Binary files /dev/null and b/examples/express_chat/assets/climbing/184x4288x2848.jpg differ diff --git a/examples/express_chat/assets/climbing/191x2560x1707.jpg b/examples/express_chat/assets/climbing/191x2560x1707.jpg new file mode 100644 index 000000000..c4b43f633 Binary files /dev/null and b/examples/express_chat/assets/climbing/191x2560x1707.jpg differ diff --git a/examples/express_chat/assets/climbing/231x4088x2715.jpg b/examples/express_chat/assets/climbing/231x4088x2715.jpg new file mode 100644 index 000000000..5de42021c Binary files /dev/null and b/examples/express_chat/assets/climbing/231x4088x2715.jpg differ diff --git a/examples/express_chat/assets/climbing/235x5000x3333.jpg b/examples/express_chat/assets/climbing/235x5000x3333.jpg new file mode 100644 index 000000000..0d6fb7fef Binary files /dev/null and b/examples/express_chat/assets/climbing/235x5000x3333.jpg differ diff --git a/examples/express_chat/assets/climbing/243x2300x1533.jpg b/examples/express_chat/assets/climbing/243x2300x1533.jpg new file mode 100644 index 000000000..27b1fae73 Binary files /dev/null and b/examples/express_chat/assets/climbing/243x2300x1533.jpg differ diff --git a/examples/express_chat/assets/climbing/247x3264x2168.jpg b/examples/express_chat/assets/climbing/247x3264x2168.jpg new file mode 100644 index 000000000..a06f61ba3 Binary files /dev/null and b/examples/express_chat/assets/climbing/247x3264x2168.jpg differ diff --git a/examples/express_chat/assets/climbing/287x4288x2848.jpg b/examples/express_chat/assets/climbing/287x4288x2848.jpg new file mode 100644 index 000000000..0f065f29c Binary files /dev/null and b/examples/express_chat/assets/climbing/287x4288x2848.jpg differ diff --git a/examples/express_chat/assets/climbing/28x4928x3264.jpg b/examples/express_chat/assets/climbing/28x4928x3264.jpg new file mode 100644 index 000000000..ba10a2852 Binary files /dev/null and b/examples/express_chat/assets/climbing/28x4928x3264.jpg differ diff --git a/examples/express_chat/assets/climbing/296x3072x2048.jpg b/examples/express_chat/assets/climbing/296x3072x2048.jpg new file mode 100644 index 000000000..15a8303b8 Binary files /dev/null and b/examples/express_chat/assets/climbing/296x3072x2048.jpg differ diff --git a/examples/express_chat/assets/climbing/29x4000x2670.jpg b/examples/express_chat/assets/climbing/29x4000x2670.jpg new file mode 100644 index 000000000..96b8ace76 Binary files /dev/null and b/examples/express_chat/assets/climbing/29x4000x2670.jpg differ diff --git a/examples/express_chat/assets/climbing/315x2100x1500.jpg b/examples/express_chat/assets/climbing/315x2100x1500.jpg new file mode 100644 index 000000000..e4aa9cfa3 Binary files /dev/null and b/examples/express_chat/assets/climbing/315x2100x1500.jpg differ diff --git a/examples/express_chat/assets/climbing/343x2304x1536.jpg b/examples/express_chat/assets/climbing/343x2304x1536.jpg new file mode 100644 index 000000000..825aa5081 Binary files /dev/null and b/examples/express_chat/assets/climbing/343x2304x1536.jpg differ diff --git a/examples/express_chat/assets/climbing/368x4896x3264.jpg b/examples/express_chat/assets/climbing/368x4896x3264.jpg new file mode 100644 index 000000000..41f022786 Binary files /dev/null and b/examples/express_chat/assets/climbing/368x4896x3264.jpg differ diff --git a/examples/express_chat/assets/climbing/377x4884x3256.jpg b/examples/express_chat/assets/climbing/377x4884x3256.jpg new file mode 100644 index 000000000..9a3c64ff7 Binary files /dev/null and b/examples/express_chat/assets/climbing/377x4884x3256.jpg differ diff --git a/examples/express_chat/assets/climbing/450x4288x2848.jpg b/examples/express_chat/assets/climbing/450x4288x2848.jpg new file mode 100644 index 000000000..30c8971f7 Binary files /dev/null and b/examples/express_chat/assets/climbing/450x4288x2848.jpg differ diff --git a/examples/express_chat/assets/climbing/46x3264x2448.jpg b/examples/express_chat/assets/climbing/46x3264x2448.jpg new file mode 100644 index 000000000..c6a4f2975 Binary files /dev/null and b/examples/express_chat/assets/climbing/46x3264x2448.jpg differ diff --git a/examples/express_chat/assets/climbing/472x5000x3333.jpg b/examples/express_chat/assets/climbing/472x5000x3333.jpg new file mode 100644 index 000000000..4a31d8112 Binary files /dev/null and b/examples/express_chat/assets/climbing/472x5000x3333.jpg differ diff --git a/examples/express_chat/assets/climbing/475x4288x2848.jpg b/examples/express_chat/assets/climbing/475x4288x2848.jpg new file mode 100644 index 000000000..ac707f630 Binary files /dev/null and b/examples/express_chat/assets/climbing/475x4288x2848.jpg differ diff --git a/examples/express_chat/assets/climbing/485x4084x2713.jpg b/examples/express_chat/assets/climbing/485x4084x2713.jpg new file mode 100644 index 000000000..2d512b1a6 Binary files /dev/null and b/examples/express_chat/assets/climbing/485x4084x2713.jpg differ diff --git a/examples/express_chat/assets/climbing/62x2000x1333.jpg b/examples/express_chat/assets/climbing/62x2000x1333.jpg new file mode 100644 index 000000000..0837b0867 Binary files /dev/null and b/examples/express_chat/assets/climbing/62x2000x1333.jpg differ diff --git a/examples/express_chat/assets/climbing/66x3264x2448.jpg b/examples/express_chat/assets/climbing/66x3264x2448.jpg new file mode 100644 index 000000000..3bd8c5cea Binary files /dev/null and b/examples/express_chat/assets/climbing/66x3264x2448.jpg differ diff --git a/examples/express_chat/assets/climbing/67x2848x4288.jpg b/examples/express_chat/assets/climbing/67x2848x4288.jpg new file mode 100644 index 000000000..65d12fc4e Binary files /dev/null and b/examples/express_chat/assets/climbing/67x2848x4288.jpg differ diff --git a/examples/express_chat/assets/climbing/79x2000x3011.jpg b/examples/express_chat/assets/climbing/79x2000x3011.jpg new file mode 100644 index 000000000..2f142b541 Binary files /dev/null and b/examples/express_chat/assets/climbing/79x2000x3011.jpg differ diff --git a/examples/express_chat/integration_test/README.md b/examples/express_chat/integration_test/README.md new file mode 100644 index 000000000..dbc27d6a5 --- /dev/null +++ b/examples/express_chat/integration_test/README.md @@ -0,0 +1,14 @@ +# Integration tests + +Renders canned A2UI samples through `ChatScreen` with a `FakeAiClient` — no API key. + +From `examples/express_chat`: + +```bash +flutter pub get +flutter test integration_test/app_test.dart -d macos +``` + +Swap `macos` for any device from `flutter devices`. `flutter pub get` is +required first; without it you'll see a misleading `'../pubspec.yaml'` error +from the pub-workspace lookup. diff --git a/examples/express_chat/integration_test/app_test.dart b/examples/express_chat/integration_test/app_test.dart new file mode 100644 index 000000000..299114d04 --- /dev/null +++ b/examples/express_chat/integration_test/app_test.dart @@ -0,0 +1,141 @@ +// 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 'dart:io'; + +import 'package:express_chat/main.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:genkit/genkit.dart' as genkit; +import 'package:integration_test/integration_test.dart'; +import 'package:logging/logging.dart'; +import 'package:network_image_mock/network_image_mock.dart'; + +// Import from ../test via relative path since it is not in lib +import '../test/fake_ai_client.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + // Configure logging + Logger.root.level = Level.ALL; + Logger.root.onRecord.listen((record) { + debugPrint('${record.level.name}: ${record.time}: ${record.message}'); + }); + + group('Simple Chat Integration Tests', () { + testWidgets('render hello world sample', (tester) async { + await mockNetworkImagesFor(() async { + await _runTestForSample( + tester, + 'integration_test/samples/sample_1_hello.json', + (tester, client) async { + expect(find.textContaining('Hello, World!'), findsOneWidget); + }, + ); + }); + }); + + testWidgets('render button sample', (tester) async { + await mockNetworkImagesFor(() async { + await _runTestForSample( + tester, + 'integration_test/samples/sample_2_button.json', + (tester, client) async { + // Button might be ElevatedButton, TextButton, or FilledButton. + // Just finding text is safer for integration test unless we care + // about specific styling. + expect(find.text('Click Me'), findsOneWidget); + + // Interaction Verification + await tester.tap(find.text('Click Me')); + await tester.pump(); + // Button action does not trigger a response if the fake client is + // empty, but it should send the prompt. + expect( + client.receivedPrompts, + contains(contains('Button Clicked')), + ); + }, + ); + }); + }); + + testWidgets('render form sample', (tester) async { + await mockNetworkImagesFor(() async { + await _runTestForSample( + tester, + 'integration_test/samples/sample_4_form.json', + (tester, client) async { + // Debug dump if fails + expect(find.text('Type'), findsOneWidget); + expect(find.text('Size'), findsOneWidget); + expect(find.text('Submit Filters'), findsOneWidget); + }, + ); + }); + }); + + testWidgets('render mixed sample', (tester) async { + await mockNetworkImagesFor(() async { + await _runTestForSample( + tester, + 'integration_test/samples/sample_5_mixed.json', + (tester, client) async { + expect(find.text('Do you want to proceed?'), findsOneWidget); + expect(find.text('Yes, proceed'), findsOneWidget); + }, + ); + }); + }); + }); +} + +Future _runTestForSample( + WidgetTester tester, + String samplePath, + Future Function(WidgetTester, FakeGenkitClient) verify, +) async { + // Read sample file + final file = File(samplePath); + if (!file.existsSync()) { + fail('Sample file not found: $samplePath'); + } + final String jsonString = await file.readAsString(); + + // Initialize FakeGenkitClient + final fakeClient = FakeGenkitClient(); + + // Queue the response + // SurfaceController expects A2UI messages to be wrapped in markdown code + // blocks or detectable as structured content. Standard LLM behavior using + // GenUi is to return ```json ... ``` blocks. + fakeClient.addResponse('Here is the UI:\n```json\n$jsonString\n```'); + + await tester.pumpWidget( + MaterialApp( + home: ChatScreen( + ai: fakeClient.ai, + model: genkit.modelRef('local/fake-model'), + ), + ), + ); + + // Trigger a message to start the flow + await tester.enterText(find.byType(TextField), 'Test Trigger'); + await tester.tap(find.byIcon(Icons.send)); + await tester.pump(); // Start processing + + // Wait for response and rendering. + // The FakeAiClient splits it into chunks with delays. + // We can't use pumpAndSettle() because some catalog widgets (e.g. Image's + // loadingBuilder shows a CircularProgressIndicator) have indeterminate + // animations that is not handled by pumpAndSettle. + for (var i = 0; i < 30; i++) { + await tester.pump(const Duration(milliseconds: 100)); + } + + // Run verification + await verify(tester, fakeClient); +} diff --git a/examples/express_chat/integration_test/samples/sample_1_hello.json b/examples/express_chat/integration_test/samples/sample_1_hello.json new file mode 100644 index 000000000..68168ce1d --- /dev/null +++ b/examples/express_chat/integration_test/samples/sample_1_hello.json @@ -0,0 +1,22 @@ +[ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "sample_1_hello", + "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json" + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "sample_1_hello", + "components": [ + { + "id": "root", + "component": "Text", + "text": "Hello, World!" + } + ] + } + } +] diff --git a/examples/express_chat/integration_test/samples/sample_2_button.json b/examples/express_chat/integration_test/samples/sample_2_button.json new file mode 100644 index 000000000..c0b0b01cd --- /dev/null +++ b/examples/express_chat/integration_test/samples/sample_2_button.json @@ -0,0 +1,40 @@ +[ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "sample_2_button", + "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json" + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "sample_2_button", + "components": [ + { + "id": "root", + "component": "Column", + "children": ["the_button"] + }, + { + "id": "the_button", + "component": "Button", + "child": "btn_label", + "action": { + "event": { + "name": "submit", + "context": { + "value": "Button Clicked" + } + } + } + }, + { + "id": "btn_label", + "component": "Text", + "text": "Click Me" + } + ] + } + } +] diff --git a/examples/express_chat/integration_test/samples/sample_4_form.json b/examples/express_chat/integration_test/samples/sample_4_form.json new file mode 100644 index 000000000..f4687de47 --- /dev/null +++ b/examples/express_chat/integration_test/samples/sample_4_form.json @@ -0,0 +1,49 @@ +[ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "sample_4_form", + "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json" + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "sample_4_form", + "components": [ + { + "id": "root", + "component": "Column", + "children": ["field_type", "field_size", "submit_btn"] + }, + { + "id": "field_type", + "component": "TextField", + "label": "Type", + "text": "" + }, + { + "id": "field_size", + "component": "TextField", + "label": "Size", + "text": "" + }, + { + "id": "submit_btn", + "component": "Button", + "child": "btn_text", + "action": { + "event": { + "name": "submit_form" + } + } + }, + { + "id": "btn_text", + "component": "Text", + "text": "Submit Filters" + } + ] + } + } +] diff --git a/examples/express_chat/integration_test/samples/sample_5_mixed.json b/examples/express_chat/integration_test/samples/sample_5_mixed.json new file mode 100644 index 000000000..39fc6eb79 --- /dev/null +++ b/examples/express_chat/integration_test/samples/sample_5_mixed.json @@ -0,0 +1,54 @@ +[ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "sample_5_mixed", + "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json" + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "sample_5_mixed", + "components": [ + { + "id": "root", + "component": "Column", + "children": ["question", "btn_yes", "btn_no"] + }, + { + "id": "question", + "component": "Text", + "text": "Do you want to proceed?" + }, + { + "id": "btn_yes", + "component": "Button", + "child": "yes_text", + "action": { + "event": { "name": "proceed_yes" } + } + }, + { + "id": "yes_text", + "component": "Text", + "text": "Yes, proceed" + }, + { + "id": "btn_no", + "component": "Button", + "child": "no_text", + "variant": "borderless", + "action": { + "event": { "name": "proceed_no" } + } + }, + { + "id": "no_text", + "component": "Text", + "text": "No, cancel" + } + ] + } + } +] diff --git a/examples/express_chat/ios/.gitignore b/examples/express_chat/ios/.gitignore new file mode 100644 index 000000000..b3afe3f58 --- /dev/null +++ b/examples/express_chat/ios/.gitignore @@ -0,0 +1,28 @@ +**/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.* diff --git a/examples/express_chat/ios/Flutter/AppFrameworkInfo.plist b/examples/express_chat/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000..0d1408009 --- /dev/null +++ b/examples/express_chat/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 15.0 + + diff --git a/examples/express_chat/ios/Flutter/Debug.xcconfig b/examples/express_chat/ios/Flutter/Debug.xcconfig new file mode 100644 index 000000000..ec97fc6f3 --- /dev/null +++ b/examples/express_chat/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/examples/express_chat/ios/Flutter/Release.xcconfig b/examples/express_chat/ios/Flutter/Release.xcconfig new file mode 100644 index 000000000..c4855bfe2 --- /dev/null +++ b/examples/express_chat/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/examples/express_chat/ios/Podfile b/examples/express_chat/ios/Podfile new file mode 100644 index 000000000..6649374d4 --- /dev/null +++ b/examples/express_chat/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '15.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/examples/express_chat/ios/Podfile.lock b/examples/express_chat/ios/Podfile.lock new file mode 100644 index 000000000..59bb23525 --- /dev/null +++ b/examples/express_chat/ios/Podfile.lock @@ -0,0 +1,130 @@ +PODS: + - AppCheckCore (11.2.0): + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - PromisesObjC (~> 2.4) + - Firebase/Auth (12.4.0): + - Firebase/CoreOnly + - FirebaseAuth (~> 12.4.0) + - Firebase/CoreOnly (12.4.0): + - FirebaseCore (~> 12.4.0) + - firebase_app_check (0.4.1-2): + - Firebase/CoreOnly (~> 12.4.0) + - firebase_core + - FirebaseAppCheck (~> 12.4.0) + - Flutter + - firebase_auth (6.1.2): + - Firebase/Auth (= 12.4.0) + - firebase_core + - Flutter + - firebase_core (4.2.1): + - Firebase/CoreOnly (= 12.4.0) + - Flutter + - FirebaseAppCheck (12.4.0): + - AppCheckCore (~> 11.0) + - FirebaseAppCheckInterop (~> 12.4.0) + - FirebaseCore (~> 12.4.0) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/UserDefaults (~> 8.1) + - FirebaseAppCheckInterop (12.4.0) + - FirebaseAuth (12.4.0): + - FirebaseAppCheckInterop (~> 12.4.0) + - FirebaseAuthInterop (~> 12.4.0) + - FirebaseCore (~> 12.4.0) + - FirebaseCoreExtension (~> 12.4.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/Environment (~> 8.1) + - GTMSessionFetcher/Core (< 6.0, >= 3.4) + - RecaptchaInterop (~> 101.0) + - FirebaseAuthInterop (12.4.0) + - FirebaseCore (12.4.0): + - FirebaseCoreInternal (~> 12.4.0) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/Logger (~> 8.1) + - FirebaseCoreExtension (12.4.0): + - FirebaseCore (~> 12.4.0) + - FirebaseCoreInternal (12.4.0): + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - Flutter (1.0.0) + - GoogleUtilities/AppDelegateSwizzler (8.1.0): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Privacy + - GoogleUtilities/Environment (8.1.0): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (8.1.0): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - GoogleUtilities/Network (8.1.0): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (8.1.0)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (8.1.0) + - GoogleUtilities/Reachability (8.1.0): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GoogleUtilities/UserDefaults (8.1.0): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GTMSessionFetcher/Core (5.0.0) + - PromisesObjC (2.4.0) + - RecaptchaInterop (101.0.0) + +DEPENDENCIES: + - firebase_app_check (from `.symlinks/plugins/firebase_app_check/ios`) + - firebase_auth (from `.symlinks/plugins/firebase_auth/ios`) + - firebase_core (from `.symlinks/plugins/firebase_core/ios`) + - Flutter (from `Flutter`) + +SPEC REPOS: + trunk: + - AppCheckCore + - Firebase + - FirebaseAppCheck + - FirebaseAppCheckInterop + - FirebaseAuth + - FirebaseAuthInterop + - FirebaseCore + - FirebaseCoreExtension + - FirebaseCoreInternal + - GoogleUtilities + - GTMSessionFetcher + - PromisesObjC + - RecaptchaInterop + +EXTERNAL SOURCES: + firebase_app_check: + :path: ".symlinks/plugins/firebase_app_check/ios" + firebase_auth: + :path: ".symlinks/plugins/firebase_auth/ios" + firebase_core: + :path: ".symlinks/plugins/firebase_core/ios" + Flutter: + :path: Flutter + +SPEC CHECKSUMS: + AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f + Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e + firebase_app_check: 61fb3578a0761c806533482aca240a2d5cc5b5ef + firebase_auth: 9225db04db5d8e3b46dc8940e04bc6aec6833e27 + firebase_core: f1aafb21c14f497e5498f7ffc4dc63cbb52b2594 + FirebaseAppCheck: 73721d98fa29cf199da6004e57715cbaddd49651 + FirebaseAppCheckInterop: f734c802f21fe1da0837708f0f9a27218c8a4ed0 + FirebaseAuth: 4a2aed737c84114a9d9b33d11ae1b147d6b94889 + FirebaseAuthInterop: 858e6b754966e70740a4370dd1503dfffe6dbb49 + FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3 + FirebaseCoreExtension: 7e1f7118ee970e001a8013719fb90950ee5e0018 + FirebaseCoreInternal: d7f5a043c2cd01a08103ab586587c1468047bca6 + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 + GTMSessionFetcher: 02d6e866e90bc236f48a703a041dfe43e6221a29 + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + RecaptchaInterop: 11e0b637842dfb48308d242afc3f448062325aba + +PODFILE CHECKSUM: 53a6aebc29ccee84c41f92f409fc20cd4ca011f1 + +COCOAPODS: 1.16.2 diff --git a/examples/express_chat/ios/Runner.xcodeproj/project.pbxproj b/examples/express_chat/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000..6b5a68502 --- /dev/null +++ b/examples/express_chat/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,758 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 13BE413A7A6F0715D37BE240 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5C343F7D99DDE3FFA47912B5 /* GoogleService-Info.plist */; }; + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 648F713DD7F9A1E19447DA35 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E2423408A58C6DDAF60EB235 /* Pods_Runner.framework */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 93C0D28C73168168CD7FFE52 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6EA1AA490C057AD827AF63AC /* 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 = ""; }; + 175B4A03834CA8F680CD2202 /* 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 = ""; }; + 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 = ""; }; + 4B7DABC696DF2988D9D7A1D4 /* 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 = ""; }; + 5C343F7D99DDE3FFA47912B5 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; + 6EA1AA490C057AD827AF63AC /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 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 = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 91A4FAAFD9106577D5E76C9A /* 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 = ""; }; + 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 = ""; }; + 9EB25C26CF76E8A920F22D5A /* 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 = ""; }; + A582BB0C4D57BE8171CA4C04 /* 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 = ""; }; + E2423408A58C6DDAF60EB235 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F95ED38A4EF475EE27E888DC /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 648F713DD7F9A1E19447DA35 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C7883C98504D9C01B0C0B8DB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 93C0D28C73168168CD7FFE52 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0FF38C0DE60523A00C090E29 /* Pods */ = { + isa = PBXGroup; + children = ( + 238CBA884696395134813285 /* Pods-Runner.debug.xcconfig */, + 5E3B4A8BDEF2FF6E6FC6EDCC /* Pods-Runner.release.xcconfig */, + 508DF401F69417764E1E5B66 /* Pods-Runner.profile.xcconfig */, + 6EEB7E7FB6E5A6E29821B663 /* Pods-RunnerTests.debug.xcconfig */, + 9A097EFF4049C4AE69A97A4F /* Pods-RunnerTests.release.xcconfig */, + 429904E3B2952C32432502DE /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 47AB846AF9F050FE2C708492 /* Frameworks */ = { + isa = PBXGroup; + children = ( + D8CE641C45D7677CAFE8A9D5 /* Pods_Runner.framework */, + 4986175F9B388CE631CC1EC2 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 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 */, + 5C343F7D99DDE3FFA47912B5 /* GoogleService-Info.plist */, + 9E383EE76196043F53C0F088 /* Pods */, + BC1235CD57B2C51D54A332BA /* 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 */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + 9E383EE76196043F53C0F088 /* Pods */ = { + isa = PBXGroup; + children = ( + 9EB25C26CF76E8A920F22D5A /* Pods-Runner.debug.xcconfig */, + 175B4A03834CA8F680CD2202 /* Pods-Runner.release.xcconfig */, + 4B7DABC696DF2988D9D7A1D4 /* Pods-Runner.profile.xcconfig */, + A582BB0C4D57BE8171CA4C04 /* Pods-RunnerTests.debug.xcconfig */, + 91A4FAAFD9106577D5E76C9A /* Pods-RunnerTests.release.xcconfig */, + F95ED38A4EF475EE27E888DC /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + BC1235CD57B2C51D54A332BA /* Frameworks */ = { + isa = PBXGroup; + children = ( + E2423408A58C6DDAF60EB235 /* Pods_Runner.framework */, + 6EA1AA490C057AD827AF63AC /* 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 = ( + E042B4F72D8E0D6A85A07AB4 /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + C7883C98504D9C01B0C0B8DB /* 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 = ( + 89ACBDE759E82DA6A7743559 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + D3A66B593851E25A1A8795D6 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + 13BE413A7A6F0715D37BE240 /* GoogleService-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 89ACBDE759E82DA6A7743559 /* [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; + }; + 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"; + }; + D3A66B593851E25A1A8795D6 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + E042B4F72D8E0D6A85A07AB4 /* [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; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 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 = 15.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.simpleChat; + 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 = A582BB0C4D57BE8171CA4C04 /* 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.simpleChat.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 = 91A4FAAFD9106577D5E76C9A /* 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.simpleChat.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 = F95ED38A4EF475EE27E888DC /* 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.simpleChat.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 = 15.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 = 15.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.simpleChat; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.simpleChat; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/examples/express_chat/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/examples/express_chat/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/examples/express_chat/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/examples/express_chat/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/examples/express_chat/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/examples/express_chat/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/examples/express_chat/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/examples/express_chat/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000..f9b0d7c5e --- /dev/null +++ b/examples/express_chat/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/examples/express_chat/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/examples/express_chat/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000..e3773d42e --- /dev/null +++ b/examples/express_chat/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/express_chat/ios/Runner.xcworkspace/contents.xcworkspacedata b/examples/express_chat/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..21a3cc14c --- /dev/null +++ b/examples/express_chat/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/examples/express_chat/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/examples/express_chat/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/examples/express_chat/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/examples/express_chat/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/examples/express_chat/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000..f9b0d7c5e --- /dev/null +++ b/examples/express_chat/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/examples/express_chat/ios/Runner/AppDelegate.swift b/examples/express_chat/ios/Runner/AppDelegate.swift new file mode 100644 index 000000000..4cb238206 --- /dev/null +++ b/examples/express_chat/ios/Runner/AppDelegate.swift @@ -0,0 +1,17 @@ +// 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 Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..d36b1fab2 --- /dev/null +++ b/examples/express_chat/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/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 000000000..dc9ada472 Binary files /dev/null and b/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 000000000..7353c41ec Binary files /dev/null and b/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 000000000..797d452e4 Binary files /dev/null and b/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 000000000..6ed2d933e Binary files /dev/null and b/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 000000000..4cd7b0099 Binary files /dev/null and b/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 000000000..fe730945a Binary files /dev/null and b/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 000000000..321773cd8 Binary files /dev/null and b/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 000000000..797d452e4 Binary files /dev/null and b/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 000000000..502f463a9 Binary files /dev/null and b/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 000000000..0ec303439 Binary files /dev/null and b/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 000000000..0ec303439 Binary files /dev/null and b/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 000000000..e9f5fea27 Binary files /dev/null and b/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 000000000..84ac32ae7 Binary files /dev/null and b/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 000000000..8953cba09 Binary files /dev/null and b/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 000000000..0467bf12a Binary files /dev/null and b/examples/express_chat/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/examples/express_chat/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/examples/express_chat/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 000000000..0bedcf2fd --- /dev/null +++ b/examples/express_chat/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/examples/express_chat/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/examples/express_chat/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 000000000..9da19eaca Binary files /dev/null and b/examples/express_chat/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/examples/express_chat/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/examples/express_chat/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 000000000..9da19eaca Binary files /dev/null and b/examples/express_chat/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/examples/express_chat/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/examples/express_chat/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 000000000..9da19eaca Binary files /dev/null and b/examples/express_chat/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/examples/express_chat/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/examples/express_chat/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 000000000..89c2725b7 --- /dev/null +++ b/examples/express_chat/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/examples/express_chat/ios/Runner/Base.lproj/LaunchScreen.storyboard b/examples/express_chat/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000..f2e259c7c --- /dev/null +++ b/examples/express_chat/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/express_chat/ios/Runner/Base.lproj/Main.storyboard b/examples/express_chat/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 000000000..f3c28516f --- /dev/null +++ b/examples/express_chat/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/express_chat/ios/Runner/Info.plist b/examples/express_chat/ios/Runner/Info.plist new file mode 100644 index 000000000..61f7ff009 --- /dev/null +++ b/examples/express_chat/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Simple Chat + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + simple_chat + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/examples/express_chat/ios/Runner/Runner-Bridging-Header.h b/examples/express_chat/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 000000000..02588e01d --- /dev/null +++ b/examples/express_chat/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1,5 @@ +// 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 "GeneratedPluginRegistrant.h" diff --git a/examples/express_chat/ios/RunnerTests/RunnerTests.swift b/examples/express_chat/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 000000000..7d077cd5a --- /dev/null +++ b/examples/express_chat/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,16 @@ +// 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 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/examples/express_chat/lib/chat_session.dart b/examples/express_chat/lib/chat_session.dart new file mode 100644 index 000000000..aa74fd4be --- /dev/null +++ b/examples/express_chat/lib/chat_session.dart @@ -0,0 +1,322 @@ +// 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 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:genkit/genkit.dart' as genkit; +import 'package:genui/genui.dart'; +import 'package:genui_express/genui_express.dart'; +import 'package:logging/logging.dart'; + +import 'chrome_plugin_stub.dart' + if (dart.library.js_interop) 'chrome_plugin_web.dart'; + +import 'primitives/app_mode.dart'; +import 'primitives/climbing/a2ui_components/climbing.dart'; +import 'primitives/message.dart'; + +/// System prompts used to configure the chat sessions in this example. +abstract final class Prompts { + Prompts._(); + + static const String summary = + 'You are a helpful assistant who chats with a user.'; + + static final String choicePicker = + ''' +When you need additional information from the user, try to use the component '${BasicCatalogItems.choicePicker.name}' to ask for it. +'''; + + static final String textFieldFallback = + ''' +If there is no way to itemize all the options, either use the component '${BasicCatalogItems.textField.name}' or add option 'Other' to the '${BasicCatalogItems.choicePicker.name}'. +'''; + + static final String climbingLocations = + ''' +IMPORTANT: Always immediately display the matching climbing locations using the rich 'ClimbingLocation' component card in your response. Do not ask the user for more information, preferences, or clarification first. Show the best matches (like beginner-friendly locations) immediately in A2UI Express syntax. +IMPORTANT: You MUST surround the entire A2UI Express layout DSL block with the sentinel tags '' and '' to separate it from your conversational explanation. + +Available Climbing Locations (use these exact identifiers in ClimbingLocation): +- 'kraft_boulders': Kraft Boulders (Outdoor, Free, Bouldering, Beginner/Intermediate/Advanced) +- 'calico_hills_i': Calico Hills I (Outdoor, Paid, Lead/Top Rope, Beginner/Intermediate) +- 'willow_springs': Willow Springs (Outdoor, Paid, Lead/Bouldering, Beginner/Intermediate) +- 'origin_climbing_fitness': Origin Climbing + Fitness (Indoor, Paid, Bouldering/Lead/Top Rope, Beginner/Intermediate/Advanced) +- 'the_refuge_climbing_center': The Refuge Climbing Center (Indoor, Paid, Bouldering, Beginner/Intermediate/Advanced) +- 'red_rock_climbing_center': Red Rock Climbing Center (Indoor, Paid, Lead/Top Rope/Bouldering, Beginner/Intermediate/Advanced) +- 'lone_mountain': Lone Mountain (Outdoor, Free, Lead, Beginner/Intermediate) + +When the user asks about climbing locations, you must choose the most appropriate location identifiers matching their query (e.g., beginner-friendly, indoor/outdoor, bouldering, free/paid) and display them. +Always use the component named '${climbingLocationItem.name}' with the chosen identifier to display each location. +You must compose the final layout tree under the reserved 'root' variable using Column or Row to hold the location components. +Do not add any extra submit or confirmation buttons next to '${climbingLocationItem.name}' since it already contains a 'Learn more' button. + +Example: + +root = Column([loc1, loc2]) +loc1 = ClimbingLocation("kraft_boulders") +loc2 = ClimbingLocation("lone_mountain") + + +When the user clicks 'Learn more' on a '${climbingLocationItem.name}', a UI action named 'learnMoreAboutLocation' will be sent with the location's identifier and name in its context. Respond with detailed information about that specific location. +'''; +} + +final Catalog _basicCatalog = BasicCatalogItems.asNoAssetCatalog( + systemPromptFragments: [Prompts.choicePicker, Prompts.textFieldFallback], +); + +final Catalog _customCatalog = _basicCatalog.copyWith( + systemPromptFragments: [ + Prompts.climbingLocations, + ..._basicCatalog.systemPromptFragments, + ], + newItems: [climbingLocationItem], +); + +sealed class ChatSession extends ChangeNotifier { + ChatSession._(); + + factory ChatSession({ + genkit.Genkit? ai, + genkit.ModelRef? model, + required AppMode mode, + }) { + final genkit.Genkit effectiveAi = + ai ?? genkit.Genkit(isDevEnv: false, plugins: getPlatformPlugins()); + if (ai == null) { + GenuiExpressLocalModels.register(effectiveAi); + } + final genkit.ModelRef effectiveModel = + model ?? + (kIsWeb + ? genkit.modelRef('chrome/gemini-nano') + : genkit.modelRef(GenuiExpressLocalModels.httpCompletion)); + + return switch (mode) { + AppMode.customCatalog => A2uiChatSession( + ai: effectiveAi, + model: effectiveModel, + catalog: _customCatalog, + ), + AppMode.basicCatalog => A2uiChatSession( + ai: effectiveAi, + model: effectiveModel, + catalog: _basicCatalog, + ), + AppMode.textOnly => TextOnlyChatSession( + ai: effectiveAi, + model: effectiveModel, + ), + }; + } + + final List _messages = []; + List get messages => _messages; + + bool _isProcessing = false; + bool get isProcessing => _isProcessing; + + /// The surface host for rendering generative UI surfaces, or `null` if this + /// session does not produce surfaces (e.g. text-only chat). + SurfaceHost? get surfaceController => null; + + final Logger _logger = Logger('ChatSession'); + + Message? _currentAiMessage; + + Future sendMessage(String text); + + void _addUserMessage(String text) { + _messages.add(Message(isUser: true, text: 'You: $text')); + notifyListeners(); + } + + void _updateAiMessage(String chunk) { + if (_currentAiMessage == null) { + _currentAiMessage = Message(isUser: false, text: ''); + _messages.add(_currentAiMessage!); + } + _currentAiMessage!.text = (_currentAiMessage!.text ?? '') + chunk; + notifyListeners(); + } + + void _reportError(Object error, {required bool showInChat}) { + _logger.severe('Error in conversation', error); + if (showInChat) { + _messages.add(Message(isUser: false, text: 'Error: $error')); + notifyListeners(); + } + } + + Future _runRequest(Future Function() body) async { + _isProcessing = true; + notifyListeners(); + try { + await body(); + } catch (exception, stackTrace) { + _logger.severe('Error sending request', exception, stackTrace); + _reportError(exception, showInChat: true); + } finally { + _isProcessing = false; + notifyListeners(); + } + } +} + +/// A chat session that only supports text messages. +class TextOnlyChatSession extends ChatSession { + TextOnlyChatSession({ + required genkit.Genkit ai, + required genkit.ModelRef model, + }) : _ai = ai, + _model = model, + super._() { + _messagesHistory.add( + genkit.Message( + role: genkit.Role.system, + content: [genkit.TextPart(text: Prompts.summary)], + ), + ); + } + + final genkit.Genkit _ai; + final genkit.ModelRef _model; + final List _messagesHistory = []; + + @override + Future sendMessage(String text) async { + if (text.isEmpty) return; + + _currentAiMessage = null; + _addUserMessage(text); + + await _runRequest(() async { + _messagesHistory.add( + genkit.Message( + role: genkit.Role.user, + content: [genkit.TextPart(text: text)], + ), + ); + + final Stream> stream = _ai + .generateStream( + model: _model, + messages: _messagesHistory, + ); + + final buffer = StringBuffer(); + await for (final chunk in stream) { + if (chunk.text.isNotEmpty) { + buffer.write(chunk.text); + _updateAiMessage(chunk.text); + } + } + + _messagesHistory.add( + genkit.Message( + role: genkit.Role.model, + content: [genkit.TextPart(text: buffer.toString())], + ), + ); + }); + } +} + +/// A chat session that supports generative UI. +class A2uiChatSession extends ChatSession { + A2uiChatSession({ + required genkit.Genkit ai, + required genkit.ModelRef model, + required Catalog catalog, + }) : _catalog = catalog, + super._() { + _transport = ExpressLocalTransport(ai: ai, model: model, catalog: catalog); + _surfaceController = SurfaceController(catalogs: [catalog]); + _init(); + } + + final Catalog _catalog; + + late final ExpressLocalTransport _transport; + late final SurfaceController _surfaceController; + + @override + SurfaceController get surfaceController => _surfaceController; + + late final StreamSubscription _messageSub; + late final StreamSubscription _textSub; + late final StreamSubscription _submitSub; + late final StreamSubscription _surfaceSub; + + void _init() { + _messageSub = _transport.incomingMessages.listen( + _surfaceController.handleMessage, + ); + _textSub = _transport.incomingText.listen(_updateAiMessage); + _submitSub = _surfaceController.onSubmit.listen( + (message) => _runRequest(() => _transport.sendRequest(message)), + ); + _surfaceSub = _surfaceController.surfaceUpdates.listen(_onSurfaceUpdate); + + _transport.addSystemMessage( + ExpressPromptBuilder.chat( + catalog: _catalog, + systemPromptFragments: [ + Prompts.summary, + PromptFragments.acknowledgeUser(), + PromptFragments.requireAtLeastOneSubmitElement( + prefix: PromptBuilder.defaultImportancePrefix, + ), + ], + ).systemPromptJoined(), + ); + } + + void _onSurfaceUpdate(SurfaceUpdate update) { + switch (update) { + case SurfaceAdded(:final surfaceId): + _addSurfaceMessage(surfaceId); + case SurfaceRemoved(:final surfaceId): + _reportError( + 'Surface $surfaceId removed, that should not happen in chat.', + showInChat: false, + ); + case ComponentsUpdated(): + break; + } + } + + void _addSurfaceMessage(String surfaceId) { + final bool exists = _messages.any((m) => m.surfaceId == surfaceId); + if (!exists) { + _messages.add(Message(isUser: false, text: null, surfaceId: surfaceId)); + notifyListeners(); + } + } + + @override + Future sendMessage(String text) async { + if (text.isEmpty) return; + + // Reset current AI message so new response gets a new bubble + _currentAiMessage = null; + + _addUserMessage(text); + + await _runRequest(() => _transport.sendRequest(ChatMessage.user(text))); + } + + @override + void dispose() { + _messageSub.cancel(); + _textSub.cancel(); + _submitSub.cancel(); + _surfaceSub.cancel(); + _surfaceController.dispose(); + _transport.dispose(); + super.dispose(); + } +} diff --git a/examples/express_chat/lib/chrome_plugin_stub.dart b/examples/express_chat/lib/chrome_plugin_stub.dart new file mode 100644 index 000000000..027416dfd --- /dev/null +++ b/examples/express_chat/lib/chrome_plugin_stub.dart @@ -0,0 +1,8 @@ +// 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:genkit/plugin.dart'; + +/// Native stub helper to return platform-specific plugins. +List getPlatformPlugins() => []; diff --git a/examples/express_chat/lib/chrome_plugin_web.dart b/examples/express_chat/lib/chrome_plugin_web.dart new file mode 100644 index 000000000..9e2e9657e --- /dev/null +++ b/examples/express_chat/lib/chrome_plugin_web.dart @@ -0,0 +1,9 @@ +// 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:genkit/plugin.dart'; +import 'package:genkit_chrome/genkit_chrome.dart'; + +/// Web helper to return the Chrome AI plugin. +List getPlatformPlugins() => [chromeAI()]; diff --git a/examples/express_chat/lib/main.dart b/examples/express_chat/lib/main.dart new file mode 100644 index 000000000..e77e6f2e0 --- /dev/null +++ b/examples/express_chat/lib/main.dart @@ -0,0 +1,208 @@ +// 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:genkit/genkit.dart' as genkit; +import 'package:logging/logging.dart'; + +import 'chat_session.dart'; +import 'primitives/app_mode.dart'; +import 'primitives/message.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + // Configure logging for the app. + Logger.root.level = Level.ALL; + Logger.root.onRecord.listen((record) { + debugPrint('${record.level.name}: ${record.time}: ${record.message}'); + }); + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + final colorScheme = ColorScheme.fromSeed(seedColor: Colors.blue); + return MaterialApp( + title: 'Simple Chat Controller', + theme: ThemeData(colorScheme: colorScheme), + darkTheme: ThemeData( + colorScheme: colorScheme.copyWith(brightness: Brightness.dark), + ), + home: const ChatScreen(), + ); + } +} + +class ChatScreen extends StatefulWidget { + const ChatScreen({super.key, this.ai, this.model}); + + final genkit.Genkit? ai; + final genkit.ModelRef? model; + + @override + State createState() => _ChatScreenState(); +} + +const String _defaultUserMessage = + """I'm into rock climbing. Give me a few climbing locations around Las Vegas. I'm a beginner."""; + +class _ChatScreenState extends State { + final TextEditingController _textController = TextEditingController( + text: _defaultUserMessage, + ); + final ScrollController _scrollController = ScrollController(); + late ChatSession _chatSession; + AppMode _appMode = AppMode.customCatalog; + + @override + void initState() { + super.initState(); + _reCreateChatSession(dispose: false); + } + + void _reCreateChatSession({bool dispose = true}) { + if (dispose) { + _chatSession.removeListener(_scrollToBottom); + _chatSession.dispose(); + } + _chatSession = ChatSession( + ai: widget.ai, + model: widget.model, + mode: _appMode, + ); + // Add a listener to scroll to bottom when messages change. + _chatSession.addListener(_scrollToBottom); + _textController.text = _defaultUserMessage; + } + + @override + Widget build(BuildContext context) { + return ListenableBuilder( + listenable: _chatSession, + builder: (context, _) { + return Scaffold( + appBar: AppBar( + title: const Text('Chat (Controller + Gemma 4)'), + actions: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: DropdownButton( + value: _appMode, + underline: const SizedBox.shrink(), + onChanged: (mode) { + if (mode == null) return; + _changeMode(mode); + }, + items: [ + for (final mode in AppMode.values) + DropdownMenuItem( + value: mode, + child: Text(mode.displayName), + ), + ], + ), + ), + ], + ), + + body: SafeArea( + child: Column( + children: [ + Expanded( + child: ListView.builder( + controller: _scrollController, + itemCount: _chatSession.messages.length, + itemBuilder: (context, index) { + final Message message = _chatSession.messages[index]; + // Pass the controller as the host. + return ListTile( + title: MessageView( + message, + _chatSession.surfaceController, + ), + tileColor: message.isUser + ? Colors.blue.withValues(alpha: 0.1) + : null, + ); + }, + ), + ), + + if (_chatSession.isProcessing) + const Padding( + padding: EdgeInsets.all(8.0), + child: CircularProgressIndicator(), + ), + + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _textController, + decoration: const InputDecoration( + hintText: 'Type your message...', + ), + enabled: !_chatSession.isProcessing, + onSubmitted: (_) => _sendMessage(), + ), + ), + IconButton( + icon: const Icon(Icons.send), + onPressed: _chatSession.isProcessing + ? null + : _sendMessage, + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + } + + void _changeMode(AppMode mode) { + if (mode == _appMode) return; + setState(() { + _appMode = mode; + _reCreateChatSession(); + }); + } + + Future _sendMessage() async { + final String text = _textController.text; + if (text.isEmpty) return; + _textController.clear(); + await _chatSession.sendMessage(text); + } + + void _scrollToBottom() { + if (!mounted) return; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + if (_scrollController.hasClients) { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } + }); + } + + @override + void dispose() { + _chatSession.dispose(); + _textController.dispose(); + _scrollController.dispose(); + super.dispose(); + } +} diff --git a/examples/express_chat/lib/primitives/app_mode.dart b/examples/express_chat/lib/primitives/app_mode.dart new file mode 100644 index 000000000..dd4c3e925 --- /dev/null +++ b/examples/express_chat/lib/primitives/app_mode.dart @@ -0,0 +1,20 @@ +// 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. + +/// Defines the mode of the application. +enum AppMode { + /// Agent responds with text only. + textOnly('Text only'), + + /// Agent responds with text and the basic catalog items. + basicCatalog('Basic catalog'), + + /// Agent responds with text and custom catalog items. + customCatalog('Custom catalog'); + + const AppMode(this.displayName); + + /// The user-friendly name of the app mode. + final String displayName; +} diff --git a/examples/express_chat/lib/primitives/climbing/a2ui_components/climbing.dart b/examples/express_chat/lib/primitives/climbing/a2ui_components/climbing.dart new file mode 100644 index 000000000..611f80efc --- /dev/null +++ b/examples/express_chat/lib/primitives/climbing/a2ui_components/climbing.dart @@ -0,0 +1,196 @@ +// 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 '../climbing_db.dart'; + +final CatalogItem climbingLocationItem = CatalogItem( + name: 'ClimbingLocation', + dataSchema: S.object( + description: 'A card showing information about a climbing location.', + properties: { + 'identifier': S.string( + description: 'The unique identifier of the climbing location.', + ), + }, + required: ['identifier'], + ), + exampleData: [ + () => ''' + [ + { + "id": "root", + "component": "ClimbingLocation", + "identifier": "kraft_boulders" + } + ] + ''', + ], + widgetBuilder: (itemContext) { + final data = itemContext.data as Map; + final Object? identifier = data['identifier']; + if (identifier is! String) { + return const Card( + child: Padding( + padding: EdgeInsets.all(16.0), + child: Text( + 'Invalid or missing climbing location identifier', + style: TextStyle(color: Colors.red), + ), + ), + ); + } + + final int index = climbingLocations.indexWhere( + (v) => v.identifier == identifier, + ); + if (index == -1) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + 'Climbing location not found: $identifier', + style: const TextStyle(color: Colors.red), + ), + ), + ); + } + final ClimbingLocationInfo info = climbingLocations[index]; + + return ClimbingLocation( + info: info, + onLearnMore: () { + itemContext.dispatchEvent( + UserActionEvent( + name: 'learnMoreAboutLocation', + sourceComponentId: itemContext.id, + context: {'identifier': info.identifier, 'name': info.name}, + ), + ); + }, + ); + }, +); + +/// A card widget that displays information about a climbing location. +class ClimbingLocation extends StatelessWidget { + const ClimbingLocation({super.key, required this.info, this.onLearnMore}); + + final ClimbingLocationInfo info; + final VoidCallback? onLearnMore; + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + + return Card( + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.0)), + elevation: 4, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 200, + width: double.infinity, + child: Image.asset( + 'assets/climbing/${info.image}', + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + color: theme.colorScheme.surfaceContainerHighest, + child: const Icon(Icons.broken_image, size: 48), + ); + }, + ), + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + info.name, + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + info.address, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 8), + Wrap( + spacing: 8.0, + runSpacing: 4.0, + children: info.properties.map((prop) { + return Chip( + label: Text(prop.displayName), + backgroundColor: + theme.colorScheme.surfaceContainerHighest, + labelStyle: TextStyle( + color: theme.colorScheme.onSurfaceVariant, + ), + side: BorderSide.none, + ); + }).toList(), + ), + const SizedBox(height: 12), + Text('Climbing Types', style: theme.textTheme.titleSmall), + const SizedBox(height: 4), + Wrap( + spacing: 8.0, + runSpacing: 4.0, + children: info.climbingTypes.map((type) { + return Chip( + label: Text(type.displayName), + backgroundColor: theme.colorScheme.primaryContainer, + labelStyle: TextStyle( + color: theme.colorScheme.onPrimaryContainer, + ), + side: BorderSide.none, + ); + }).toList(), + ), + const SizedBox(height: 12), + Text('Experience Levels', style: theme.textTheme.titleSmall), + const SizedBox(height: 4), + Wrap( + spacing: 8.0, + runSpacing: 4.0, + children: info.experienceRanges.map((level) { + return Chip( + label: Text(level.displayName), + backgroundColor: theme.colorScheme.secondaryContainer, + labelStyle: TextStyle( + color: theme.colorScheme.onSecondaryContainer, + ), + side: BorderSide.none, + ); + }).toList(), + ), + if (onLearnMore != null) ...[ + const SizedBox(height: 16), + Align( + alignment: Alignment.centerRight, + child: FilledButton( + onPressed: onLearnMore, + child: const Text('Learn more'), + ), + ), + ], + ], + ), + ), + ], + ), + ); + } +} diff --git a/examples/express_chat/lib/primitives/climbing/a2ui_components/climbing_gallery.dart b/examples/express_chat/lib/primitives/climbing/a2ui_components/climbing_gallery.dart new file mode 100644 index 000000000..0cacf49bc --- /dev/null +++ b/examples/express_chat/lib/primitives/climbing/a2ui_components/climbing_gallery.dart @@ -0,0 +1,29 @@ +// 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 '../climbing_db.dart'; +import 'climbing.dart'; + +class ClimbingGallery extends StatelessWidget { + const ClimbingGallery({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Climbing Locations Gallery')), + body: ListView.builder( + padding: const EdgeInsets.all(16.0), + itemCount: climbingLocations.length, + itemBuilder: (context, index) { + final ClimbingLocationInfo location = climbingLocations[index]; + return Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: ClimbingLocation(info: location), + ); + }, + ), + ); + } +} diff --git a/examples/express_chat/lib/primitives/climbing/climbing_db.dart b/examples/express_chat/lib/primitives/climbing/climbing_db.dart new file mode 100644 index 000000000..5ca4e1296 --- /dev/null +++ b/examples/express_chat/lib/primitives/climbing/climbing_db.dart @@ -0,0 +1,286 @@ +// 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. + +/// Types of climbing available at a location. +enum ClimbingType { + /// Climbing short walls without ropes over pads. + bouldering('Bouldering'), + + /// Climbing with a rope anchored at the top. + topRope('Top Rope'), + + /// Climbing while clipping the rope into protection on the way up. + lead('Lead'), + + /// Climbing up cracks using specialized technique. + crack('Crack'); + + const ClimbingType(this.displayName); + + /// The user-friendly name of the climbing type. + final String displayName; +} + +/// Experience ranges for routes at a location. +enum ExperienceRange { + /// Easy routes suitable for first-timers and novices. + beginner('Beginner'), + + /// Moderately challenging routes for climbers with some experience. + intermediate('Intermediate'), + + /// Challenging routes for seasoned climbers. + advanced('Advanced'); + + const ExperienceRange(this.displayName); + + /// The user-friendly name of the experience range. + final String displayName; +} + +/// Properties or features of a climbing location. +enum LocationProperty { + /// Located indoors. + indoor('Indoor'), + + /// Located outdoors. + outdoor('Outdoor'), + + /// Free to access. + free('Free'), + + /// Requires payment to access. + paid('Paid'), + + /// Requires a permit to climb. + permitRequired('Permit Required'); + + const LocationProperty(this.displayName); + + /// The user-friendly name of the property. + final String displayName; +} + +/// Information about a climbing location. +class ClimbingLocationInfo { + const ClimbingLocationInfo({ + required this.identifier, + required this.image, + required this.name, + required this.address, + required this.climbingTypes, + required this.experienceRanges, + required this.properties, + }); + + final String identifier; + final String image; + final String name; + final String address; + + /// Types of climbing available at this location. + final List climbingTypes; + + /// Experience ranges for routes at this location. + final List experienceRanges; + + /// Properties or features of this location. + final List properties; + Map toJson() { + return { + 'identifier': identifier, + 'image': image, + 'name': name, + 'address': address, + 'climbingTypes': climbingTypes.map((e) => e.name).toList(), + 'experienceRanges': experienceRanges.map((e) => e.name).toList(), + 'properties': properties.map((e) => e.name).toList(), + }; + } +} + +/// A global list of climbing locations. +List climbingLocations = [ + const ClimbingLocationInfo( + identifier: 'calico_hills_i', + image: '10x2500x1667.jpg', + name: 'Calico Hills I', + address: 'Red Rock Canyon, NV', + climbingTypes: [ClimbingType.lead, ClimbingType.topRope], + experienceRanges: [ExperienceRange.beginner, ExperienceRange.intermediate], + properties: [LocationProperty.outdoor, LocationProperty.paid], + ), + const ClimbingLocationInfo( + identifier: 'kraft_boulders', + image: '121x1600x1067.jpg', + name: 'Kraft Boulders', + address: 'Calico Basin, NV', + climbingTypes: [ClimbingType.bouldering], + experienceRanges: [ + ExperienceRange.beginner, + ExperienceRange.intermediate, + ExperienceRange.advanced, + ], + properties: [LocationProperty.outdoor, LocationProperty.free], + ), + const ClimbingLocationInfo( + identifier: 'sandstone_quarry', + image: '128x3823x2549.jpg', + name: 'Sandstone Quarry', + address: 'Red Rock Canyon, NV', + climbingTypes: [ClimbingType.lead, ClimbingType.crack], + experienceRanges: [ExperienceRange.intermediate, ExperienceRange.advanced], + properties: [LocationProperty.outdoor, LocationProperty.paid], + ), + const ClimbingLocationInfo( + identifier: 'black_velvet_canyon', + image: '134x4928x3264.jpg', + name: 'Black Velvet Canyon', + address: 'Red Rock Canyon, NV', + climbingTypes: [ClimbingType.lead, ClimbingType.crack], + experienceRanges: [ExperienceRange.advanced], + properties: [ + LocationProperty.outdoor, + LocationProperty.free, + LocationProperty.permitRequired, + ], + ), + const ClimbingLocationInfo( + identifier: 'willow_springs', + image: '136x4032x2272.jpg', + name: 'Willow Springs', + address: 'Red Rock Canyon, NV', + climbingTypes: [ClimbingType.lead, ClimbingType.bouldering], + experienceRanges: [ExperienceRange.beginner, ExperienceRange.intermediate], + properties: [LocationProperty.outdoor, LocationProperty.paid], + ), + const ClimbingLocationInfo( + identifier: 'icebox_canyon', + image: '15x2500x1667.jpg', + name: 'Icebox Canyon', + address: 'Red Rock Canyon, NV', + climbingTypes: [ClimbingType.crack, ClimbingType.lead], + experienceRanges: [ExperienceRange.advanced], + properties: [LocationProperty.outdoor, LocationProperty.paid], + ), + const ClimbingLocationInfo( + identifier: 'pine_creek_canyon', + image: '166x1280x720.jpg', + name: 'Pine Creek Canyon', + address: 'Red Rock Canyon, NV', + climbingTypes: [ClimbingType.lead, ClimbingType.crack], + experienceRanges: [ExperienceRange.intermediate, ExperienceRange.advanced], + properties: [LocationProperty.outdoor, LocationProperty.paid], + ), + const ClimbingLocationInfo( + identifier: 'the_gallery', + image: '177x2515x1830.jpg', + name: 'The Gallery', + address: 'Calico Hills, Red Rock Canyon, NV', + climbingTypes: [ClimbingType.lead], + experienceRanges: [ExperienceRange.intermediate, ExperienceRange.advanced], + properties: [LocationProperty.outdoor, LocationProperty.paid], + ), + const ClimbingLocationInfo( + identifier: 'magic_bus', + image: '184x4288x2848.jpg', + name: 'Magic Bus', + address: 'Calico Hills, Red Rock Canyon, NV', + climbingTypes: [ClimbingType.lead, ClimbingType.topRope], + experienceRanges: [ExperienceRange.beginner, ExperienceRange.intermediate], + properties: [LocationProperty.outdoor, LocationProperty.paid], + ), + const ClimbingLocationInfo( + identifier: 'the_hamlet', + image: '191x2560x1707.jpg', + name: 'The Hamlet', + address: 'Calico Hills, Red Rock Canyon, NV', + climbingTypes: [ClimbingType.lead], + experienceRanges: [ExperienceRange.beginner, ExperienceRange.intermediate], + properties: [LocationProperty.outdoor, LocationProperty.paid], + ), + const ClimbingLocationInfo( + identifier: 'moderate_mecca', + image: '243x2300x1533.jpg', + name: 'Moderate Mecca', + address: 'Calico Hills, Red Rock Canyon, NV', + climbingTypes: [ClimbingType.lead, ClimbingType.topRope], + experienceRanges: [ExperienceRange.beginner, ExperienceRange.intermediate], + properties: [LocationProperty.outdoor, LocationProperty.paid], + ), + const ClimbingLocationInfo( + identifier: 'the_black_corridor', + image: '247x3264x2168.jpg', + name: 'The Black Corridor', + address: 'Red Rock Canyon, NV', + climbingTypes: [ClimbingType.lead], + experienceRanges: [ExperienceRange.intermediate], + properties: [LocationProperty.outdoor, LocationProperty.paid], + ), + const ClimbingLocationInfo( + identifier: 'origin_climbing_fitness', + image: '287x4288x2848.jpg', + name: 'Origin Climbing + Fitness', + address: '7585 S Rainbow Blvd, Las Vegas, NV', + climbingTypes: [ + ClimbingType.bouldering, + ClimbingType.lead, + ClimbingType.topRope, + ], + experienceRanges: [ + ExperienceRange.beginner, + ExperienceRange.intermediate, + ExperienceRange.advanced, + ], + properties: [LocationProperty.indoor, LocationProperty.paid], + ), + const ClimbingLocationInfo( + identifier: 'the_refuge_climbing_center', + image: '28x4928x3264.jpg', + name: 'The Refuge Climbing Center', + address: '6283 S Valley View Blvd, Las Vegas, NV', + climbingTypes: [ClimbingType.bouldering], + experienceRanges: [ + ExperienceRange.beginner, + ExperienceRange.intermediate, + ExperienceRange.advanced, + ], + properties: [LocationProperty.indoor, LocationProperty.paid], + ), + const ClimbingLocationInfo( + identifier: 'red_rock_climbing_center', + image: '296x3072x2048.jpg', + name: 'Red Rock Climbing Center', + address: '8201 W Charleston Blvd, Las Vegas, NV', + climbingTypes: [ + ClimbingType.lead, + ClimbingType.topRope, + ClimbingType.bouldering, + ], + experienceRanges: [ + ExperienceRange.beginner, + ExperienceRange.intermediate, + ExperienceRange.advanced, + ], + properties: [LocationProperty.indoor, LocationProperty.paid], + ), + const ClimbingLocationInfo( + identifier: 'lone_mountain', + image: '29x4000x2670.jpg', + name: 'Lone Mountain', + address: 'Las Vegas, NV', + climbingTypes: [ClimbingType.lead], + experienceRanges: [ExperienceRange.beginner, ExperienceRange.intermediate], + properties: [LocationProperty.outdoor, LocationProperty.free], + ), + const ClimbingLocationInfo( + identifier: 'mount_charleston', + image: '343x2304x1536.jpg', + name: 'Mount Charleston', + address: 'Mt Charleston, NV', + climbingTypes: [ClimbingType.lead], + experienceRanges: [ExperienceRange.intermediate, ExperienceRange.advanced], + properties: [LocationProperty.outdoor, LocationProperty.free], + ), +]; diff --git a/examples/express_chat/lib/primitives/message.dart b/examples/express_chat/lib/primitives/message.dart new file mode 100644 index 000000000..83532e11b --- /dev/null +++ b/examples/express_chat/lib/primitives/message.dart @@ -0,0 +1,45 @@ +// 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:flutter_markdown_plus/flutter_markdown_plus.dart'; +import 'package:genui/genui.dart'; + +class Message { + Message({this.text, this.surfaceId, this.isUser = false}) + : assert((surfaceId == null) != (text == null)); + + String? text; + final String? surfaceId; + final bool isUser; +} + +class MessageView extends StatelessWidget { + const MessageView(this.message, this.host, {super.key}); + + final Message message; + + /// The surface host used to render generative UI surfaces. Required only + /// when [Message.surfaceId] is non-null. + final SurfaceHost? host; + + @override + Widget build(BuildContext context) { + final String? surfaceId = message.surfaceId; + + if (surfaceId == null) { + if (message.isUser) { + return Text(message.text ?? ''); + } else { + return MarkdownBody(data: message.text ?? ''); + } + } + + final SurfaceHost? host = this.host; + if (host == null) { + return Text('Error: Missing SurfaceHost for surface $surfaceId'); + } + return Surface(surfaceContext: host.contextFor(surfaceId)); + } +} diff --git a/examples/express_chat/linux/.gitignore b/examples/express_chat/linux/.gitignore new file mode 100644 index 000000000..d3896c984 --- /dev/null +++ b/examples/express_chat/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/examples/express_chat/linux/CMakeLists.txt b/examples/express_chat/linux/CMakeLists.txt new file mode 100644 index 000000000..a82519949 --- /dev/null +++ b/examples/express_chat/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 "simple_chat") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.simple_chat") + +# 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/examples/express_chat/linux/flutter/CMakeLists.txt b/examples/express_chat/linux/flutter/CMakeLists.txt new file mode 100644 index 000000000..d5bd01648 --- /dev/null +++ b/examples/express_chat/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/examples/express_chat/linux/flutter/generated_plugin_registrant.cc b/examples/express_chat/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 000000000..cc10c4daa --- /dev/null +++ b/examples/express_chat/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/examples/express_chat/linux/flutter/generated_plugin_registrant.h b/examples/express_chat/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 000000000..e0f0a47bc --- /dev/null +++ b/examples/express_chat/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/examples/express_chat/linux/flutter/generated_plugins.cmake b/examples/express_chat/linux/flutter/generated_plugins.cmake new file mode 100644 index 000000000..a2eef970f --- /dev/null +++ b/examples/express_chat/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/examples/express_chat/linux/runner/CMakeLists.txt b/examples/express_chat/linux/runner/CMakeLists.txt new file mode 100644 index 000000000..e97dabc70 --- /dev/null +++ b/examples/express_chat/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/examples/express_chat/linux/runner/main.cc b/examples/express_chat/linux/runner/main.cc new file mode 100644 index 000000000..ceea29f8a --- /dev/null +++ b/examples/express_chat/linux/runner/main.cc @@ -0,0 +1,10 @@ +// 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. + +#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/examples/express_chat/linux/runner/my_application.cc b/examples/express_chat/linux/runner/my_application.cc new file mode 100644 index 000000000..cbe6ea137 --- /dev/null +++ b/examples/express_chat/linux/runner/my_application.cc @@ -0,0 +1,148 @@ +// 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. + +#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, "simple_chat"); + 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, "simple_chat"); + } + + 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/examples/express_chat/linux/runner/my_application.h b/examples/express_chat/linux/runner/my_application.h new file mode 100644 index 000000000..bfd36acf6 --- /dev/null +++ b/examples/express_chat/linux/runner/my_application.h @@ -0,0 +1,22 @@ +// 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. + +#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/examples/express_chat/macos/.gitignore b/examples/express_chat/macos/.gitignore new file mode 100644 index 000000000..746adbb6b --- /dev/null +++ b/examples/express_chat/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/examples/express_chat/macos/Flutter/Flutter-Debug.xcconfig b/examples/express_chat/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 000000000..4b81f9b2d --- /dev/null +++ b/examples/express_chat/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/examples/express_chat/macos/Flutter/Flutter-Release.xcconfig b/examples/express_chat/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 000000000..5caa9d157 --- /dev/null +++ b/examples/express_chat/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/examples/express_chat/macos/Flutter/GeneratedPluginRegistrant.swift b/examples/express_chat/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 000000000..432a2c93b --- /dev/null +++ b/examples/express_chat/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,18 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import audioplayers_darwin +import genui_express +import url_launcher_macos +import video_player_avfoundation + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) + GenuiExpressPlugin.register(with: registry.registrar(forPlugin: "GenuiExpressPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + VideoPlayerPlugin.register(with: registry.registrar(forPlugin: "VideoPlayerPlugin")) +} diff --git a/examples/express_chat/macos/Podfile b/examples/express_chat/macos/Podfile new file mode 100644 index 000000000..276cd7043 --- /dev/null +++ b/examples/express_chat/macos/Podfile @@ -0,0 +1,45 @@ +platform :osx, '15.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', 'ephemeral', '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 Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_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_macos_build_settings(target) + target.build_configurations.each do |config| + config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '15.0' + end + end +end diff --git a/examples/express_chat/macos/Podfile.lock b/examples/express_chat/macos/Podfile.lock new file mode 100644 index 000000000..65a448c2f --- /dev/null +++ b/examples/express_chat/macos/Podfile.lock @@ -0,0 +1,16 @@ +PODS: + - FlutterMacOS (1.0.0) + +DEPENDENCIES: + - FlutterMacOS (from `Flutter/ephemeral`) + +EXTERNAL SOURCES: + FlutterMacOS: + :path: Flutter/ephemeral + +SPEC CHECKSUMS: + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + +PODFILE CHECKSUM: 561fd0563568131ed3f09843fee96c105133644a + +COCOAPODS: 1.16.2 diff --git a/examples/express_chat/macos/Runner.xcodeproj/project.pbxproj b/examples/express_chat/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000..c90413099 --- /dev/null +++ b/examples/express_chat/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,807 @@ +// !$*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 */ + 3194601105D167D63F955843 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CF3A4C5899CF552200E8CBC /* Pods_Runner.framework */; }; + 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 */; }; + F11A3C6CCBEE37426C146C43 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BE242B4E6EAF01C24056F2F7 /* Pods_RunnerTests.framework */; }; +/* 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 */ + 2F7F7B8A1B8DC017B7FAE266 /* 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 = ""; }; + 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 /* simple_chat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = simple_chat.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 = ""; }; + 3E04E509BAB54C12D0CB3C89 /* 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 = ""; }; + 592EA936871A3FFDC0018EA3 /* 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 = ""; }; + 5CF3A4C5899CF552200E8CBC /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 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 = ""; }; + 87D75098A6499C97DA68E6EE /* 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 = ""; }; + 96934E673176E47665E60D9F /* 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 = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + BE242B4E6EAF01C24056F2F7 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C6F17EC3323727C4F655EBBA /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F11A3C6CCBEE37426C146C43 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + 3194601105D167D63F955843 /* Pods_Runner.framework 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 */, + CB35E92F67C0BC65B6874AAC /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* simple_chat.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 = ""; + }; + CB35E92F67C0BC65B6874AAC /* Pods */ = { + isa = PBXGroup; + children = ( + 2F7F7B8A1B8DC017B7FAE266 /* Pods-Runner.debug.xcconfig */, + 87D75098A6499C97DA68E6EE /* Pods-Runner.release.xcconfig */, + C6F17EC3323727C4F655EBBA /* Pods-Runner.profile.xcconfig */, + 3E04E509BAB54C12D0CB3C89 /* Pods-RunnerTests.debug.xcconfig */, + 96934E673176E47665E60D9F /* Pods-RunnerTests.release.xcconfig */, + 592EA936871A3FFDC0018EA3 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 5CF3A4C5899CF552200E8CBC /* Pods_Runner.framework */, + BE242B4E6EAF01C24056F2F7 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 6328F0C8CB2DBF584473AC8E /* [CP] Check Pods Manifest.lock */, + 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 = ( + B1571D5ABC89ECCBDDAC7D0F /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* simple_chat.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 */, + ); + 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"; + }; + 6328F0C8CB2DBF584473AC8E /* [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; + }; + B1571D5ABC89ECCBDDAC7D0F /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 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; + baseConfigurationReference = 3E04E509BAB54C12D0CB3C89 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.simpleChat.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/simple_chat.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/simple_chat"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 96934E673176E47665E60D9F /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.simpleChat.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/simple_chat.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/simple_chat"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 592EA936871A3FFDC0018EA3 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.simpleChat.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/simple_chat.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/simple_chat"; + }; + 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 = 15.0; + 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 = 15.0; + 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 = 15.0; + 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/examples/express_chat/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/examples/express_chat/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/examples/express_chat/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/examples/express_chat/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/examples/express_chat/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000..bb91c8cb8 --- /dev/null +++ b/examples/express_chat/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/express_chat/macos/Runner.xcworkspace/contents.xcworkspacedata b/examples/express_chat/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..21a3cc14c --- /dev/null +++ b/examples/express_chat/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/examples/express_chat/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/examples/express_chat/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/examples/express_chat/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/examples/express_chat/macos/Runner/AppDelegate.swift b/examples/express_chat/macos/Runner/AppDelegate.swift new file mode 100644 index 000000000..43bd41192 --- /dev/null +++ b/examples/express_chat/macos/Runner/AppDelegate.swift @@ -0,0 +1,17 @@ +// 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 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/examples/express_chat/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/examples/express_chat/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..a2ec33f19 --- /dev/null +++ b/examples/express_chat/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/examples/express_chat/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/examples/express_chat/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 000000000..82b6f9d9a Binary files /dev/null and b/examples/express_chat/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/examples/express_chat/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/examples/express_chat/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 000000000..13b35eba5 Binary files /dev/null and b/examples/express_chat/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/examples/express_chat/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/examples/express_chat/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 000000000..0a3f5fa40 Binary files /dev/null and b/examples/express_chat/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/examples/express_chat/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/examples/express_chat/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 000000000..bdb57226d Binary files /dev/null and b/examples/express_chat/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/examples/express_chat/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/examples/express_chat/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 000000000..f083318e0 Binary files /dev/null and b/examples/express_chat/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/examples/express_chat/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/examples/express_chat/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 000000000..326c0e72c Binary files /dev/null and b/examples/express_chat/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/examples/express_chat/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/examples/express_chat/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 000000000..2f1632cfd Binary files /dev/null and b/examples/express_chat/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/examples/express_chat/macos/Runner/Base.lproj/MainMenu.xib b/examples/express_chat/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 000000000..80e867a4e --- /dev/null +++ b/examples/express_chat/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/express_chat/macos/Runner/Configs/AppInfo.xcconfig b/examples/express_chat/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 000000000..cb258ecf9 --- /dev/null +++ b/examples/express_chat/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 = simple_chat + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.simpleChat + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. diff --git a/examples/express_chat/macos/Runner/Configs/Debug.xcconfig b/examples/express_chat/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 000000000..36b0fd946 --- /dev/null +++ b/examples/express_chat/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/examples/express_chat/macos/Runner/Configs/Release.xcconfig b/examples/express_chat/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 000000000..dff4f4956 --- /dev/null +++ b/examples/express_chat/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/examples/express_chat/macos/Runner/Configs/Warnings.xcconfig b/examples/express_chat/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 000000000..42bcbf478 --- /dev/null +++ b/examples/express_chat/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/examples/express_chat/macos/Runner/DebugProfile.entitlements b/examples/express_chat/macos/Runner/DebugProfile.entitlements new file mode 100644 index 000000000..78c36cf44 --- /dev/null +++ b/examples/express_chat/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/examples/express_chat/macos/Runner/Info.plist b/examples/express_chat/macos/Runner/Info.plist new file mode 100644 index 000000000..4789daa6a --- /dev/null +++ b/examples/express_chat/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/examples/express_chat/macos/Runner/MainFlutterWindow.swift b/examples/express_chat/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 000000000..79861d1c4 --- /dev/null +++ b/examples/express_chat/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,19 @@ +// 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 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/examples/express_chat/macos/Runner/Release.entitlements b/examples/express_chat/macos/Runner/Release.entitlements new file mode 100644 index 000000000..08ba3a3fa --- /dev/null +++ b/examples/express_chat/macos/Runner/Release.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/examples/express_chat/macos/RunnerTests/RunnerTests.swift b/examples/express_chat/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 000000000..8b03e329d --- /dev/null +++ b/examples/express_chat/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,16 @@ +// 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 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/examples/express_chat/pubspec.yaml b/examples/express_chat/pubspec.yaml new file mode 100644 index 000000000..455dc5891 --- /dev/null +++ b/examples/express_chat/pubspec.yaml @@ -0,0 +1,38 @@ +# 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. + +name: express_chat +publish_to: "none" + +environment: + sdk: ">=3.10.0 <4.0.0" + flutter: ">=3.35.7 <4.0.0" + +resolution: workspace + +dependencies: + collection: ^1.19.1 + dartantic_ai: ^3.2.0 + flutter: + sdk: flutter + flutter_markdown_plus: ^1.0.7 + genkit: ^0.13.2 + genkit_chrome: ^0.0.8 + genui: ^0.9.0 + genui_express: ^0.1.0 + json_schema_builder: ^0.1.3 + logging: ^1.3.0 + +dev_dependencies: + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + network_image_mock: ^2.1.1 + +flutter: + uses-material-design: true + + assets: + - assets/climbing/ diff --git a/examples/express_chat/test/fake_ai_client.dart b/examples/express_chat/test/fake_ai_client.dart new file mode 100644 index 000000000..f64b861c5 --- /dev/null +++ b/examples/express_chat/test/fake_ai_client.dart @@ -0,0 +1,76 @@ +// 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 'dart:async'; + +import 'package:collection/collection.dart'; +import 'package:genkit/genkit.dart' as genkit; + +/// A Genkit-based fake client implementation for integration tests. +class FakeGenkitClient { + final genkit.Genkit ai = genkit.Genkit(isDevEnv: false); + final List responses = []; + final List receivedPrompts = []; + + FakeGenkitClient() { + ai.defineModel( + name: 'local/fake-model', + fn: (request, context) async { + // Extract the user prompt from request messages + final genkit.Message? userMessage = request.messages.lastWhereOrNull( + (m) => m.role == genkit.Role.user, + ); + final String prompt = + userMessage?.content + .where((p) => p.isText) + .map((p) => p.text) + .join('') ?? + ''; + receivedPrompts.add(prompt); + + if (responses.isEmpty) { + const resp = 'I have no response for that.'; + context.sendChunk( + genkit.ModelResponseChunk(content: [genkit.TextPart(text: resp)]), + ); + return genkit.ModelResponse( + finishReason: genkit.FinishReason.stop, + message: genkit.Message( + role: genkit.Role.model, + content: [genkit.TextPart(text: resp)], + ), + ); + } + + final String response = responses.removeAt(0); + + // Simulate streaming chunks + const chunkSize = 10; + for (var i = 0; i < response.length; i += chunkSize) { + final int end = (i + chunkSize < response.length) + ? i + chunkSize + : response.length; + final String chunk = response.substring(i, end); + context.sendChunk( + genkit.ModelResponseChunk(content: [genkit.TextPart(text: chunk)]), + ); + await Future.delayed(const Duration(milliseconds: 1)); + } + + return genkit.ModelResponse( + finishReason: genkit.FinishReason.stop, + message: genkit.Message( + role: genkit.Role.model, + content: [genkit.TextPart(text: response)], + ), + ); + }, + ); + } + + /// Adds a response to the queue. + void addResponse(String response) { + responses.add(response); + } +} diff --git a/examples/express_chat/test/smoke_test.dart b/examples/express_chat/test/smoke_test.dart new file mode 100644 index 000000000..a128a3f23 --- /dev/null +++ b/examples/express_chat/test/smoke_test.dart @@ -0,0 +1,15 @@ +// 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:express_chat/main.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Smoke test: App starts without issues', ( + WidgetTester tester, + ) async { + await tester.pumpWidget(const MyApp()); + expect(find.byType(ChatScreen), findsOneWidget); + }); +} diff --git a/examples/express_chat/web/favicon.png b/examples/express_chat/web/favicon.png new file mode 100644 index 000000000..8aaa46ac1 Binary files /dev/null and b/examples/express_chat/web/favicon.png differ diff --git a/examples/express_chat/web/icons/Icon-192.png b/examples/express_chat/web/icons/Icon-192.png new file mode 100644 index 000000000..b749bfef0 Binary files /dev/null and b/examples/express_chat/web/icons/Icon-192.png differ diff --git a/examples/express_chat/web/icons/Icon-512.png b/examples/express_chat/web/icons/Icon-512.png new file mode 100644 index 000000000..88cfd48df Binary files /dev/null and b/examples/express_chat/web/icons/Icon-512.png differ diff --git a/examples/express_chat/web/icons/Icon-maskable-192.png b/examples/express_chat/web/icons/Icon-maskable-192.png new file mode 100644 index 000000000..eb9b4d76e Binary files /dev/null and b/examples/express_chat/web/icons/Icon-maskable-192.png differ diff --git a/examples/express_chat/web/icons/Icon-maskable-512.png b/examples/express_chat/web/icons/Icon-maskable-512.png new file mode 100644 index 000000000..d69c56691 Binary files /dev/null and b/examples/express_chat/web/icons/Icon-maskable-512.png differ diff --git a/examples/express_chat/web/index.html b/examples/express_chat/web/index.html new file mode 100644 index 000000000..1c76469f0 --- /dev/null +++ b/examples/express_chat/web/index.html @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + simple_chat + + + + + + diff --git a/examples/express_chat/web/manifest.json b/examples/express_chat/web/manifest.json new file mode 100644 index 000000000..b7b5f9bbd --- /dev/null +++ b/examples/express_chat/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "simple_chat", + "short_name": "simple_chat", + "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/examples/express_chat/windows/.gitignore b/examples/express_chat/windows/.gitignore new file mode 100644 index 000000000..d492d0d98 --- /dev/null +++ b/examples/express_chat/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/examples/express_chat/windows/CMakeLists.txt b/examples/express_chat/windows/CMakeLists.txt new file mode 100644 index 000000000..f4a3d0a37 --- /dev/null +++ b/examples/express_chat/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(simple_chat 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 "simple_chat") + +# 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/examples/express_chat/windows/flutter/CMakeLists.txt b/examples/express_chat/windows/flutter/CMakeLists.txt new file mode 100644 index 000000000..903f4899d --- /dev/null +++ b/examples/express_chat/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/examples/express_chat/windows/flutter/generated_plugin_registrant.cc b/examples/express_chat/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 000000000..8d8a566fd --- /dev/null +++ b/examples/express_chat/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,20 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + AudioplayersWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); + VideoPlayerWinPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("VideoPlayerWinPluginCApi")); +} diff --git a/examples/express_chat/windows/flutter/generated_plugin_registrant.h b/examples/express_chat/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 000000000..dc139d85a --- /dev/null +++ b/examples/express_chat/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/examples/express_chat/windows/flutter/generated_plugins.cmake b/examples/express_chat/windows/flutter/generated_plugins.cmake new file mode 100644 index 000000000..158064786 --- /dev/null +++ b/examples/express_chat/windows/flutter/generated_plugins.cmake @@ -0,0 +1,27 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + audioplayers_windows + 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/examples/express_chat/windows/runner/CMakeLists.txt b/examples/express_chat/windows/runner/CMakeLists.txt new file mode 100644 index 000000000..394917c05 --- /dev/null +++ b/examples/express_chat/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/examples/express_chat/windows/runner/Runner.rc b/examples/express_chat/windows/runner/Runner.rc new file mode 100644 index 000000000..b7473dcaf --- /dev/null +++ b/examples/express_chat/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", "simple_chat" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "simple_chat" "\0" + VALUE "LegalCopyright", "Copyright (C) 2025 com.example." "\0" + VALUE "OriginalFilename", "simple_chat.exe" "\0" + VALUE "ProductName", "simple_chat" "\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/examples/express_chat/windows/runner/flutter_window.cpp b/examples/express_chat/windows/runner/flutter_window.cpp new file mode 100644 index 000000000..84d1989ec --- /dev/null +++ b/examples/express_chat/windows/runner/flutter_window.cpp @@ -0,0 +1,75 @@ +// 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. + +#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/examples/express_chat/windows/runner/flutter_window.h b/examples/express_chat/windows/runner/flutter_window.h new file mode 100644 index 000000000..9ca6118a7 --- /dev/null +++ b/examples/express_chat/windows/runner/flutter_window.h @@ -0,0 +1,37 @@ +// 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. + +#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/examples/express_chat/windows/runner/main.cpp b/examples/express_chat/windows/runner/main.cpp new file mode 100644 index 000000000..29bee7f56 --- /dev/null +++ b/examples/express_chat/windows/runner/main.cpp @@ -0,0 +1,47 @@ +// 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. + +#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"simple_chat", 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/examples/express_chat/windows/runner/resource.h b/examples/express_chat/windows/runner/resource.h new file mode 100644 index 000000000..66a65d1e4 --- /dev/null +++ b/examples/express_chat/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/examples/express_chat/windows/runner/resources/app_icon.ico b/examples/express_chat/windows/runner/resources/app_icon.ico new file mode 100644 index 000000000..c04e20caf Binary files /dev/null and b/examples/express_chat/windows/runner/resources/app_icon.ico differ diff --git a/examples/express_chat/windows/runner/runner.exe.manifest b/examples/express_chat/windows/runner/runner.exe.manifest new file mode 100644 index 000000000..153653e8d --- /dev/null +++ b/examples/express_chat/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/examples/express_chat/windows/runner/utils.cpp b/examples/express_chat/windows/runner/utils.cpp new file mode 100644 index 000000000..3f2acf58e --- /dev/null +++ b/examples/express_chat/windows/runner/utils.cpp @@ -0,0 +1,69 @@ +// 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. + +#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(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/examples/express_chat/windows/runner/utils.h b/examples/express_chat/windows/runner/utils.h new file mode 100644 index 000000000..c3891dbcb --- /dev/null +++ b/examples/express_chat/windows/runner/utils.h @@ -0,0 +1,23 @@ +// 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. + +#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/examples/express_chat/windows/runner/win32_window.cpp b/examples/express_chat/windows/runner/win32_window.cpp new file mode 100644 index 000000000..fffe7f693 --- /dev/null +++ b/examples/express_chat/windows/runner/win32_window.cpp @@ -0,0 +1,292 @@ +// 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. + +#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/examples/express_chat/windows/runner/win32_window.h b/examples/express_chat/windows/runner/win32_window.h new file mode 100644 index 000000000..9ee3eb04f --- /dev/null +++ b/examples/express_chat/windows/runner/win32_window.h @@ -0,0 +1,106 @@ +// 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. + +#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/examples/simple_chat/linux/flutter/generated_plugins.cmake b/examples/simple_chat/linux/flutter/generated_plugins.cmake index 8e2a1900c..a2eef970f 100644 --- a/examples/simple_chat/linux/flutter/generated_plugins.cmake +++ b/examples/simple_chat/linux/flutter/generated_plugins.cmake @@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/examples/simple_chat/macos/Flutter/GeneratedPluginRegistrant.swift b/examples/simple_chat/macos/Flutter/GeneratedPluginRegistrant.swift index ad1073ab5..7b265476e 100644 --- a/examples/simple_chat/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/examples/simple_chat/macos/Flutter/GeneratedPluginRegistrant.swift @@ -12,5 +12,5 @@ import video_player_avfoundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) - FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) + VideoPlayerPlugin.register(with: registry.registrar(forPlugin: "VideoPlayerPlugin")) } diff --git a/examples/simple_chat/windows/flutter/generated_plugins.cmake b/examples/simple_chat/windows/flutter/generated_plugins.cmake index 97b61367a..158064786 100644 --- a/examples/simple_chat/windows/flutter/generated_plugins.cmake +++ b/examples/simple_chat/windows/flutter/generated_plugins.cmake @@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/examples/verdure/client/linux/flutter/generated_plugins.cmake b/examples/verdure/client/linux/flutter/generated_plugins.cmake index 04f81f4b4..ac700e247 100644 --- a/examples/verdure/client/linux/flutter/generated_plugins.cmake +++ b/examples/verdure/client/linux/flutter/generated_plugins.cmake @@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/examples/verdure/client/macos/Flutter/GeneratedPluginRegistrant.swift b/examples/verdure/client/macos/Flutter/GeneratedPluginRegistrant.swift index 074b04b4c..46b142243 100644 --- a/examples/verdure/client/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/examples/verdure/client/macos/Flutter/GeneratedPluginRegistrant.swift @@ -16,5 +16,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) - FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) + VideoPlayerPlugin.register(with: registry.registrar(forPlugin: "VideoPlayerPlugin")) } diff --git a/packages/genui/lib/src/catalog/basic_catalog_widgets/column.dart b/packages/genui/lib/src/catalog/basic_catalog_widgets/column.dart index 6c92f537b..c50829caa 100644 --- a/packages/genui/lib/src/catalog/basic_catalog_widgets/column.dart +++ b/packages/genui/lib/src/catalog/basic_catalog_widgets/column.dart @@ -17,6 +17,11 @@ const _verticalColumnSpacing = 8.0; final _schema = S.object( description: 'A layout widget that arranges its children vertically.', properties: { + 'children': A2uiSchemas.componentArrayReference( + description: + 'Either an explicit list of widget IDs for the children, or a ' + 'template with a data binding to the list of children.', + ), 'justify': S.string( description: 'How children are aligned on the main axis. ', enumValues: [ @@ -33,11 +38,6 @@ final _schema = S.object( description: 'How children are aligned on the cross axis. ', enumValues: ['start', 'center', 'end', 'stretch'], ), - 'children': A2uiSchemas.componentArrayReference( - description: - 'Either an explicit list of widget IDs for the children, or a ' - 'template with a data binding to the list of children.', - ), }, required: ['children'], ); diff --git a/packages/genui/lib/src/engine/surface_controller.dart b/packages/genui/lib/src/engine/surface_controller.dart index 69731d290..b7111d6d5 100644 --- a/packages/genui/lib/src/engine/surface_controller.dart +++ b/packages/genui/lib/src/engine/surface_controller.dart @@ -108,6 +108,9 @@ interface class SurfaceController implements SurfaceHost, A2uiMessageSink { } on A2uiValidationException catch (e) { genUiLogger.warning('Validation failed for surface ${e.surfaceId}: $e'); reportError(e, StackTrace.current); + // ignore: avoid_catching_errors + } on Error { + rethrow; } catch (exception, stackTrace) { genUiLogger.severe( 'Error handling message: $message', diff --git a/packages/genui_express/.gitignore b/packages/genui_express/.gitignore new file mode 100644 index 000000000..b9d7f25b9 --- /dev/null +++ b/packages/genui_express/.gitignore @@ -0,0 +1,33 @@ +# 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 +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.flutter-plugins-dependencies +/build/ +/coverage/ diff --git a/packages/genui_express/.metadata b/packages/genui_express/.metadata new file mode 100644 index 000000000..26db2dc0f --- /dev/null +++ b/packages/genui_express/.metadata @@ -0,0 +1,39 @@ +# 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: "559ffa3f75e7402d65a8def9c28389a9b2e6fe42" + channel: "stable" + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + - platform: android + create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + - platform: ios + create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + - platform: macos + create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + - platform: web + create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + + # 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/packages/genui_express/CHANGELOG.md b/packages/genui_express/CHANGELOG.md new file mode 100644 index 000000000..bfadce11b --- /dev/null +++ b/packages/genui_express/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.1.0 + +* Initial release of genui_express package. diff --git a/packages/genui_express/DESIGN.md b/packages/genui_express/DESIGN.md new file mode 100644 index 000000000..89d61dbac --- /dev/null +++ b/packages/genui_express/DESIGN.md @@ -0,0 +1,345 @@ +# Design specification: local generative UI with A2UI Express and Genkit + +This document outlines the design for the `genui_express` Flutter package. The package integrates the A2UI Express compiler with the official Google Genkit Dart framework (`package:genkit`) and platform-specific on-device LLM engines to enable local Generative UI (GenUI) experiences. + +## Architecture overview + +The `genui_express` package serves as the coordinator between: +1. The **A2UI Express compiler**: translates simplified, human-writable layout DSL structures generated by Genkit models into standard A2UI JSON specifications. +2. The **Express prompt builder**: dynamically inspects component catalogs and generates compact A2UI Express grammar contracts to instruct local LLM engines. +3. The **Genkit model plugins**: registers native hardware-accelerated on-device models (Apple FoundationModels, Android AI Edge SDK, and Chrome Prompt API) as standard Genkit `Model` resources. +4. The **GenUI core transport system**: exposes standard Dart stream interfaces (`Transport`) that integrate with a `SurfaceController` to render interactive UI surfaces. + +The following component diagram illustrates this flow: + +```mermaid +graph TD + App[Flutter application] --> Session[Chat session] + Session --> Host[SurfaceHost] + Session --> Controller[SurfaceController] + Session --> Transport[ExpressLocalTransport] + + Transport --> Compiler[ExpressCompiler] + Transport --> Genkit[package:genkit] + + Genkit -.-> iOS[Genkit plugin: iOS/macOS FoundationModels] + Genkit -.-> Android[Genkit plugin: Android AI Edge SDK] + Genkit -.-> Chrome[Genkit plugin: Chrome Prompt API] + Genkit -.-> Fallback[Genkit plugin: Local HTTP completions] +``` + +## Interface contracts + +Following the Contract-First Design principles, the package defines decoupled, intent-revealing interfaces for the compiler, prompt builder, native Genkit model plugins, and transport coordinator. + +### Express compiler interface + +The compiler class encapsulates parsing and validation logic to build standard A2UI envelopes. + +```dart +/// A high-performance compiler that converts A2UI Express scripts into +/// valid A2UI envelopes. +class ExpressCompiler { + /// Creates an [ExpressCompiler] instance configured with [catalog]. + ExpressCompiler(Catalog catalog); + + /// Compiles A2UI Express script [dslText] into a flat JSON-compatible + /// envelope structure. + Map compile( + String dslText, { + String surfaceId = 'default_surface', + String catalogId = '', + }); +} +``` + +### Express prompt builder interface + +To guide the LLM in generating appropriate layout trees, we define a dedicated `ExpressPromptBuilder` that extends and conforms to GenUI's core `PromptBuilder` interface. This allows developers to use it interchangeably in any existing GenUI initialization loops. + +Traditional A2UI prompt builders write large JSON schemas to system prompts. Because local, on-device models (like Gemini Nano or Apple Intelligence) operate under smaller context windows and strict performance budgets, `ExpressPromptBuilder` converts catalog properties into compact, single-line positional signatures. This decreases token usage and improves inference speed. + +```dart +import 'package:genui/genui.dart'; + +/// A prompt builder for A2UI Express that instructs the AI on outputting +/// layout trees in compact Express DSL syntax. +abstract class ExpressPromptBuilder implements PromptBuilder { + /// Creates an [ExpressPromptBuilder] configured for a typical chat session. + factory ExpressPromptBuilder.chat({ + required Catalog catalog, + Iterable systemPromptFragments = const [], + String importancePrefix = PromptBuilder.defaultImportancePrefix, + Map? clientDataModel, + }); + + /// Creates an [ExpressPromptBuilder] with full control over operations, + /// capabilities, and custom fragments. + factory ExpressPromptBuilder.custom({ + required Catalog catalog, + required SurfaceOperations allowedOperations, + Iterable systemPromptFragments = const [], + String importancePrefix = PromptBuilder.defaultImportancePrefix, + TechnicalPossibilities technicalPossibilities = const TechnicalPossibilities(), + Map? clientDataModel, + }); +} +``` + +* **Positional Contract Generation**: The prompt builder dynamically introspects the `Catalog` and maps the properties in schema order. It lists required parameters and optional parameters (denoted with a `?` suffix) to generate compact documentation block signatures: + * *Component Signature*: `• TextField(label, value, placeholder?, type?, checks?)` + * *Function Signature*: `• required(value)` +* **Grammar Guidelines**: The generated prompt contract directs the model to output layouts inside the `` and `` sentinel tags, following clear single-variable assignments per line, with a single root element designated by the `root` variable. + +### Genkit custom model registration + +Instead of defining custom ad-hoc interfaces, we register each native platform model as a standard Genkit model using `defineModel` from `package:genkit/plugin.dart`. + +```dart +import 'package:genkit/genkit.dart'; + +/// Model reference for the on-device Apple FoundationModels engine. +final foundationModelRef = ModelReference( + name: 'local/apple-foundation-models', +); + +/// Model reference for the Android AI Edge SDK Gemini Nano engine. +final aiEdgeSdkModelRef = ModelReference( + name: 'local/android-ai-edge', +); + +/// Model reference for the developer HTTP completion engine. +final fallbackModelRef = ModelReference( + name: 'local/http-completion', +); +``` + +### Local transport interface + +The transport implementation coordinates the Genkit generation request and compiler execution, conforming directly to GenUI's `Transport` contract so it can be plugged into standard `SurfaceController` pipelines. + +```dart +/// A [Transport] implementation that coordinates local Genkit LLM inference +/// and compiles the output using A2UI Express. +class ExpressLocalTransport implements Transport { + /// The core Genkit engine instance. + final Genkit ai; + + /// The target Genkit model to invoke. + final ModelReference model; + + /// The A2UI Express compiler instance. + final ExpressCompiler compiler; + + /// The component catalog used for compilation maps. + final Catalog catalog; + + ExpressLocalTransport({ + required this.ai, + required this.model, + required this.catalog, + }) : compiler = ExpressCompiler(catalog); + + @override + Stream get incomingText => ...; + + @override + Stream get incomingMessages => ...; + + @override + Future sendRequest(ChatMessage message) async { ... } + + @override + void dispose() { ... } +} +``` + +## Platform-specific on-device Genkit plugins + +Each native platform engine is wrapped in a custom Genkit plugin that executes local inference via platform channels. + +### iOS and macOS: FoundationModels plugin + +For Apple platforms (iOS 18+ and macOS 15+), the plugin registers a custom Genkit model that communicates with the native Swift `NaturalLanguage.LanguageModelSession` (Apple Intelligence) using `MethodChannel` and `EventChannel`. + +* **Swift API**: + ```swift + import NaturalLanguage + + func checkAvailability() -> Bool { + return LanguageModelSession.hasCapability(.textGeneration) + } + + func generateStream(prompt: String, systemPrompt: String?, completion: @escaping (String) -> Void) async throws { + var config = LanguageModelSession.Configuration() + if let system = systemPrompt { + config.systemPrompt = system + } + let session = try await LanguageModelSession.create(configuration: config) + let stream = try await session.generateResponse(for: prompt) + for try await chunk in stream { + completion(chunk) + } + } + ``` +* **Dart Genkit model registration**: + ```dart + import 'package:genkit/genkit.dart'; + import 'package:genkit/plugin.dart'; + + final foundationModelsPlugin = definePlugin( + 'foundationModels', + (ai) { + ai.defineModel( + name: 'local/apple-foundation-models', + fn: (request, streamController) async { + // Exposes a streaming generator that feeds native EventChannel chunks + // back to Genkit. + }, + ); + }, + ); + ``` + +### Android: Google Play Services AI Edge SDK plugin + +For Android, the plugin wraps `AICore` (Gemini Nano) via the Play Services AI Edge client libraries, exposing it as a standard Genkit model. + +* **Kotlin API**: + ```kotlin + import com.google.android.gms.ai.AiFeatureManager + import com.google.android.gms.ai.generativeai.GenerativeModel + + fun checkAvailability(context: Context): Boolean { + val manager = AiFeatureManager.create(context) + return manager.isFeatureAvailable(AiFeatureManager.FEATURE_GEMINI_NANO) + } + + suspend fun generateStream(prompt: String, systemPrompt: String?, onChunk: (String) -> Void) { + val builder = GenerativeModel.Builder() + .setModelName("gemini-nano") + systemPrompt?.let { builder.setSystemInstruction(it) } + val model = builder.build() + model.generateContentStream(prompt).collect { chunk -> + onChunk(chunk.text ?: "") + } + } + ``` +* **Dart Genkit model registration**: + ```dart + import 'package:genkit/genkit.dart'; + import 'package:genkit/plugin.dart'; + + final aiEdgePlugin = definePlugin( + 'aiEdge', + (ai) { + ai.defineModel( + name: 'local/android-ai-edge', + fn: (request, streamController) async { + // Subscribes to a native EventChannel capturing Gemini Nano's + // generateContentStream response chunks. + }, + ); + }, + ); + ``` + +### Chrome: Prompt API plugin + +For Chrome Web, we can utilize `package:genkit_chrome` directly or integrate a custom Genkit model mapping to `window.ai.languageModel`. + +* **JavaScript API**: + ```javascript + async function checkAvailability() { + if (!window.ai || !window.ai.languageModel) return false; + const caps = await window.ai.languageModel.capabilities(); + return caps.available !== 'no'; + } + + async function generateStream(prompt, systemPrompt, onChunk) { + const session = await window.ai.languageModel.create({ + systemPrompt: systemPrompt + }); + const stream = session.promptStreaming(prompt); + for await (const chunk of stream) { + onChunk(chunk); + } + session.destroy(); + } + ``` + +### Developer fallback: local HTTP completion plugin + +To provide a stable developer workflow without target device hardware constraints, we register a local HTTP model that connects to Ollama, LM Studio, or a local MLX server running on `localhost`. + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit/plugin.dart'; + +final localHttpPlugin = definePlugin( + 'localHttp', + (ai) { + ai.defineModel( + name: 'local/http-completion', + fn: (request, streamController) async { + // Contacts http://localhost:11434/v1/chat/completions via HttpClient + // and streams OpenAI Server-Sent Events (SSE) chunks back to Genkit. + }, + ); + }, +); +``` + +## Usage walkthrough + +A typical Flutter application initializes Genkit with the appropriate native plugins, sets up the custom `ExpressLocalTransport`, and connects it to A2UI's `SurfaceController`: + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genui_express/genui_express.dart'; + +void main() async { + // 1. Initialize Genkit with the on-device platform plugins + final ai = Genkit( + plugins: [ + foundationModelsPlugin, + aiEdgePlugin, + localHttpPlugin, + ], + ); + + // 2. Resolve appropriate model reference + final model = await resolveLocalModel(ai); + + // 3. Initialize component catalog and A2UI controller + final catalog = CustomCatalog(); + final surfaceController = SurfaceController(catalogs: [catalog]); + + // 4. Create A2UI Express prompt builder and transport wrapping Genkit + final promptBuilder = ExpressPromptBuilder.chat( + catalog: catalog, + systemPromptFragments: [ + "You are a helpful offline assistant.", + ], + ); + + final transport = ExpressLocalTransport( + ai: ai, + model: model, + catalog: catalog, + ); + + // 5. Connect streams + transport.incomingMessages.listen(surfaceController.handleMessage); + transport.incomingText.listen((chunk) { + // Render streaming text bubble in chat list + }); + + // 6. Pre-populate the system instruction via Genkit / A2UI session flow + final systemPrompt = promptBuilder.systemPromptJoined(); + // (Configure Genkit request system prompts accordingly) + + // 7. Execute requests + await transport.sendRequest(ChatMessage.user("Find beginner climbing spots")); +} +``` diff --git a/packages/genui_express/LICENSE b/packages/genui_express/LICENSE new file mode 100644 index 000000000..650b89564 --- /dev/null +++ b/packages/genui_express/LICENSE @@ -0,0 +1,26 @@ +Copyright 2025 The Flutter Authors. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/packages/genui_express/README.md b/packages/genui_express/README.md new file mode 100644 index 000000000..e6039b87e --- /dev/null +++ b/packages/genui_express/README.md @@ -0,0 +1,58 @@ +# genui_express + +Integrates A2UI Express compilation and local on-device AI engines with GenUI. + +This package provides a transport layer that orchestrates offline Genkit model streams, maintains conversational session history, and compiles layout specifications using the A2UI Express layout DSL. + +## Getting started + +Add the package to your `pubspec.yaml`: + +```yaml +dependencies: + genui_express: ^0.1.0 +``` + +Make sure you also have `package:genui` and `package:genkit` configured in your project. + +## Usage + +The core class in this package is `ExpressLocalTransport`, which coordinates Genkit stream generation and A2UI Express compilation. + +### Code example + +Below is an example demonstrating how to set up a chat session using `ExpressLocalTransport` and `SurfaceController`: + +```dart +import 'package:genkit/genkit.dart' as genkit; +import 'package:genui/genui.dart'; +import 'package:genui_express/genui_express.dart'; + +void setupChat() { + // Initialize the local AI engine using Genkit + final ai = genkit.Genkit(isDevEnv: false); + + // Reference the local inference model + final model = genkit.modelRef('local/http-completion'); + + // Define your component catalog + final catalog = Catalog( + systemPromptFragments: [], + items: [], + ); + + // Set up the express local transport + final transport = ExpressLocalTransport( + ai: ai, + model: model, + catalog: catalog, + ); + + // Bind to the surface controller to handle UI rendering + final controller = SurfaceController(catalogs: [catalog]); + transport.incomingMessages.listen(controller.handleMessage); + + // Send a prompt to the local AI session + transport.sendRequest(ChatMessage.user('Compare Kraft Boulders and Lone Mountain climbing sites.')); +} +``` diff --git a/packages/genui_express/android/.gitignore b/packages/genui_express/android/.gitignore new file mode 100644 index 000000000..161bdcdaf --- /dev/null +++ b/packages/genui_express/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/packages/genui_express/android/build.gradle.kts b/packages/genui_express/android/build.gradle.kts new file mode 100644 index 000000000..5cbfec8e4 --- /dev/null +++ b/packages/genui_express/android/build.gradle.kts @@ -0,0 +1,77 @@ +group = "com.example.genui_express" +version = "1.0-SNAPSHOT" + +buildscript { + val kotlinVersion = "2.3.20" + repositories { + google() + mavenCentral() + } + + dependencies { + classpath("com.android.tools.build:gradle:9.0.1") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +plugins { + id("com.android.library") +} + +android { + namespace = "com.example.genui_express" + + compileSdk = 36 + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + sourceSets { + getByName("main") { + java.srcDirs("src/main/kotlin") + } + getByName("test") { + java.srcDirs("src/test/kotlin") + } + } + + defaultConfig { + minSdk = 24 + } + + testOptions { + unitTests { + isIncludeAndroidResources = true + all { + it.useJUnitPlatform() + + it.outputs.upToDateWhen { false } + + it.testLogging { + events("passed", "skipped", "failed", "standardOut", "standardError") + showStandardStreams = true + } + } + } + } +} + +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + +dependencies { + testImplementation("org.jetbrains.kotlin:kotlin-test") + testImplementation("org.mockito:mockito-core:5.0.0") +} diff --git a/packages/genui_express/android/settings.gradle.kts b/packages/genui_express/android/settings.gradle.kts new file mode 100644 index 000000000..465b04c7f --- /dev/null +++ b/packages/genui_express/android/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "genui_express" diff --git a/packages/genui_express/android/src/main/AndroidManifest.xml b/packages/genui_express/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000..2c3c7727a --- /dev/null +++ b/packages/genui_express/android/src/main/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/genui_express/android/src/main/kotlin/com/example/genui_express/GenuiExpressPlugin.kt b/packages/genui_express/android/src/main/kotlin/com/example/genui_express/GenuiExpressPlugin.kt new file mode 100644 index 000000000..bcfb6b141 --- /dev/null +++ b/packages/genui_express/android/src/main/kotlin/com/example/genui_express/GenuiExpressPlugin.kt @@ -0,0 +1,42 @@ +// 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. + +package com.example.genui_express + +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result + +/** GenuiExpressPlugin */ +class GenuiExpressPlugin : + FlutterPlugin, + MethodCallHandler { + // The MethodChannel that will the communication between Flutter and native Android + // + // This local reference serves to register the plugin with the Flutter Engine and unregister it + // when the Flutter Engine is detached from the Activity + private lateinit var channel: MethodChannel + + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "genui_express") + channel.setMethodCallHandler(this) + } + + override fun onMethodCall( + call: MethodCall, + result: Result + ) { + if (call.method == "getPlatformVersion") { + result.success("Android ${android.os.Build.VERSION.RELEASE}") + } else { + result.notImplemented() + } + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } +} diff --git a/packages/genui_express/android/src/test/kotlin/com/example/genui_express/GenuiExpressPluginTest.kt b/packages/genui_express/android/src/test/kotlin/com/example/genui_express/GenuiExpressPluginTest.kt new file mode 100644 index 000000000..71d49f167 --- /dev/null +++ b/packages/genui_express/android/src/test/kotlin/com/example/genui_express/GenuiExpressPluginTest.kt @@ -0,0 +1,31 @@ +// 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. + +package com.example.genui_express + +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import org.mockito.Mockito +import kotlin.test.Test + +/* + * This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation. + * + * Once you have built the plugin's example app, you can run these tests from the command + * line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or + * you can run them directly from IDEs that support JUnit such as Android Studio. + */ + +internal class GenuiExpressPluginTest { + @Test + fun onMethodCall_getPlatformVersion_returnsExpectedValue() { + val plugin = GenuiExpressPlugin() + + val call = MethodCall("getPlatformVersion", null) + val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java) + plugin.onMethodCall(call, mockResult) + + Mockito.verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE) + } +} diff --git a/packages/genui_express/ios/.gitignore b/packages/genui_express/ios/.gitignore new file mode 100644 index 000000000..034771fc9 --- /dev/null +++ b/packages/genui_express/ios/.gitignore @@ -0,0 +1,38 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/ephemeral/ +/Flutter/flutter_export_environment.sh diff --git a/packages/genui_express/ios/genui_express.podspec b/packages/genui_express/ios/genui_express.podspec new file mode 100644 index 000000000..22960b1ae --- /dev/null +++ b/packages/genui_express/ios/genui_express.podspec @@ -0,0 +1,29 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint genui_express.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'genui_express' + s.version = '0.0.1' + s.summary = 'A new Flutter plugin project.' + s.description = <<-DESC +A new Flutter plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'genui_express/Sources/genui_express/**/*' + s.dependency 'Flutter' + s.platform = :ios, '18.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' + + # If your plugin requires a privacy manifest, for example if it uses any + # required reason APIs, update the PrivacyInfo.xcprivacy file to describe your + # plugin's privacy impact, and then uncomment this line. For more information, + # see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files + # s.resource_bundles = {'genui_express_privacy' => ['genui_express/Sources/genui_express/PrivacyInfo.xcprivacy']} +end diff --git a/packages/genui_express/ios/genui_express/Package.swift b/packages/genui_express/ios/genui_express/Package.swift new file mode 100644 index 000000000..278914b28 --- /dev/null +++ b/packages/genui_express/ios/genui_express/Package.swift @@ -0,0 +1,40 @@ +// 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. + +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "genui_express", + platforms: [ + .iOS("18.0") + ], + products: [ + .library(name: "genui-express", targets: ["genui_express"]) + ], + dependencies: [ + .package(name: "FlutterFramework", path: "../FlutterFramework") + ], + targets: [ + .target( + name: "genui_express", + dependencies: [ + .product(name: "FlutterFramework", package: "FlutterFramework") + ], + resources: [ + // If your plugin requires a privacy manifest, for example if it uses any required + // reason APIs, update the PrivacyInfo.xcprivacy file to describe your plugin's + // privacy impact, and then uncomment these lines. For more information, see + // https://developer.apple.com/documentation/bundleresources/privacy_manifest_files + // .process("PrivacyInfo.xcprivacy"), + + // If you have other resources that need to be bundled with your plugin, refer to + // the following instructions to add them: + // https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package + ] + ) + ] +) diff --git a/packages/genui_express/ios/genui_express/Sources/genui_express/GenuiExpressPlugin.swift b/packages/genui_express/ios/genui_express/Sources/genui_express/GenuiExpressPlugin.swift new file mode 100644 index 000000000..e69de29bb diff --git a/packages/genui_express/ios/genui_express/Sources/genui_express/PrivacyInfo.xcprivacy b/packages/genui_express/ios/genui_express/Sources/genui_express/PrivacyInfo.xcprivacy new file mode 100644 index 000000000..a34b7e2e6 --- /dev/null +++ b/packages/genui_express/ios/genui_express/Sources/genui_express/PrivacyInfo.xcprivacy @@ -0,0 +1,14 @@ + + + + + NSPrivacyTrackingDomains + + NSPrivacyAccessedAPITypes + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + diff --git a/packages/genui_express/lib/genui_express.dart b/packages/genui_express/lib/genui_express.dart new file mode 100644 index 000000000..16bf6dcb9 --- /dev/null +++ b/packages/genui_express/lib/genui_express.dart @@ -0,0 +1,11 @@ +// 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. + +/// The A2UI Express compiler and local AI engine integration for GenUI. +library; + +export 'src/compiler/express_compiler.dart' show ExpressCompiler; +export 'src/plugins/on_device_plugins.dart' show GenuiExpressLocalModels; +export 'src/prompt/express_prompt_builder.dart' show ExpressPromptBuilder; +export 'src/transport/express_local_transport.dart' show ExpressLocalTransport; diff --git a/packages/genui_express/lib/genui_express_web.dart b/packages/genui_express/lib/genui_express_web.dart new file mode 100644 index 000000000..821a56b5c --- /dev/null +++ b/packages/genui_express/lib/genui_express_web.dart @@ -0,0 +1,14 @@ +// 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_web_plugins/flutter_web_plugins.dart'; + +/// The web-specific implementation of the GenuiExpress plugin. +class GenuiExpressWeb { + /// Registers this class as the web implementation of the plugin. + static void registerWith(Registrar registrar) { + // We do not need custom web method channels, but this is required by + // Flutter web compilation tools to compile the project successfully. + } +} diff --git a/packages/genui_express/lib/src/compiler/catalog_schema_helper.dart b/packages/genui_express/lib/src/compiler/catalog_schema_helper.dart new file mode 100644 index 000000000..6da591d3d --- /dev/null +++ b/packages/genui_express/lib/src/compiler/catalog_schema_helper.dart @@ -0,0 +1,131 @@ +// 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:genui/genui.dart'; + +/// Helper class that inspects active in-memory catalog schemas to map +/// positional signatures. +class CatalogSchemaHelper { + /// The wrapped component/functions [catalog]. + final Catalog catalog; + + /// Maps component names to their properties keys list in schema order. + final Map> componentProperties = {}; + + /// Maps component names to their required property keys list. + final Map> componentRequired = {}; + + /// Maps component names to whether they support checks validation rules. + final Map componentIsCheckable = {}; + + /// Maps function names to their argument properties list. + final Map> functionProperties = {}; + + /// Maps function names to their required argument keys list. + final Map> functionRequired = {}; + + /// Creates a [CatalogSchemaHelper] and triggers schema parsing for [catalog]. + CatalogSchemaHelper(this.catalog) { + _loadMappings(); + } + + /// Iterates through catalog items and functions to establish property key + /// ordering maps. + void _loadMappings() { + for (final CatalogItem item in catalog.items) { + final String name = item.name; + final Map schema = item.dataSchema.value; + + final props = {}; + final reqs = []; + var isCheckable = false; + + final subSchemas = >[schema]; + if (schema.containsKey('allOf')) { + final Object? allOf = schema['allOf']; + if (allOf is List) { + for (final Object? sub in allOf) { + if (sub is Map) { + subSchemas.add(sub); + } + } + } + } + + for (final sub in subSchemas) { + if (sub.containsKey(r'$ref')) { + final ref = sub[r'$ref'] as String; + if (ref.contains('Checkable')) { + isCheckable = true; + } + } + if (sub.containsKey('properties')) { + final Object? p = sub['properties']; + if (p is Map) { + props.addAll(p); + } + } + if (sub.containsKey('required')) { + final Object? r = sub['required']; + if (r is List) { + reqs.addAll(r.cast()); + } + } + } + + final orderedKeys = []; + for (final String k in props.keys) { + if (k != 'component' && k != 'id') { + orderedKeys.add(k); + } + } + + if (isCheckable) { + orderedKeys.add('checks'); + } + + componentProperties[name] = orderedKeys; + componentRequired[name] = reqs; + componentIsCheckable[name] = isCheckable; + } + + for (final ClientFunction func in catalog.functions) { + final String name = func.name; + final Map schema = + func.argumentSchema.value as Map? ?? const {}; + final Map props = + schema['properties'] as Map? ?? const {}; + final List reqs = + schema['required'] as List? ?? const []; + + final orderedKeys = []; + orderedKeys.addAll(props.keys); + + final requiredKeys = []; + requiredKeys.addAll(reqs.cast()); + + functionProperties[name] = orderedKeys; + functionRequired[name] = requiredKeys; + } + } + + /// Returns the properties list in schema declaration order for [name]. + List getComponentProperties(String name) => + componentProperties[name] ?? const []; + + /// Returns the required property keys list for component [name]. + List getComponentRequired(String name) => + componentRequired[name] ?? const []; + + /// Returns whether component [name] supports check validation rules. + bool isCheckable(String name) => componentIsCheckable[name] ?? false; + + /// Returns the argument properties list in schema order for function [name]. + List getFunctionProperties(String name) => + functionProperties[name] ?? const []; + + /// Returns the required argument keys list for function [name]. + List getFunctionRequired(String name) => + functionRequired[name] ?? const []; +} diff --git a/packages/genui_express/lib/src/compiler/express_compiler.dart b/packages/genui_express/lib/src/compiler/express_compiler.dart new file mode 100644 index 000000000..a44b5ba0a --- /dev/null +++ b/packages/genui_express/lib/src/compiler/express_compiler.dart @@ -0,0 +1,497 @@ +// 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:collection/collection.dart'; +import 'package:genui/genui.dart'; + +import 'catalog_schema_helper.dart'; +import 'parser.dart'; +import 'token.dart'; + +/// A high-performance compiler that converts A2UI Express DSL scripts into +/// valid A2UI envelopes. +class ExpressCompiler { + /// The catalog schema helper holding property mapping configurations. + final CatalogSchemaHelper helper; + + /// Creates an [ExpressCompiler] instance mapping against [catalog]. + ExpressCompiler(Catalog catalog) : helper = CatalogSchemaHelper(catalog); + + /// Compiles A2UI Express script [dslText] into a flat JSON-compatible + /// envelope structure. + /// + /// Returns a `Map` containing `createSurface` and the + /// compiled flat components array. + Map compile( + String dslText, { + String surfaceId = 'default_surface', + String catalogId = '', + }) { + final List lines = dslText + .split('\n') + .map((e) => e.trim()) + .where((e) => e.isNotEmpty) + .toList(); + + final List statements = []; + StringBuffer? currentStatement; + final assignmentStartRegex = RegExp( + r'^(?:[a-zA-Z_][a-zA-Z0-9_-]*|\$[a-zA-Z0-9_/]+)\s*=', + ); + + for (final line in lines) { + if (line.contains('') || line.contains('')) { + continue; + } + if (assignmentStartRegex.hasMatch(line)) { + if (currentStatement != null) { + statements.add(currentStatement.toString().trim()); + } + currentStatement = StringBuffer(line); + } else { + if (currentStatement != null) { + currentStatement.write('\n$line'); + } + } + } + if (currentStatement != null) { + statements.add(currentStatement.toString().trim()); + } + + final Map rawSymbols = {}; + final Map pathAssignments = {}; + + for (final stmt in statements) { + if (!stmt.contains('=')) { + continue; + } + final int index = stmt.indexOf('='); + final String leftHandSide = stmt.substring(0, index).trim(); + final String exprText = stmt.substring(index + 1).trim(); + + try { + final List tokens = tokenize(exprText); + final parser = TokenParser(tokens); + final Object? parsedExpr = parser.parseExpression(); + + if (leftHandSide.startsWith(r'$/')) { + pathAssignments[leftHandSide] = parsedExpr; + } else { + rawSymbols[leftHandSide] = parsedExpr; + } + } catch (e) { + // Recover gracefully: register dummy loading text for the failed + // branch. + rawSymbols[leftHandSide] = { + 'call': 'Text', + 'args': ['Loading...'], + }; + } + } + + final List> compiledComponents = []; + + if (!rawSymbols.containsKey('root')) { + if (rawSymbols.isNotEmpty) { + final String lastKey = rawSymbols.keys.last; + final Object? lastVal = rawSymbols.remove(lastKey); + rawSymbols['root'] = lastVal; + } else { + throw ArgumentError( + "A2UI Express source must define a 'root' variable or at least " + 'one component.', + ); + } + } + + for (final MapEntry entry in rawSymbols.entries) { + final String varName = entry.key; + final Object? ast = entry.value; + final Map? compDict = _compileAstNode( + varName, + ast, + rawSymbols, + compiledComponents, + ); + if (compDict != null) { + compiledComponents.add(compDict); + } + } + + final Map dataModelAccumulator = {}; + for (final MapEntry entry in pathAssignments.entries) { + final String pathKey = entry.key.substring(2); // strip $/ + final Object? evaluated = _compileValue( + entry.value, + rawSymbols, + compiledComponents, + ); + _setValueAtPath(dataModelAccumulator, pathKey, evaluated); + } + + final String resolvedCatalogId = catalogId.isNotEmpty + ? catalogId + : (helper.catalog.catalogId ?? 'https://a2ui.org/catalog.json'); + + return { + 'version': 'v0.9', + 'createSurface': { + 'surfaceId': surfaceId, + 'catalogId': resolvedCatalogId, + 'components': compiledComponents, + 'dataModel': dataModelAccumulator, + }, + }; + } + + /// Compiles an individual variable's AST node into flat component + /// dictionary format. + Map? _compileAstNode( + String varName, + Object? ast, + Map rawSymbols, + List> compiledComponents, + ) { + if (ast is! Map || !ast.containsKey('call')) { + return null; + } + + final compName = ast['call'] as String; + final args = ast['args'] as List; + + if (!helper.componentProperties.containsKey(compName)) { + // Not a component, could be a standalone action/helper; skip writing + // as component. + return null; + } + + final List properties = helper.getComponentProperties(compName); + final Map compDict = { + 'id': varName, + 'component': compName, + }; + + Object? siblingValuePath; + + // First pass: map basic properties + for (var idx = 0; idx < args.length; idx++) { + if (idx >= properties.length) { + break; + } + final String propName = properties[idx]; + if (propName == 'checks') { + continue; // Compile checks in second pass + } + + Object? mappedVal = _compileValue( + args[idx], + rawSymbols, + compiledComponents, + isAction: propName == 'action' || propName == 'submitAction', + ); + + // Resilient Auto-Wrapping of string literals in component slots + if ((propName == 'child' || + propName == 'trigger' || + propName == 'content') && + mappedVal is String && + !mappedVal.startsWith('inline_') && + !mappedVal.startsWith('txt_') && + !rawSymbols.containsKey(mappedVal)) { + final syntheticId = 'txt_${varName}_$idx'; + compiledComponents.add({ + 'id': syntheticId, + 'component': 'Text', + 'text': mappedVal, + }); + mappedVal = syntheticId; + } + + if (propName == 'children' && mappedVal is List) { + final List newChildren = []; + for (var cIdx = 0; cIdx < mappedVal.length; cIdx++) { + final Object? item = mappedVal[cIdx]; + if (item is String && + !item.startsWith('inline_') && + !item.startsWith('txt_') && + !rawSymbols.containsKey(item)) { + final syntheticId = 'txt_${varName}_c$cIdx'; + compiledComponents.add({ + 'id': syntheticId, + 'component': 'Text', + 'text': item, + }); + newChildren.add(syntheticId); + } else { + newChildren.add(item); + } + } + mappedVal = newChildren; + } + + if ((propName == 'action' || propName == 'submitAction') && + mappedVal is String && + !mappedVal.startsWith('inline_') && + !mappedVal.startsWith('txt_') && + !rawSymbols.containsKey(mappedVal)) { + mappedVal = { + 'event': { + 'name': mappedVal, + 'context': const {}, + }, + }; + } + + compDict[propName] = mappedVal; + + if (propName == 'value' && + mappedVal is Map && + mappedVal.containsKey('path')) { + siblingValuePath = mappedVal; + } + } + + // Second pass: compile checks with implicit path injection + for (var idx = 0; idx < args.length; idx++) { + if (idx >= properties.length) { + break; + } + final String propName = properties[idx]; + if (propName == 'checks') { + final List> compiledChecks = []; + final List rawChecks = args[idx] is List + ? args[idx] as List + : [args[idx]]; + + for (final rc in rawChecks) { + if (rc is Map && rc.containsKey('check')) { + final checkName = rc['check'] as String; + final checkArgs = rc['args'] as List; + final Map compiledArgs = {}; + + final List checkProps = helper.getFunctionProperties( + checkName, + ); + var messageVal = + '${checkName[0].toUpperCase()}${checkName.substring(1)} ' + 'check failed'; + + final explicitArgs = List.from(checkArgs); + var isValueInjected = false; + + // Handle implicit target 'value' injection + if (checkProps.isNotEmpty && checkProps[0] == 'value') { + if (explicitArgs.isNotEmpty && + explicitArgs[0] is Map && + (explicitArgs[0] as Map).containsKey( + 'path', + )) { + // already has a path, do nothing + } else { + if (siblingValuePath != null) { + compiledArgs['value'] = siblingValuePath; + isValueInjected = true; + } + } + } + + final startPropIdx = isValueInjected ? 1 : 0; + + for (var cIdx = 0; cIdx < explicitArgs.length; cIdx++) { + final int propTargetIdx = cIdx + startPropIdx; + if (propTargetIdx < checkProps.length) { + compiledArgs[checkProps[propTargetIdx]] = _compileValue( + explicitArgs[cIdx], + rawSymbols, + compiledComponents, + ); + } else { + if (explicitArgs[cIdx] is String) { + messageVal = explicitArgs[cIdx] as String; + } + } + } + + compiledChecks.add({ + 'condition': {'call': checkName, 'args': compiledArgs}, + 'message': messageVal, + }); + } + } + compDict['checks'] = compiledChecks; + } + } + + return compDict; + } + + /// Compiles an individual AST node value into valid A2UI equivalents. + Object? _compileValue( + Object? val, + Map rawSymbols, + List> compiledComponents, { + bool isAction = false, + }) { + if (val is Map) { + if (val.containsKey('path')) { + return val; + } + if (val.containsKey('variable')) { + // Resolve variable ID + return val['variable']; + } + if (val.containsKey('call')) { + final fnName = val['call'] as String; + final fnArgs = val['args'] as List; + + // If it is a component call, auto-flatten it! + if (helper.componentProperties.containsKey(fnName)) { + final syntheticId = 'inline_${fnName}_${compiledComponents.length}'; + final Map? inlineComp = _compileAstNode( + syntheticId, + val, + rawSymbols, + compiledComponents, + ); + if (inlineComp != null) { + compiledComponents.add(inlineComp); + } + return syntheticId; + } + + // Is it a reserved Template signature? + if (fnName == 'Template') { + final pathVal = + _compileValue( + fnArgs[0], + rawSymbols, + compiledComponents, + isAction: isAction, + ) + as Map; + final Object? compIdVal = _compileValue( + fnArgs[1], + rawSymbols, + compiledComponents, + isAction: isAction, + ); + return {'path': pathVal['path'], 'componentId': compIdVal}; + } + + // Is it a reserved Event signature? + if (fnName == 'Event') { + final eventName = fnArgs.isNotEmpty ? fnArgs[0] as String : ''; + final contextMap = fnArgs.length > 1 + ? fnArgs[1] as Map + : const {}; + final Map compiledContext = {}; + for (final MapEntry entry in contextMap.entries) { + compiledContext[entry.key] = _compileValue( + entry.value, + rawSymbols, + compiledComponents, + isAction: isAction, + ); + } + return { + 'event': {'name': eventName, 'context': compiledContext}, + }; + } + + // Is it a regular catalog function? + if (helper.functionProperties.containsKey(fnName)) { + final List fnProps = helper.getFunctionProperties(fnName); + final Map compiledArgs = {}; + for (var idx = 0; idx < fnArgs.length; idx++) { + if (idx < fnProps.length) { + compiledArgs[fnProps[idx]] = _compileValue( + fnArgs[idx], + rawSymbols, + compiledComponents, + isAction: isAction, + ); + } + } + + // Wrap in functionCall only if inside an action field + if (isAction) { + return { + 'functionCall': {'call': fnName, 'args': compiledArgs}, + }; + } + + // Compile direct dynamic function call expression (with returnType!) + final Map resExpr = { + 'call': fnName, + 'args': compiledArgs, + }; + // Read returnType from catalog definition if present + final ClientFunction? fnDef = helper.catalog.functions + .firstWhereOrNull((f) => f.name == fnName); + final String? returnTypeConst = fnDef?.returnType.value; + if (returnTypeConst != null) { + resExpr['returnType'] = returnTypeConst; + } + return resExpr; + } + + // Fallback + return { + 'call': fnName, + 'args': fnArgs + .map( + (a) => _compileValue( + a, + rawSymbols, + compiledComponents, + isAction: isAction, + ), + ) + .toList(), + }; + } + + return val.map( + (k, v) => MapEntry( + k, + _compileValue(v, rawSymbols, compiledComponents, isAction: isAction), + ), + ); + } + + if (val is List) { + return val + .map( + (item) => _compileValue( + item, + rawSymbols, + compiledComponents, + isAction: isAction, + ), + ) + .toList(); + } + + return val; + } + + /// Helper method to structuredly set values at a nested JSON [path]. + void _setValueAtPath(Map map, String path, Object? value) { + final List keys = path + .split('/') + .where((k) => k.isNotEmpty) + .toList(); + if (keys.isEmpty) return; + + var current = map; + for (var i = 0; i < keys.length - 1; i++) { + final String key = keys[i]; + if (!current.containsKey(key) || current[key] is! Map) { + current[key] = {}; + } + current = current[key] as Map; + } + current[keys.last] = value; + } +} diff --git a/packages/genui_express/lib/src/compiler/parser.dart b/packages/genui_express/lib/src/compiler/parser.dart new file mode 100644 index 000000000..5be881e9d --- /dev/null +++ b/packages/genui_express/lib/src/compiler/parser.dart @@ -0,0 +1,169 @@ +// 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 'token.dart'; + +/// A recursive-descent parser that parses a list of [Token] objects into an +/// AST. +class TokenParser { + /// The sequence of scanned tokens. + final List tokens; + + /// The current parsing index pointer. + int pos = 0; + + /// Creates a [TokenParser] wrapping a token stream. + TokenParser(this.tokens); + + /// Inspects the current token at the pointer without consuming it. + Token? peek() { + if (pos < tokens.length) { + return tokens[pos]; + } + return null; + } + + /// Consumes and returns the current token, optionally validating its [kind]. + /// + /// Throws a [FormatException] if the pointer is at the end of input or + /// if the token category mismatch. + Token consume([TokenKind? kind]) { + final Token? tok = peek(); + if (tok == null) { + throw const FormatException('Unexpected end of input'); + } + if (kind != null && tok.kind != kind) { + throw FormatException( + 'Expected ${kind.name}, got ${tok.kind.name}: ${tok.text}', + ); + } + pos++; + return tok; + } + + /// Parses the current expression subtree recursively. + /// + /// Supports arrays, paths, check rules, function calls, and primitive values. + Object? parseExpression() { + final Token? tok = peek(); + if (tok == null) { + throw const FormatException('Expected expression'); + } + + final TokenKind kind = tok.kind; + if (kind == TokenKind.lbracket) { + return parseArray(); + } + if (kind == TokenKind.path) { + consume(); + return {'path': (tok.value as String).substring(1)}; + } + if (kind == TokenKind.check) { + return parseCheck(); + } + if (kind == TokenKind.identifier) { + consume(); + final Token? nextTok = peek(); + if (nextTok != null && nextTok.kind == TokenKind.lparen) { + return parseCall(tok.value as String); + } + return {'variable': tok.value}; + } + if (kind == TokenKind.string || + kind == TokenKind.number || + kind == TokenKind.boolean || + kind == TokenKind.nullValue) { + consume(); + return tok.value; + } + throw FormatException('Unexpected token ${kind.name}: ${tok.text}'); + } + + /// Parses a comma-separated array literal enclosed in brackets `[...]`. + List parseArray() { + consume(TokenKind.lbracket); + final List items = []; + final Token? p = peek(); + if (p != null && p.kind != TokenKind.rbracket) { + items.add(parseExpression()); + while (peek() != null && peek()!.kind == TokenKind.comma) { + consume(TokenKind.comma); + items.add(parseExpression()); + } + } + consume(TokenKind.rbracket); + return items; + } + + /// Parses a client-side check rule starting with `?` (e.g. `?required` or + /// `?length(min, max)`). + Map parseCheck() { + final Token tok = consume(TokenKind.check); + final String name = (tok.value as String).substring(1); // strip ? + final Token? nextTok = peek(); + final List args = []; + if (nextTok != null && nextTok.kind == TokenKind.lparen) { + consume(TokenKind.lparen); + final Token? p = peek(); + if (p != null && p.kind != TokenKind.rparen) { + args.add(parseExpression()); + while (peek() != null && peek()!.kind == TokenKind.comma) { + consume(TokenKind.comma); + args.add(parseExpression()); + } + } + consume(TokenKind.rparen); + } + return {'check': name, 'args': args}; + } + + /// Parses a function call expression (e.g. `ComponentName(args)` or + /// `FunctionName(args)`). + Map parseCall(String name) { + consume(TokenKind.lparen); + final List args = []; + final Token? p = peek(); + if (p != null && p.kind != TokenKind.rparen) { + if (p.kind == TokenKind.lbrace) { + args.add(parseMap()); + } else { + args.add(parseExpression()); + } + + while (peek() != null && peek()!.kind == TokenKind.comma) { + consume(TokenKind.comma); + final Token? nextP = peek(); + if (nextP != null && nextP.kind == TokenKind.lbrace) { + args.add(parseMap()); + } else { + args.add(parseExpression()); + } + } + } + consume(TokenKind.rparen); + return {'call': name, 'args': args}; + } + + /// Parses a key-value map literal enclosed in braces `{key: value, ...}`. + Map parseMap() { + consume(TokenKind.lbrace); + final Map res = {}; + final Token? p = peek(); + if (p != null && p.kind != TokenKind.rbrace) { + final Token kTok = consume(TokenKind.identifier); + consume(TokenKind.colon); + final Object? v = parseExpression(); + res[kTok.value as String] = v; + while (peek() != null && peek()!.kind == TokenKind.comma) { + consume(TokenKind.comma); + final Token nextKTok = consume(TokenKind.identifier); + consume(TokenKind.colon); + final Object? nextV = parseExpression(); + res[nextKTok.value as String] = nextV; + } + } + consume(TokenKind.rbrace); + return res; + } +} diff --git a/packages/genui_express/lib/src/compiler/token.dart b/packages/genui_express/lib/src/compiler/token.dart new file mode 100644 index 000000000..6fc006746 --- /dev/null +++ b/packages/genui_express/lib/src/compiler/token.dart @@ -0,0 +1,144 @@ +// 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. + +/// The token kinds supported by the A2UI Express lexer. +enum TokenKind { + /// String literal token enclosed in double quotes (e.g. `"label"`). + string, + + /// Absolute or relative path reference in the data model (starts with `$` + /// followed by alphanumeric path segments). + path, + + /// Client-side check rule validation (starts with `?` followed by the check + /// name). + check, + + /// Numeric literal token (integer or decimal). + number, + + /// Boolean literal token (`true` or `false`). + boolean, + + /// Null value literal token (`null`). + nullValue, + + /// An alphanumeric identifier representing a component or function name. + identifier, + + /// Left parenthesis token `(`. + lparen, + + /// Right parenthesis token `)`. + rparen, + + /// Left bracket token `[`. + lbracket, + + /// Right bracket token `]`. + rbracket, + + /// Comma separator token `,`. + comma, + + /// Equals assignment token `=`. + equals, + + /// Colon key-value separator token `:`. + colon, + + /// Left curly brace token `{`. + lbrace, + + /// Right curly brace token `}`. + rbrace, + + /// Whitespace token (ignored by parser). + ws, +} + +/// Represents a lexical token parsed from A2UI Express input. +class Token { + /// The category/type of this token. + final TokenKind kind; + + /// The parsed semantic value (e.g., `double`, `bool`, or stripped `String`). + final Object? value; + + /// The raw matched substring from the input source code. + final String text; + + /// Creates a lexical [Token] with its type, value, and original source text. + Token(this.kind, this.value, this.text); + + @override + String toString() => 'Token(${kind.name}, $value)'; +} + +/// Scans the input [text] and produces a flat list of scanned [Token] +/// objects. +/// +/// Throws a [FormatException] if any unrecognized character sequence is +/// encountered. +List tokenize(String text) { + final List tokens = []; + var index = 0; + + final patterns = <(TokenKind, RegExp)>[ + (TokenKind.ws, RegExp(r'^\s+')), + (TokenKind.string, RegExp(r'^"(?:[^"\\]|\\.)*"')), + (TokenKind.path, RegExp(r'^\$[a-zA-Z0-9_/]+')), + (TokenKind.check, RegExp(r'^\?[a-zA-Z_][a-zA-Z0-9_]*')), + (TokenKind.number, RegExp(r'^-?\d+(?:\.\d+)?')), + (TokenKind.boolean, RegExp(r'^\b(?:true|false)\b')), + (TokenKind.nullValue, RegExp(r'^\bnull\b')), + (TokenKind.identifier, RegExp(r'^[a-zA-Z_][a-zA-Z0-9_-]*')), + (TokenKind.lparen, RegExp(r'^\(')), + (TokenKind.rparen, RegExp(r'^\)')), + (TokenKind.lbracket, RegExp(r'^\[')), + (TokenKind.rbracket, RegExp(r'^\]')), + (TokenKind.comma, RegExp(r'^,')), + (TokenKind.equals, RegExp(r'^=')), + (TokenKind.colon, RegExp(r'^:')), + (TokenKind.lbrace, RegExp(r'^\{')), + (TokenKind.rbrace, RegExp(r'^\}')), + ]; + + while (index < text.length) { + final String substring = text.substring(index); + var matched = false; + for (final (kind, regex) in patterns) { + final RegExpMatch? match = regex.firstMatch(substring); + if (match != null) { + final String matchedText = match.group(0)!; + index += matchedText.length; + matched = true; + + if (kind == TokenKind.ws) { + break; // skip whitespace + } + + Object? value = matchedText; + if (kind == TokenKind.string) { + value = matchedText + .substring(1, matchedText.length - 1) + .replaceAll(r'\"', '"'); + } else if (kind == TokenKind.number) { + value = num.parse(matchedText); + } else if (kind == TokenKind.boolean) { + value = matchedText == 'true'; + } else if (kind == TokenKind.nullValue) { + value = null; + } + + tokens.add(Token(kind, value, matchedText)); + break; + } + } + if (!matched) { + throw FormatException('Unexpected character at index $index in "$text"'); + } + } + return tokens; +} diff --git a/packages/genui_express/lib/src/plugins/on_device_plugins.dart b/packages/genui_express/lib/src/plugins/on_device_plugins.dart new file mode 100644 index 000000000..dcf09fa9a --- /dev/null +++ b/packages/genui_express/lib/src/plugins/on_device_plugins.dart @@ -0,0 +1,226 @@ +// 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 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:flutter/services.dart'; +import 'package:genkit/genkit.dart'; + +const _channel = MethodChannel('genui_express/local_ai'); +const _eventChannel = EventChannel('genui_express/local_ai_stream'); + +/// Registers custom local on-device models with a [Genkit] instance. +class GenuiExpressLocalModels { + GenuiExpressLocalModels._(); + + /// Apple Intelligence local foundation models reference. + static const String appleFoundationModels = 'local/apple-foundation-models'; + + /// Google Android AI Edge (Gemini Nano) reference. + static const String androidAiEdge = 'local/android-ai-edge'; + + /// Developer local HTTP completions model reference. + static const String httpCompletion = 'local/http-completion'; + + /// Helper to extract the prompt (last user message) and system instruction + /// from a [ModelRequest]. + static (String prompt, String? systemInstruction) _extractInputs( + ModelRequest request, + ) { + final Message? userMessage = request.messages.lastWhereOrNull( + (m) => m.role == Role.user, + ); + final String prompt = + userMessage?.content + .where((p) => p.isText) + .map((p) => p.text) + .join('') ?? + ''; + + final Message? systemMessage = request.messages.firstWhereOrNull( + (m) => m.role == Role.system, + ); + final String? systemInstruction = systemMessage?.content + .where((p) => p.isText) + .map((p) => p.text) + .join(''); + + return (prompt, systemInstruction); + } + + /// Registers Apple Intelligence (FoundationModels), Android AI Edge (Gemini + /// Nano), and local developer HTTP models with the given [ai] instance. + static void register(Genkit ai) { + // 1. Apple Intelligence model + ai.defineModel( + name: appleFoundationModels, + fn: (request, context) async { + final (String prompt, String? systemInstruction) = _extractInputs( + request, + ); + + final bool isAvailable = + await _channel.invokeMethod('checkAvailability') ?? false; + if (!isAvailable) { + throw StateError( + 'FoundationModels framework is not available or configured on ' + 'this device.', + ); + } + + final Stream nativeStream = _eventChannel + .receiveBroadcastStream({ + 'prompt': prompt, + 'systemPrompt': systemInstruction, + }) + .cast(); + + final buffer = StringBuffer(); + await for (final chunk in nativeStream) { + buffer.write(chunk); + context.sendChunk( + ModelResponseChunk(content: [TextPart(text: chunk)]), + ); + } + + return ModelResponse( + finishReason: FinishReason.stop, + message: Message( + role: Role.model, + content: [TextPart(text: buffer.toString())], + ), + ); + }, + ); + + // 2. Android AI Edge model + ai.defineModel( + name: androidAiEdge, + fn: (request, context) async { + final (String prompt, String? systemInstruction) = _extractInputs( + request, + ); + + final bool isAvailable = + await _channel.invokeMethod('checkAvailability') ?? false; + if (!isAvailable) { + throw StateError( + 'Google AI Edge SDK is not available or configured on this ' + 'device.', + ); + } + + final Stream nativeStream = _eventChannel + .receiveBroadcastStream({ + 'prompt': prompt, + 'systemPrompt': systemInstruction, + }) + .cast(); + + final buffer = StringBuffer(); + await for (final chunk in nativeStream) { + buffer.write(chunk); + context.sendChunk( + ModelResponseChunk(content: [TextPart(text: chunk)]), + ); + } + + return ModelResponse( + finishReason: FinishReason.stop, + message: Message( + role: Role.model, + content: [TextPart(text: buffer.toString())], + ), + ); + }, + ); + + // 3. Local HTTP Completions model + ai.defineModel( + name: httpCompletion, + fn: (request, context) async { + final (String prompt, String? systemInstruction) = _extractInputs( + request, + ); + final client = HttpClient(); + final buffer = StringBuffer(); + try { + // Support dynamic configurations via request options, + // defaulting to the local MLX server on port 8080. + final String baseUrl = + request.config?['baseUrl'] as String? ?? + 'http://localhost:8080/v1'; + final String modelName = + request.config?['model'] as String? ?? + 'mlx-community/gemma-4-e2b-it-4bit'; + + final Uri uri = Uri.parse('$baseUrl/chat/completions'); + final HttpClientRequest httpReq = await client.postUrl(uri); + httpReq.headers.contentType = ContentType.json; + + final Map payload = { + 'model': modelName, + 'messages': [ + if (systemInstruction != null) + {'role': 'system', 'content': systemInstruction}, + {'role': 'user', 'content': prompt}, + ], + 'stream': true, + }; + + httpReq.write(jsonEncode(payload)); + final HttpClientResponse response = await httpReq.close(); + + if (response.statusCode != 200) { + throw HttpException( + 'Local HTTP completions server error: ${response.statusCode}', + ); + } + + final Stream lines = response + .transform(utf8.decoder) + .transform(const LineSplitter()); + + await for (final String line in lines) { + if (line.startsWith('data: ')) { + final String data = line.substring(6).trim(); + if (data == '[DONE]') { + break; + } + try { + final parsed = jsonDecode(data) as Map; + final choices = parsed['choices'] as List?; + final firstChoice = + choices?.firstOrNull as Map?; + final delta = firstChoice?['delta'] as Map?; + final content = delta?['content'] as String?; + if (content != null && content.isNotEmpty) { + buffer.write(content); + context.sendChunk( + ModelResponseChunk(content: [TextPart(text: content)]), + ); + } + } catch (_) { + // Ignore parse errors on comments or SSE headers + } + } + } + + return ModelResponse( + finishReason: FinishReason.stop, + message: Message( + role: Role.model, + content: [TextPart(text: buffer.toString())], + ), + ); + } finally { + client.close(); + } + }, + ); + } +} diff --git a/packages/genui_express/lib/src/prompt/express_prompt_builder.dart b/packages/genui_express/lib/src/prompt/express_prompt_builder.dart new file mode 100644 index 000000000..b62d8967d --- /dev/null +++ b/packages/genui_express/lib/src/prompt/express_prompt_builder.dart @@ -0,0 +1,79 @@ +// 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 'dart:convert'; + +import 'package:genui/genui.dart'; + +import 'express_prompt_generator.dart'; + +/// Conforms to the [PromptBuilder] facade in GenUI to provide A2UI Express +/// system prompts. +class ExpressPromptBuilder implements PromptBuilder { + /// The registered component/functions [catalog]. + final Catalog catalog; + + /// High-level conversational prompt fragments. + final Iterable systemPromptFragments; + + /// Optional client-side mock data model schema configuration. + final Map? clientDataModel; + + /// Allowed operations config if using custom builder. + final SurfaceOperations? allowedOperations; + + /// Technical capabilities if using custom builder. + final TechnicalPossibilities? technicalPossibilities; + + /// Prefix for important prompt fragments. + final String importancePrefix; + + /// Creates an [ExpressPromptBuilder] configured for a typical chat session. + ExpressPromptBuilder.chat({ + required this.catalog, + this.systemPromptFragments = const [], + this.clientDataModel, + this.importancePrefix = PromptBuilder.defaultImportancePrefix, + }) : allowedOperations = null, + technicalPossibilities = null; + + /// Creates an [ExpressPromptBuilder] with full custom configuration control. + ExpressPromptBuilder.custom({ + required this.catalog, + required this.allowedOperations, + this.systemPromptFragments = const [], + this.importancePrefix = PromptBuilder.defaultImportancePrefix, + this.technicalPossibilities = const TechnicalPossibilities(), + this.clientDataModel, + }); + + @override + Iterable systemPrompt() { + final promptGenerator = ExpressPromptGenerator(catalog); + final String expressContract = promptGenerator.generatePrompt(); + + final fragments = [ + ...systemPromptFragments, + ...catalog.systemPromptFragments, + 'Use A2UI Express syntax to generate rich UI elements.', + expressContract, + if (clientDataModel != null) _encodedDataModel(clientDataModel), + ]; + + return fragments.map((e) => e.trim()); + } + + @override + String systemPromptJoined({ + String sectionSeparator = '\n-------------------------------------\n\n', + }) => systemPrompt().map((e) => '${e.trim()}\n').join(sectionSeparator); + + static String _encodedDataModel(Map? clientDataModel) { + if (clientDataModel == null) return ''; + final String encodedModel = const JsonEncoder.withIndent( + ' ', + ).convert(clientDataModel); + return 'Client Data Model:\n$encodedModel'; + } +} diff --git a/packages/genui_express/lib/src/prompt/express_prompt_generator.dart b/packages/genui_express/lib/src/prompt/express_prompt_generator.dart new file mode 100644 index 000000000..3306b8d47 --- /dev/null +++ b/packages/genui_express/lib/src/prompt/express_prompt_generator.dart @@ -0,0 +1,131 @@ +// 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:genui/genui.dart'; + +import '../compiler/catalog_schema_helper.dart'; + +/// Generates A2UI Express contract signatures based on the introspection +/// helper. +class ExpressPromptGenerator { + /// The active catalog helper. + final CatalogSchemaHelper helper; + + /// Creates an [ExpressPromptGenerator] wrapping [catalog]. + ExpressPromptGenerator(Catalog catalog) + : helper = CatalogSchemaHelper(catalog); + + /// Generates compact positional signatures for all components in the + /// catalog. + String generateComponentSignatures() { + final List signatures = []; + final List sortedNames = helper.componentProperties.keys.toList() + ..sort(); + for (final name in sortedNames) { + final List props = helper.getComponentProperties(name); + final List reqs = helper.getComponentRequired(name); + final List orderedArgs = []; + for (final p in props) { + final bool isReq = reqs.contains(p); + final optSuffix = isReq ? '' : '?'; + orderedArgs.add('$p$optSuffix'); + } + final sig = "• $name(${orderedArgs.join(', ')})"; + signatures.add(sig); + } + return signatures.join('\n'); + } + + /// Generates compact signatures for all client logic functions in the + /// catalog. + String generateFunctionSignatures() { + final List signatures = []; + final List sortedNames = helper.functionProperties.keys.toList() + ..sort(); + for (final name in sortedNames) { + final List props = helper.getFunctionProperties(name); + final List reqs = helper.getFunctionRequired(name); + final List orderedArgs = []; + for (final p in props) { + final bool isReq = reqs.contains(p); + final optSuffix = isReq ? '' : '?'; + orderedArgs.add('$p$optSuffix'); + } + final sig = "• $name(${orderedArgs.join(', ')})"; + signatures.add(sig); + } + return signatures.join('\n'); + } + + /// Returns the complete system prompt contract text to guide the LLM. + String generatePrompt() { + final String compSigs = generateComponentSignatures(); + final String funcSigs = generateFunctionSignatures(); + + return ''' +# A2UI Express Output Contract + +You must output the user interface using the compact A2UI Express DSL notation. +You MUST surround the entire A2UI Express DSL block with the sentinel tags +`` and ``. + +## Grammar Rules + +1. Output exactly one variable assignment statement per line: + variable_name = ComponentName(arg1, arg2, ...) + +2. The interface tree must have a single entry point assigned to the + reserved variable 'root'. + +3. Primitives: + - Strings: enclose in double quotes, e.g., "label" + - Numbers: write as integers or decimals, e.g., 42 + - Booleans: write true or false + - Null values: write null + +4. Lists: represent as arrays, e.g., [child1, child2] + +5. Data bindings: prefix absolute paths in the data model with '\$', + e.g., \$/user/firstName. Prefix relative list scopes with '\$', + e.g., \$firstName. + +6. Logic and validation: prefix client check rules with '?', e.g., ?required or + ?regex("^[0-9]{5}\$"). + +7. Action events: represent server-side actions using the Event helper: + Event("save_deal", {rep: \$/form/rep}) + +8. Nested functions: call client functions directly using catalog signatures, + for example openUrl("https://example.com"). + +9. Data model population: Assign a value directly to an absolute data path + (e.g. \$/path/to/key = "value") to populate or initialize values inside + the shared dataModel. The value can be a primitive, array, or map. + +## Positional Component Signatures + +Use these exact positional signatures to instantiate components. Do not +output property keys: +$compSigs + +## Positional Function Signatures + +Use these exact positional signatures to instantiate check rules or logic +functions: +$funcSigs + +## Examples + + +root = Column([repField, valueField]) +repField = TextField("Representative", \$/form/rep, "Enter name") +valueField = TextField( + "Deal Value", \$/form/value, "0.00", "number", [?required] +) +\$/form/rep = "John Doe" +\$/form/value = 1500.00 + +'''; + } +} diff --git a/packages/genui_express/lib/src/transport/express_local_transport.dart b/packages/genui_express/lib/src/transport/express_local_transport.dart new file mode 100644 index 000000000..30fb0e6b4 --- /dev/null +++ b/packages/genui_express/lib/src/transport/express_local_transport.dart @@ -0,0 +1,280 @@ +// 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 'dart:async'; +import 'dart:convert'; + +import 'package:genkit/genkit.dart' as genkit; +import 'package:genui/genui.dart'; + +import '../compiler/express_compiler.dart'; + +/// A [Transport] implementation that coordinates local Genkit LLM inference +/// streams, maintains session conversation history, and compiles layout DSL +/// outputs. +/// +/// Following the Robustness Principle (Postel's Law), it resiliently handles +/// both compact A2UI Express layout DSL blocks (fenced in ``) and +/// standard A2UI JSON specifications (fenced in ```json). +class ExpressLocalTransport implements Transport { + /// The core Genkit engine instance. + final genkit.Genkit ai; + + /// The target Genkit model reference. + final genkit.ModelRef model; + + /// The A2UI Express compiler instance. + final ExpressCompiler compiler; + + /// The component catalog used for property mapping constraints. + final Catalog catalog; + + final A2uiTransportAdapter _adapter = A2uiTransportAdapter(); + final StreamController _textStreamController = + StreamController.broadcast(); + + final List _history = []; + final StringBuffer _lineBuffer = StringBuffer(); + final List _dslLines = []; + final List _jsonLines = []; + bool _insideA2ui = false; + bool _insideJson = false; + + ExpressLocalTransport({ + required this.ai, + required this.model, + required this.catalog, + }) : compiler = ExpressCompiler(catalog); + + /// Exposes the unmodifiable conversation history list. + List get history => List.unmodifiable(_history); + + /// Appends a system message to the session conversation history. + void addSystemMessage(String content) { + _history.add(ChatMessage.system(content)); + } + + @override + Stream get incomingMessages => _adapter.incomingMessages; + + @override + Stream get incomingText => _textStreamController.stream; + + /// Maps standard A2UI [ChatMessage] objects to Genkit-compatible + /// [genkit.Message] structures. + genkit.Message _mapToGenkitMessage(ChatMessage msg) { + final genkit.Role role = switch (msg.role) { + ChatMessageRole.system => genkit.Role.system, + ChatMessageRole.user => genkit.Role.user, + ChatMessageRole.model => genkit.Role.model, + }; + + final List parts = []; + for (final StandardPart part in msg.parts) { + if (part.isUiInteractionPart) { + parts.add(genkit.TextPart(text: part.asUiInteractionPart!.interaction)); + } else if (part is TextPart) { + parts.add(genkit.TextPart(text: part.text)); + } + } + + return genkit.Message(role: role, content: parts); + } + + @override + Future sendRequest(ChatMessage message) async { + // Reset stream interception states + _insideA2ui = false; + _insideJson = false; + _dslLines.clear(); + _jsonLines.clear(); + _lineBuffer.clear(); + + // Add user message to internal history list + _history.add(message); + + // Construct Genkit history messages list representing the entire + // conversation + final List genkitHistory = _history + .map(_mapToGenkitMessage) + .toList(); + + // Invoke Genkit generation stream using the complete history messages list + final Stream> stream = ai + .generateStream( + model: model, + messages: genkitHistory, + ); + + final fullResponseBuffer = StringBuffer(); + + await for (final chunk in stream) { + final String chunkText = chunk.text; + if (chunkText.isEmpty) continue; + + fullResponseBuffer.write(chunkText); + + // Buffers chunks and splits them by lines to isolate sentinel tags + _lineBuffer.write(chunkText); + final currentText = _lineBuffer.toString(); + final List lines = currentText.split('\n'); + + if (lines.length > 1) { + final String incompleteLine = lines.last; + _lineBuffer.clear(); + _lineBuffer.write(incompleteLine); + + for (var i = 0; i < lines.length - 1; i++) { + final String line = lines[i]; + final String trimmed = line.trim(); + + if (trimmed.contains('')) { + _insideA2ui = true; + continue; + } + if (trimmed.contains('')) { + _insideA2ui = false; + continue; + } + if (trimmed.contains('```json')) { + _insideJson = true; + continue; + } + if (trimmed.contains('```') && _insideJson) { + _insideJson = false; + continue; + } + + if (_insideA2ui) { + _dslLines.add(line); + } else if (_insideJson) { + _jsonLines.add(line); + } else { + if (!trimmed.startsWith('```')) { + _textStreamController.add('$line\n'); + } + } + } + } + } + + // Extract remaining line chunk on stream closure + final remaining = _lineBuffer.toString(); + _lineBuffer.clear(); + final String trimmedRemaining = remaining.trim(); + if (remaining.isNotEmpty) { + if (trimmedRemaining.contains('')) { + _insideA2ui = true; + } else if (trimmedRemaining.contains('')) { + _insideA2ui = false; + } else if (trimmedRemaining.contains('```json')) { + _insideJson = true; + } else if (trimmedRemaining.contains('```') && _insideJson) { + _insideJson = false; + } else { + if (_insideA2ui) { + _dslLines.add(remaining); + } else if (_insideJson) { + _jsonLines.add(remaining); + } else { + if (!trimmedRemaining.startsWith('```')) { + _textStreamController.add(remaining); + } + } + } + } + + // Append final full model response to the conversation history + final responseText = fullResponseBuffer.toString(); + _history.add(ChatMessage.model(responseText)); + + // 1. Compile DSL scripts if accumulated + if (_dslLines.isNotEmpty) { + final String dslText = _dslLines.join('\n').trim(); + if (dslText.isNotEmpty) { + try { + final surfaceId = 'surface_${DateTime.now().millisecondsSinceEpoch}'; + final Map compiledMap = compiler.compile( + dslText, + surfaceId: surfaceId, + ); + + final createSurface = + compiledMap['createSurface'] as Map; + final componentsList = + createSurface.remove('components') as List?; + final dataModelMap = + createSurface.remove('dataModel') as Map?; + + // Emit CreateSurface + final createMsg = A2uiMessage.fromJson(compiledMap); + _adapter.addMessage(createMsg); + + // Emit UpdateComponents if present + if (componentsList != null && componentsList.isNotEmpty) { + final updateMap = { + 'version': 'v0.9', + 'updateComponents': { + 'surfaceId': surfaceId, + 'components': componentsList, + }, + }; + final updateMsg = A2uiMessage.fromJson(updateMap); + _adapter.addMessage(updateMsg); + } + + // Emit UpdateDataModel if present + if (dataModelMap != null && dataModelMap.isNotEmpty) { + final dataMap = { + 'version': 'v0.9', + 'updateDataModel': { + 'surfaceId': surfaceId, + 'path': '/', + 'value': dataModelMap, + }, + }; + final dataMsg = A2uiMessage.fromJson(dataMap); + _adapter.addMessage(dataMsg); + } + } catch (e) { + _textStreamController.add( + '\n*(Failed to compile A2UI Express response: $e)*\n', + ); + } + } + } + + // 2. Parse standard JSON envelopes if accumulated (liberal support for + // both Maps and Lists) + if (_jsonLines.isNotEmpty) { + final String jsonText = _jsonLines.join('\n').trim(); + if (jsonText.isNotEmpty) { + try { + final Object? parsed = jsonDecode(jsonText); + if (parsed is List) { + for (final Object? item in parsed) { + if (item is Map) { + final a2uiMsg = A2uiMessage.fromJson(item); + _adapter.addMessage(a2uiMsg); + } + } + } else if (parsed is Map) { + final a2uiMsg = A2uiMessage.fromJson(parsed); + _adapter.addMessage(a2uiMsg); + } + } catch (e) { + _textStreamController.add( + '\n*(Failed to parse standard JSON response: $e)*\n', + ); + } + } + } + } + + @override + void dispose() { + _adapter.dispose(); + _textStreamController.close(); + } +} diff --git a/packages/genui_express/macos/genui_express.podspec b/packages/genui_express/macos/genui_express.podspec new file mode 100644 index 000000000..12f85b8bb --- /dev/null +++ b/packages/genui_express/macos/genui_express.podspec @@ -0,0 +1,30 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint genui_express.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'genui_express' + s.version = '0.0.1' + s.summary = 'A new Flutter plugin project.' + s.description = <<-DESC +A new Flutter plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + + s.source = { :path => '.' } + s.source_files = 'genui_express/Sources/genui_express/**/*' + + # If your plugin requires a privacy manifest, for example if it collects user + # data, update the PrivacyInfo.xcprivacy file to describe your plugin's + # privacy impact, and then uncomment this line. For more information, + # see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files + # s.resource_bundles = {'genui_express_privacy' => ['genui_express/Sources/genui_express/PrivacyInfo.xcprivacy']} + + s.dependency 'FlutterMacOS' + + s.platform = :osx, '15.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } + s.swift_version = '5.0' +end diff --git a/packages/genui_express/macos/genui_express/Package.swift b/packages/genui_express/macos/genui_express/Package.swift new file mode 100644 index 000000000..ee53825c2 --- /dev/null +++ b/packages/genui_express/macos/genui_express/Package.swift @@ -0,0 +1,40 @@ +// 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. + +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "genui_express", + platforms: [ + .macOS("15.0") + ], + products: [ + .library(name: "genui-express", targets: ["genui_express"]) + ], + dependencies: [ + .package(name: "FlutterFramework", path: "../FlutterFramework") + ], + targets: [ + .target( + name: "genui_express", + dependencies: [ + .product(name: "FlutterFramework", package: "FlutterFramework") + ], + resources: [ + // If your plugin requires a privacy manifest, for example if it collects user + // data, update the PrivacyInfo.xcprivacy file to describe your plugin's + // privacy impact, and then uncomment these lines. For more information, see + // https://developer.apple.com/documentation/bundleresources/privacy_manifest_files + // .process("PrivacyInfo.xcprivacy"), + + // If you have other resources that need to be bundled with your plugin, refer to + // the following instructions to add them: + // https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package + ] + ) + ] +) diff --git a/packages/genui_express/macos/genui_express/Sources/genui_express/GenuiExpressPlugin.swift b/packages/genui_express/macos/genui_express/Sources/genui_express/GenuiExpressPlugin.swift new file mode 100644 index 000000000..9f04a50c7 --- /dev/null +++ b/packages/genui_express/macos/genui_express/Sources/genui_express/GenuiExpressPlugin.swift @@ -0,0 +1,114 @@ +// 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 Cocoa +import FlutterMacOS +import Foundation + +#if canImport(LanguageModeling) +import LanguageModeling +#endif + +public class GenuiExpressPlugin: NSObject, FlutterPlugin, FlutterStreamHandler { + private var eventSink: FlutterEventSink? + private var activeTask: Task? + + public static func register(with registrar: FlutterPluginRegistrar) { + let methodChannel = FlutterMethodChannel( + name: "genui_express/local_ai", + binaryMessenger: registrar.messenger + ) + let eventChannel = FlutterEventChannel( + name: "genui_express/local_ai_stream", + binaryMessenger: registrar.messenger + ) + + let instance = GenuiExpressPlugin() + registrar.addMethodCallDelegate(instance, channel: methodChannel) + eventChannel.setStreamHandler(instance) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "checkAvailability": + #if canImport(LanguageModeling) + if #available(macOS 15.0, iOS 18.0, *) { + let available = LanguageModelSession.hasCapability(.textGeneration) + result(available) + return + } + #endif + result(false) + default: + result(FlutterMethodNotImplemented) + } + } + + public func onListen( + withArguments arguments: Any?, + eventSink events: @escaping FlutterEventSink + ) -> FlutterError? { + self.eventSink = events + + guard let args = arguments as? [String: Any], + let prompt = args["prompt"] as? String + else { + return FlutterError( + code: "INVALID_ARGUMENTS", + message: "Missing prompt parameter", + details: nil + ) + } + + let systemPrompt = args["systemPrompt"] as? String + + #if canImport(LanguageModeling) + if #available(macOS 15.0, iOS 18.0, *) { + activeTask = Task { + do { + var config = LanguageModelSession.Configuration() + if let system = systemPrompt { + config.systemPrompt = system + } + + let session = try await LanguageModelSession.create(configuration: config) + let stream = try await session.generateResponse(for: prompt) + + for try await chunk in stream { + guard !Task.isCancelled else { break } + events(chunk) + } + + events(FlutterEndOfEventStream) + } catch { + events( + FlutterError( + code: "INFERENCE_ERROR", + message: error.localizedDescription, + details: nil + ) + ) + } + } + return nil + } + #endif + + events( + FlutterError( + code: "UNSUPPORTED_OS", + message: "FoundationModels requires macOS 15.0 or newer and the LanguageModeling framework", + details: nil + ) + ) + return nil + } + + public func onCancel(withArguments arguments: Any?) -> FlutterError? { + activeTask?.cancel() + activeTask = nil + eventSink = nil + return nil + } +} diff --git a/packages/genui_express/macos/genui_express/Sources/genui_express/PrivacyInfo.xcprivacy b/packages/genui_express/macos/genui_express/Sources/genui_express/PrivacyInfo.xcprivacy new file mode 100644 index 000000000..918d80be4 --- /dev/null +++ b/packages/genui_express/macos/genui_express/Sources/genui_express/PrivacyInfo.xcprivacy @@ -0,0 +1,12 @@ + + + + + NSPrivacyTrackingDomains + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + diff --git a/packages/genui_express/pubspec.yaml b/packages/genui_express/pubspec.yaml new file mode 100644 index 000000000..dd88d6771 --- /dev/null +++ b/packages/genui_express/pubspec.yaml @@ -0,0 +1,45 @@ +# 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. + +name: genui_express +description: Integrates A2UI Express compilation and local on-device AI engines with GenUI. +version: 0.1.0 +homepage: https://github.com/flutter/genui/tree/main/packages/genui_express +license: BSD-3-Clause +issue_tracker: https://github.com/flutter/genui/issues + +environment: + sdk: ^3.12.0 + flutter: ">=3.3.0" + +resolution: workspace + +dependencies: + collection: any + flutter: + sdk: flutter + flutter_web_plugins: + sdk: flutter + genkit: ^0.13.2 + genui: ^0.9.1 + plugin_platform_interface: ^2.0.2 + web: ^1.0.0 +dev_dependencies: + flutter_lints: ^6.0.0 + flutter_test: + sdk: flutter + +flutter: + plugin: + platforms: + android: + package: com.example.genui_express + pluginClass: GenuiExpressPlugin + ios: + pluginClass: GenuiExpressPlugin + macos: + pluginClass: GenuiExpressPlugin + web: + pluginClass: GenuiExpressWeb + fileName: genui_express_web.dart diff --git a/packages/genui_express/test/compiler_test.dart b/packages/genui_express/test/compiler_test.dart new file mode 100644 index 000000000..50eef776f --- /dev/null +++ b/packages/genui_express/test/compiler_test.dart @@ -0,0 +1,235 @@ +// 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_test/flutter_test.dart'; +import 'package:genui/genui.dart'; +import 'package:genui_express/genui_express.dart'; +import 'package:genui_express/src/compiler/parser.dart'; +import 'package:genui_express/src/compiler/token.dart'; + +void main() { + group('A2UI Express Tokenizer & Parser', () { + test('tokenize basic assignment', () { + const dsl = 'root = Column([welcome, saveButton])'; + final List tokens = tokenize(dsl.split('=').last.trim()); + expect(tokens, hasLength(8)); + expect(tokens[0].kind, TokenKind.identifier); + expect(tokens[0].value, 'Column'); + expect(tokens[1].kind, TokenKind.lparen); + expect(tokens[2].kind, TokenKind.lbracket); + expect(tokens[3].kind, TokenKind.identifier); + expect(tokens[3].value, 'welcome'); + expect(tokens[4].kind, TokenKind.comma); + expect(tokens[5].kind, TokenKind.identifier); + expect(tokens[5].value, 'saveButton'); + expect(tokens[6].kind, TokenKind.rbracket); + expect(tokens[7].kind, TokenKind.rparen); + }); + + test('parse expressions with primitives, paths, checks, events', () { + final List tokens = tokenize( + 'TextField(\$/form/value, "Deal Value", "number", ?required)', + ); + final parser = TokenParser(tokens); + final ast = parser.parseExpression() as Map; + + expect(ast['call'], 'TextField'); + final args = ast['args'] as List; + expect(args, hasLength(4)); + expect(args[0], {'path': '/form/value'}); + expect(args[1], 'Deal Value'); + expect(args[2], 'number'); + expect(args[3], {'check': 'required', 'args': []}); + }); + }); + + group('ExpressCompiler', () { + late Catalog catalog; + late ExpressCompiler compiler; + + setUp(() { + catalog = BasicCatalogItems.asNoAssetCatalog(); + compiler = ExpressCompiler(catalog); + }); + + test('compile basic component hierarchy', () { + const dsl = ''' +root = Column([repField, valueField]) +repField = TextField(\$/form/rep, "Representative") +valueField = TextField(\$/form/value, "Deal Value", "number", ?required) +'''; + + final Map envelope = compiler.compile( + dsl, + surfaceId: 'test_surf', + ); + expect(envelope['version'], 'v0.9'); + final createSurface = envelope['createSurface'] as Map; + expect(createSurface['surfaceId'], 'test_surf'); + + final List> components = + (createSurface['components'] as List).cast>(); + expect(components, hasLength(3)); + + final Map rootComp = components.firstWhere( + (c) => c['id'] == 'root', + ); + expect(rootComp['component'], 'Column'); + expect(rootComp['children'], ['repField', 'valueField']); + + final Map repComp = components.firstWhere( + (c) => c['id'] == 'repField', + ); + expect(repComp['component'], 'TextField'); + expect(repComp['label'], 'Representative'); + expect(repComp['value'], {'path': '/form/rep'}); + + final Map valComp = components.firstWhere( + (c) => c['id'] == 'valueField', + ); + expect(valComp['component'], 'TextField'); + expect(valComp['label'], 'Deal Value'); + expect(valComp['value'], {'path': '/form/value'}); + expect(valComp['variant'], 'number'); + + // Verify implicit path validation injection + final checks = valComp['checks'] as List; + expect(checks, hasLength(1)); + final firstCheck = checks[0] as Map; + final condition = firstCheck['condition'] as Map; + expect(condition['call'], 'required'); + final conditionArgs = condition['args'] as Map; + expect(conditionArgs['value'], {'path': '/form/value'}); + expect(firstCheck['message'], 'Required check failed'); + }); + + test('compile format string and action events', () { + const dsl = ''' +root = Column([welcome, saveButton]) +welcome = Text(formatString("Welcome, \${/user/name}!")) +saveButton = Button(saveLabel, Event("submitDeal", {rep: \$/form/rep}), "primary") +saveLabel = Text("Save") +'''; + + final Map envelope = compiler.compile(dsl); + final createSurface = envelope['createSurface'] as Map; + final List> components = + (createSurface['components'] as List).cast>(); + + final Map welcomeComp = components.firstWhere( + (c) => c['id'] == 'welcome', + ); + expect(welcomeComp['text'], { + 'call': 'formatString', + 'args': {'value': 'Welcome, \${/user/name}!'}, + 'returnType': 'string', + }); + + final Map buttonComp = components.firstWhere( + (c) => c['id'] == 'saveButton', + ); + expect(buttonComp['variant'], 'primary'); + expect(buttonComp['action'], { + 'event': { + 'name': 'submitDeal', + 'context': { + 'rep': {'path': '/form/rep'}, + }, + }, + }); + }); + + test('resilient compile of missing root and auto-wrapping strings', () { + const dsl = 'submit_button = Button("Submit", "Send Request")'; + + final Map envelope = compiler.compile(dsl); + expect(envelope['version'], 'v0.9'); + final createSurface = envelope['createSurface'] as Map; + final List> components = + (createSurface['components'] as List).cast>(); + + expect(components, hasLength(2)); + + final Map textComp = components.firstWhere( + (c) => c['id'] == 'txt_root_0', + ); + expect(textComp['component'], 'Text'); + expect(textComp['text'], 'Submit'); + + final Map buttonComp = components.firstWhere( + (c) => c['id'] == 'root', + ); + expect(buttonComp['component'], 'Button'); + expect(buttonComp['child'], 'txt_root_0'); + expect(buttonComp['action'], { + 'event': {'name': 'Send Request', 'context': const {}}, + }); + }); + + test('compile inline nested components without re-wrapping', () { + const dsl = 'root = Column([Text("Hello")])'; + + final Map envelope = compiler.compile(dsl); + final createSurface = envelope['createSurface'] as Map; + final List> components = + (createSurface['components'] as List).cast>(); + + expect(components, hasLength(2)); + + final Map colComp = components.firstWhere( + (c) => c['id'] == 'root', + ); + expect(colComp['component'], 'Column'); + expect(colComp['children'], ['inline_Text_0']); + + final Map textComp = components.firstWhere( + (c) => c['id'] == 'inline_Text_0', + ); + expect(textComp['component'], 'Text'); + expect(textComp['text'], 'Hello'); + }); + + test('compile formatted multi-line statements', () { + const dsl = ''' +root = Column([ + Text("Line 1"), + Text("Line 2") +]) +'''; + + final Map envelope = compiler.compile(dsl); + final createSurface = envelope['createSurface'] as Map; + final List> components = + (createSurface['components'] as List).cast>(); + + expect(components, hasLength(3)); + + final Map colComp = components.firstWhere( + (c) => c['id'] == 'root', + ); + expect(colComp['component'], 'Column'); + expect(colComp['children'], ['inline_Text_0', 'inline_Text_1']); + }); + + test('compile absolute data model assignments', () { + const dsl = ''' +root = Text("Status") +\$/icon = "check" +\$/user/profile/firstName = "Alice" +\$/user/profile/age = 30 +'''; + + final Map envelope = compiler.compile(dsl); + final createSurface = envelope['createSurface'] as Map; + final dataModel = createSurface['dataModel'] as Map; + + expect(dataModel, { + 'icon': 'check', + 'user': { + 'profile': {'firstName': 'Alice', 'age': 30}, + }, + }); + }); + }); +} diff --git a/packages/genui_express/test/transport_test.dart b/packages/genui_express/test/transport_test.dart new file mode 100644 index 000000000..4d516c7c4 --- /dev/null +++ b/packages/genui_express/test/transport_test.dart @@ -0,0 +1,206 @@ +// 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 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:genkit/genkit.dart' as genkit; +import 'package:genui/genui.dart'; +import 'package:genui_express/genui_express.dart'; + +void main() { + group('ExpressLocalTransport', () { + late Catalog catalog; + late genkit.Genkit ai; + + setUp(() { + catalog = BasicCatalogItems.asNoAssetCatalog(); + ai = genkit.Genkit(isDevEnv: false); + }); + + test('stream mixed text and compiled layout', () async { + final genkit.ModelRef model = genkit.modelRef( + 'local/mock-dsl-model', + ); + + // Register a mock model streaming Express DSL layouts + ai.defineModel( + name: 'local/mock-dsl-model', + fn: (request, context) async { + final responses = [ + 'Here is a button for you:\n', + '\n', + 'root = Button("Save", "saveAction")\n', + '\n', + 'Hope that helps!\n', + ]; + for (final chunk in responses) { + context.sendChunk( + genkit.ModelResponseChunk( + content: [genkit.TextPart(text: chunk)], + ), + ); + await Future.delayed(const Duration(milliseconds: 1)); + } + + return genkit.ModelResponse( + finishReason: genkit.FinishReason.stop, + message: genkit.Message( + role: genkit.Role.model, + content: [genkit.TextPart(text: responses.join())], + ), + ); + }, + ); + + final transport = ExpressLocalTransport( + ai: ai, + model: model, + catalog: catalog, + ); + + final List textChunks = []; + final List messages = []; + + final StreamSubscription textSub = transport.incomingText.listen( + textChunks.add, + ); + final StreamSubscription messageSub = transport + .incomingMessages + .listen(messages.add); + + await transport.sendRequest(ChatMessage.user('Hello')); + await Future.delayed(const Duration(milliseconds: 50)); + + // Verify conversational text streaming + expect(textChunks, hasLength(2)); + expect(textChunks[0].trim(), 'Here is a button for you:'); + expect(textChunks[1].trim(), 'Hope that helps!'); + + // Verify compiled layout messages and cast to subtypes + expect(messages, hasLength(2)); + + expect(messages[0], isA()); + final createMsg = messages[0] as CreateSurface; + expect(createMsg.surfaceId, startsWith('surface_')); + + expect(messages[1], isA()); + final updateMsg = messages[1] as UpdateComponents; + expect(updateMsg.components, hasLength(2)); + + final Component buttonComp = updateMsg.components.firstWhere( + (c) => c.id == 'root', + ); + expect(buttonComp.type, 'Button'); + expect(buttonComp.properties['action'], { + 'event': {'name': 'saveAction', 'context': const {}}, + }); + + await textSub.cancel(); + await messageSub.cancel(); + transport.dispose(); + }); + + test('stream standard JSON array', () async { + final genkit.ModelRef model = genkit.modelRef( + 'local/mock-json-model', + ); + + // Register a mock model streaming standard A2UI JSON array envelopes + ai.defineModel( + name: 'local/mock-json-model', + fn: (request, context) async { + final responses = [ + 'Here is standard JSON:\n', + '```json\n', + '[\n', + ' {\n', + ' "version": "v0.9",\n', + ' "createSurface": {\n', + ' "surfaceId": "json_surf",\n', + ' "catalogId": "https://a2ui.org/spec"\n', + ' }\n', + ' },\n', + ' {\n', + ' "version": "v0.9",\n', + ' "updateComponents": {\n', + ' "surfaceId": "json_surf",\n', + ' "components": [\n', + ' {\n', + ' "id": "root",\n', + ' "component": "Text",\n', + ' "text": "Test JSON"\n', + ' }\n', + ' ]\n', + ' }\n', + ' }\n', + ']\n', + '```\n', + 'Hope that also works!\n', + ]; + for (final chunk in responses) { + context.sendChunk( + genkit.ModelResponseChunk( + content: [genkit.TextPart(text: chunk)], + ), + ); + await Future.delayed(const Duration(milliseconds: 1)); + } + + return genkit.ModelResponse( + finishReason: genkit.FinishReason.stop, + message: genkit.Message( + role: genkit.Role.model, + content: [genkit.TextPart(text: responses.join())], + ), + ); + }, + ); + + final transport = ExpressLocalTransport( + ai: ai, + model: model, + catalog: catalog, + ); + + final List textChunks = []; + final List messages = []; + + final StreamSubscription textSub = transport.incomingText.listen( + textChunks.add, + ); + final StreamSubscription messageSub = transport + .incomingMessages + .listen(messages.add); + + await transport.sendRequest(ChatMessage.user('Hello JSON')); + await Future.delayed(const Duration(milliseconds: 50)); + + // Verify conversational text streaming + expect(textChunks, hasLength(2)); + expect(textChunks[0].trim(), 'Here is standard JSON:'); + expect(textChunks[1].trim(), 'Hope that also works!'); + + // Verify parsed standard JSON messages + expect(messages, hasLength(2)); + + expect(messages[0], isA()); + final createMsg = messages[0] as CreateSurface; + expect(createMsg.surfaceId, 'json_surf'); + expect(createMsg.catalogId, 'https://a2ui.org/spec'); + + expect(messages[1], isA()); + final updateMsg = messages[1] as UpdateComponents; + expect(updateMsg.surfaceId, 'json_surf'); + expect(updateMsg.components, hasLength(1)); + expect(updateMsg.components[0].id, 'root'); + expect(updateMsg.components[0].type, 'Text'); + expect(updateMsg.components[0].properties['text'], 'Test JSON'); + + await textSub.cancel(); + await messageSub.cancel(); + transport.dispose(); + }); + }); +} diff --git a/pubspec.yaml b/pubspec.yaml index 2607aad4d..6b8058a06 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,12 +14,14 @@ workspace: - dev_tools/catalog_gallery - dev_tools/composer + - examples/express_chat - examples/simple_chat - examples/verdure/client - packages/a2ui_core - packages/genui - packages/genui_a2a + - packages/genui_express - packages/json_schema_builder - tool/e2e diff --git a/tool/test_and_fix/lib/test_and_fix.dart b/tool/test_and_fix/lib/test_and_fix.dart index b305b7412..fdc5454ca 100644 --- a/tool/test_and_fix/lib/test_and_fix.dart +++ b/tool/test_and_fix/lib/test_and_fix.dart @@ -280,6 +280,7 @@ class TestAndFix { if (!normalized.endsWith('.g.dart') && !normalized.endsWith('.freezed.dart') && !normalized.endsWith('.mocks.dart') && + !normalized.endsWith('_web.dart') && !_isPartFile(entity)) { dartFiles.add(normalized); } diff --git a/tool/test_and_fix/pubspec.yaml b/tool/test_and_fix/pubspec.yaml index c39caa069..2e3203a87 100644 --- a/tool/test_and_fix/pubspec.yaml +++ b/tool/test_and_fix/pubspec.yaml @@ -13,9 +13,7 @@ resolution: workspace # Add regular dependencies here. dependencies: - # analyzer 10.0.2+ requires meta ^1.18.0, but the Flutter SDK currently - # pins meta 1.17.0. Cap below 10.0.2 until Flutter ships a newer meta. - analyzer: ">=8.0.0 <10.0.2" + analyzer: ">=10.0.0 <14.0.0" args: ^2.7.0 coverage: ^1.15.0 file: ^7.0.1