diff --git a/.github/workflows/auto-reply.yml b/.github/workflows/auto-reply.yml new file mode 100644 index 0000000..1dc440a --- /dev/null +++ b/.github/workflows/auto-reply.yml @@ -0,0 +1,23 @@ +name: Auto Reply to New PRs + +on: + pull_request: + types: [opened] + +jobs: + welcome: + runs-on: ubuntu-latest + permissions: + pull-requests: write # Grant write permission for creating comments + steps: + - name: Post Welcome Message + uses: actions/github-script@v5 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: "Thank you for opening this Pull Request! This repository is for demo purposes only. It's not maintained and there is no CI or merge rules. If you have permissions, you're free to merge the PR without review. If you'd like a review, please explicitly request it." + }) \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index e00e633..0000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Main Branch CI - -# Declare default permissions as read only. -permissions: read-all - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - workflow_dispatch: - schedule: - - cron: "0 0 * * *" # Every day at midnight - -defaults: - run: - shell: bash - -jobs: - flutter-tests: - name: Test Flutter ${{ matrix.flutter_version }} on ${{ matrix.os }} - runs-on: ${{ matrix.os }} - if: github.repository == 'flutter/demos' - strategy: - fail-fast: false - matrix: - flutter_version: [stable, beta, master] - os: [ubuntu-latest, macos-latest, windows-latest] - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - - uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b - with: - distribution: 'zulu' - java-version: '17' - - uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 - with: - channel: ${{ matrix.flutter_version }} - - run: ./tool/flutter_ci_script_${{ matrix.flutter_version }}.sh diff --git a/firebase_ai_logic_showcase/.gitignore b/firebase_ai_logic_showcase/.gitignore new file mode 100644 index 0000000..df0e787 --- /dev/null +++ b/firebase_ai_logic_showcase/.gitignore @@ -0,0 +1,54 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +pubspec.lock + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + +# macOS +ephemeral/ + +#firebase +google-services.json +GoogleService-Info.plist +.firebaserc diff --git a/firebase_ai_logic_showcase/.idx/dev.nix b/firebase_ai_logic_showcase/.idx/dev.nix new file mode 100644 index 0000000..7d5fe2c --- /dev/null +++ b/firebase_ai_logic_showcase/.idx/dev.nix @@ -0,0 +1,61 @@ +# To learn more about how to use Nix to configure your environment +# see: https://firebase.google.com/docs/studio/customize-workspace +{ pkgs, ... }: { + # Which nixpkgs channel to use. + channel = "stable-24.05"; # or "unstable" + # Use https://search.nixos.org/packages to find packages + packages = [ + pkgs.jdk21 + pkgs.unzip + pkgs.nodejs_22 + pkgs.nodePackages.nodemon + ]; + # Sets environment variables in the workspace + env = { + # Enable AppCheck for additional security for critical endpoints. + # Follow the configuration steps in the README to set up your project. + # ENABLE_APPCHECK = "TRUE"; + LOCAL_RECOMMENDATION_SERVICE = "http://127.0.0.1:8084/"; + GOOGLE_PROJECT = ""; + CLOUDSDK_CORE_PROJECT = ""; + TF_VAR_project = ""; + # Flip to true to help improve Angular + NG_CLI_ANALYTICS = "false"; + # Quieter Terraform logs + TF_IN_AUTOMATION = "true"; + }; + idx = { + # Search for the extensions you want on https://open-vsx.org/ and use "publisher.id" + extensions = [ + "Dart-Code.flutter" + "Dart-Code.dart-code" + "hashicorp.terraform" + "ms-vscode.js-debug" + ]; + workspace = { + # Runs when a workspace is first created with this `dev.nix` file + onCreate = { + npm-install = "flutter pub get"; + default.openFiles = [ + "README.md" + "lib/main.dart" + ]; + }; + # To run something each time the workspace is (re)started, use the `onStart` hook + }; + # Enable previews and customize configuration + previews = { + enable = true; + previews = { + web = { + command = [ "flutter" "run" "--machine" "-d" "web-server" "--web-hostname" "0.0.0.0" "--web-port" "$PORT" ]; + manager = "flutter"; + }; + # android = { + # command = [ "flutter" "run" "--machine" "-d" "android" "-d" "localhost:5555" ]; + # manager = "flutter"; + # }; + }; + }; + }; +} diff --git a/firebase_ai_logic_showcase/.idx/integrations.json b/firebase_ai_logic_showcase/.idx/integrations.json new file mode 100644 index 0000000..b9b8a61 --- /dev/null +++ b/firebase_ai_logic_showcase/.idx/integrations.json @@ -0,0 +1,9 @@ +{ + "firebase_hosting": {}, + "cloud_run_deploy": { + "region": "us-central1", + "sourceFlag": "--source services/cloud-run", + "allowUnauthenticatedInvocationsFlag": "--allow-unauthenticated" + }, + "gemini_api": {} + } diff --git a/firebase_ai_logic_showcase/.metadata b/firebase_ai_logic_showcase/.metadata new file mode 100644 index 0000000..05a8ab4 --- /dev/null +++ b/firebase_ai_logic_showcase/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "05db9689081f091050f01aed79f04dce0c750154" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 05db9689081f091050f01aed79f04dce0c750154 + base_revision: 05db9689081f091050f01aed79f04dce0c750154 + - platform: android + create_revision: 05db9689081f091050f01aed79f04dce0c750154 + base_revision: 05db9689081f091050f01aed79f04dce0c750154 + - platform: ios + create_revision: 05db9689081f091050f01aed79f04dce0c750154 + base_revision: 05db9689081f091050f01aed79f04dce0c750154 + - platform: linux + create_revision: 05db9689081f091050f01aed79f04dce0c750154 + base_revision: 05db9689081f091050f01aed79f04dce0c750154 + - platform: macos + create_revision: 05db9689081f091050f01aed79f04dce0c750154 + base_revision: 05db9689081f091050f01aed79f04dce0c750154 + - platform: web + create_revision: 05db9689081f091050f01aed79f04dce0c750154 + base_revision: 05db9689081f091050f01aed79f04dce0c750154 + - platform: windows + create_revision: 05db9689081f091050f01aed79f04dce0c750154 + base_revision: 05db9689081f091050f01aed79f04dce0c750154 + + # 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/firebase_ai_logic_showcase/LICENSE.txt b/firebase_ai_logic_showcase/LICENSE.txt new file mode 100644 index 0000000..e58143f --- /dev/null +++ b/firebase_ai_logic_showcase/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2024 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/firebase_ai_logic_showcase/README.md b/firebase_ai_logic_showcase/README.md new file mode 100644 index 0000000..a627f27 --- /dev/null +++ b/firebase_ai_logic_showcase/README.md @@ -0,0 +1,103 @@ +# flutter_firebase_ai_sample +**Target Platforms:** iOS, Android, Web + +**Tech Stack:** [Flutter](https://flutter.dev/) (frontend), +[Firebase AI Logic](https://firebase.google.com/docs/ai-logic) + +![Flutter & Firebase AI Sample App Mobile Screenshots](README/flutter_firebase_ai_sample_hero.png) + +This Flutter application demonstrates Firebase AI Logic capabilities through a +series of interactive demos. Firebase AI Logic provides access to the Gemini and +Imagen family of models, enabling developers to build AI-powered experiences in +Flutter apps. + +> [!NOTE] +> Check out this Google I/O 2025 talk for a full walkthrough on Firebase AI Logic: +> [How to build agentic apps with Flutter and Firebase AI Logic](https://www.youtube.com/watch?v=xo271p-Fl_4). + +## Getting Started + +1. Follow [these instructions](https://firebase.google.com/docs/ai-logic/get-started?&api=vertex#set-up-firebase) +to set up a Firebase project & connect the app to Firebase using `flutterfire configure` + +1. Run `flutter pub get` in the root of the project directory `flutter_ai` to +install the Flutter app dependencies + +1. Run `flutter run -d ` to start the app on iOS, Android, or Web. + +> [!TIP] +> Get available devices by running `flutter devices` ex: `AA8A7357`, `macos`, `chrome`. + +`main.dart` is the entry point for the app, but the `lib/flutter_firebase_ai_demo.dart` +file serves as the table of contents for the various demos. It defines the +structure for each demo and presents them in a navigable list on the app's home screen. + +## Explore the interactive demos: + +### Live API +Real-time bidirectional audio and video streaming with Gemini, demonstrating +dynamic and interactive AI communication. +- **Start/End Call:** Tap the "Call" button (phone icon) to initiate or terminate +the real-time audio and video stream with Gemini. +- **Toggle Video:** Once a call is active, tap the "Video" button (camera icon) +to start or stop sending your camera feed. +- **Flip Camera:** If video is active and multiple cameras are available, use +the "Flip Camera" button to switch between them. +- **Mute Audio:** During a call, tap the "Mute" button to toggle your +microphone's audio input. +- **Function Calling:** This demo is integrated with Function Calling, so +you can ask Gemini to use the two tools that are built into the demo: generate +an image or change the color of the app. + +### Multimodal Prompt +Interact with Gemini by asking questions about images, audio, video, or text files, +highlighting the model's ability to process diverse inputs. +- **Select a file:** Tap the "Pick File" button to choose an image, audio, video, +or text file from your device. +- **Enter a prompt:** Type your question or request about the selected file into +the text input field. +- **Ask Gemini:** Tap the "Ask Gemini" button to send the file and your prompt +to the Gemini model. +- **View response:** The response from Gemini will appear in the output display +area. A loading indicator will be shown while Gemini is processing your request. + +### Chat with Nano Banana & Function Calling +Engage in a continuous conversation with Gemini, where the model maintains +conversation history and uses function calling to perform actions or retrieve information. +- **Switch models:** Use the dropdown menu at the top of the screen to switch +between different Gemini models. +- **Type a message:** Enter your message in the input field at the bottom of the screen. +- **Send a message:** Tap the "Send" button to send your message to Gemini. +- **Attach an image (optional):** Tap the "Image" icon to select an image +from your gallery to send with your message. +- **View conversation:** Your messages and Gemini's responses will appear in the chat history. +- **Use tools with function calling:** With `gemini-2.5-flash` (selected by default), +you can ask Gemini to use the two tools that are built into the demo: + - Generate an image using Imagen (e.g., "generate an image of a cat") or + - Change the color of the app (e.g., "change the color to blue"). +- **Nano Banana** With `gemini-2.5-flash-image`, you can generate and edit images. + - **Generate an image:** Enter a text prompt and generate a new image. + - **Edit an image:** Provide instructions for Gemini to edit a previously generated image or select one from your photo library. + +## Implementation +All Firebase AI Logic code has been separated from the Flutter UI code to make +the code easier to read and understand. For each demo, you will find all of the +encapsulated Firebase AI Logic code in their respective `firebase__service.dart` files. +These files can be found in their respective demo directories, with the exception of +the `ImageService` which is shared across demos: Live API and Chat, +so the code is instead located in `lib/shared/firebaseai_imagen_service.dart`. + +Check out [this table](https://firebase.google.com/docs/ai-logic/models) for +more info on Firebase AI Logic's supported models & features. + +## Additional Resources +- [Firebase AI Logic docs](https://firebase.google.com/docs/ai-logic) +- [[Codelab] Build a Gemini powered Flutter app with Flutter & Firebase AI Logic](https://codelabs.developers.google.com/codelabs/flutter-gemini-colorist) + +Feeling inspired? Check out these other Flutter & Firebase AI Logic sample apps! +- [Agentic App Manager](https://github.com/flutter/demos/tree/main/agentic_app_manager): +Build an agentic experience in a Flutter app using Firebase AI Logic. +- [Colorist](https://github.com/flutter/demos/tree/main/vertex_ai_firebase_flutter_app): +Explore LLM tooling interfaces by allowing users to describe colors in natural language. +The app uses Gemini LLM to interpret descriptions and change the color of a +displayed square by calling specialized color tools. diff --git a/firebase_ai_logic_showcase/README/flutter_firebase_ai_sample_hero.png b/firebase_ai_logic_showcase/README/flutter_firebase_ai_sample_hero.png new file mode 100644 index 0000000..0718317 Binary files /dev/null and b/firebase_ai_logic_showcase/README/flutter_firebase_ai_sample_hero.png differ diff --git a/firebase_ai_logic_showcase/analysis_options.yaml b/firebase_ai_logic_showcase/analysis_options.yaml new file mode 100644 index 0000000..8957fb3 --- /dev/null +++ b/firebase_ai_logic_showcase/analysis_options.yaml @@ -0,0 +1,8 @@ +include: package:flutter_lints/flutter.yaml + +linter: + rules: + prefer_relative_imports: true + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/firebase_ai_logic_showcase/android/.gitignore b/firebase_ai_logic_showcase/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/firebase_ai_logic_showcase/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/firebase_ai_logic_showcase/android/app/build.gradle.kts b/firebase_ai_logic_showcase/android/app/build.gradle.kts new file mode 100644 index 0000000..0281b3e --- /dev/null +++ b/firebase_ai_logic_showcase/android/app/build.gradle.kts @@ -0,0 +1,48 @@ +plugins { + id("com.android.application") + // START: FlutterFire Configuration + id("com.google.gms.google-services") + // END: FlutterFire Configuration + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.example.flutter_firebase_ai_sample" + 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.flutter_firebase_ai_sample" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = 23 + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + ndkVersion = "27.0.12077973" + } + + 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/firebase_ai_logic_showcase/android/app/src/debug/AndroidManifest.xml b/firebase_ai_logic_showcase/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/firebase_ai_logic_showcase/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/firebase_ai_logic_showcase/android/app/src/main/AndroidManifest.xml b/firebase_ai_logic_showcase/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..dc3eede --- /dev/null +++ b/firebase_ai_logic_showcase/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/firebase_ai_logic_showcase/android/app/src/main/kotlin/com/example/flutter_firebase_ai_sample/MainActivity.kt b/firebase_ai_logic_showcase/android/app/src/main/kotlin/com/example/flutter_firebase_ai_sample/MainActivity.kt new file mode 100644 index 0000000..8b08e14 --- /dev/null +++ b/firebase_ai_logic_showcase/android/app/src/main/kotlin/com/example/flutter_firebase_ai_sample/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.flutter_firebase_ai_sample + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/firebase_ai_logic_showcase/android/app/src/main/kotlin/com/example/multimodal_ai_prototype/MainActivity.kt b/firebase_ai_logic_showcase/android/app/src/main/kotlin/com/example/multimodal_ai_prototype/MainActivity.kt new file mode 100644 index 0000000..8b08e14 --- /dev/null +++ b/firebase_ai_logic_showcase/android/app/src/main/kotlin/com/example/multimodal_ai_prototype/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.flutter_firebase_ai_sample + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/firebase_ai_logic_showcase/android/app/src/main/res/drawable-v21/launch_background.xml b/firebase_ai_logic_showcase/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/firebase_ai_logic_showcase/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/firebase_ai_logic_showcase/android/app/src/main/res/drawable/launch_background.xml b/firebase_ai_logic_showcase/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/firebase_ai_logic_showcase/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/firebase_ai_logic_showcase/android/app/src/main/res/values-night/styles.xml b/firebase_ai_logic_showcase/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/firebase_ai_logic_showcase/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/firebase_ai_logic_showcase/android/app/src/main/res/values/styles.xml b/firebase_ai_logic_showcase/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/firebase_ai_logic_showcase/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/firebase_ai_logic_showcase/android/app/src/profile/AndroidManifest.xml b/firebase_ai_logic_showcase/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/firebase_ai_logic_showcase/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/firebase_ai_logic_showcase/android/build.gradle.kts b/firebase_ai_logic_showcase/android/build.gradle.kts new file mode 100644 index 0000000..89176ef --- /dev/null +++ b/firebase_ai_logic_showcase/android/build.gradle.kts @@ -0,0 +1,21 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/firebase_ai_logic_showcase/android/gradle.properties b/firebase_ai_logic_showcase/android/gradle.properties new file mode 100644 index 0000000..f018a61 --- /dev/null +++ b/firebase_ai_logic_showcase/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/firebase_ai_logic_showcase/android/gradle/wrapper/gradle-wrapper.properties b/firebase_ai_logic_showcase/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..afa1e8e --- /dev/null +++ b/firebase_ai_logic_showcase/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.10.2-all.zip diff --git a/firebase_ai_logic_showcase/android/settings.gradle.kts b/firebase_ai_logic_showcase/android/settings.gradle.kts new file mode 100644 index 0000000..9e2d35c --- /dev/null +++ b/firebase_ai_logic_showcase/android/settings.gradle.kts @@ -0,0 +1,28 @@ +pluginManagement { + val flutterSdkPath = run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.7.0" apply false + // START: FlutterFire Configuration + id("com.google.gms.google-services") version("4.3.15") apply false + // END: FlutterFire Configuration + id("org.jetbrains.kotlin.android") version "1.8.22" apply false +} + +include(":app") diff --git a/firebase_ai_logic_showcase/assets/firebase-ai-logic.png b/firebase_ai_logic_showcase/assets/firebase-ai-logic.png new file mode 100644 index 0000000..46d73b6 Binary files /dev/null and b/firebase_ai_logic_showcase/assets/firebase-ai-logic.png differ diff --git a/firebase_ai_logic_showcase/assets/gemini-logo.png b/firebase_ai_logic_showcase/assets/gemini-logo.png new file mode 100644 index 0000000..3adcf7c Binary files /dev/null and b/firebase_ai_logic_showcase/assets/gemini-logo.png differ diff --git a/firebase_ai_logic_showcase/idx-template.json b/firebase_ai_logic_showcase/idx-template.json new file mode 100644 index 0000000..0406147 --- /dev/null +++ b/firebase_ai_logic_showcase/idx-template.json @@ -0,0 +1,28 @@ +{ + "name": "Flutter app with Firebase AI logic", + "description": "A sample flutter app that demonstrates how to use the Gemini API with Firebase AI Logic", + "categories": [ + "AI & ML", + "Web", + "Firebase", + "Flutter", + "Android", + "iOS" + ], + "icon_image_url": "https://www.gstatic.com/monospace/240513/logo_firebase.svg", + "publisher": "Google LLC", + "params": [ + { + "id": "projectId", + "name": "Firebase Project ID", + "type": "string", + "required": true + }, + { + "id": "bootstrapJs", + "name": "Sample App Bootstrap", + "type": "string", + "required": false + } + ] +} \ No newline at end of file diff --git a/firebase_ai_logic_showcase/idx-template.nix b/firebase_ai_logic_showcase/idx-template.nix new file mode 100644 index 0000000..febda87 --- /dev/null +++ b/firebase_ai_logic_showcase/idx-template.nix @@ -0,0 +1,20 @@ +{ pkgs, projectId, bootstrapJs, ... }: +{ + bootstrap = '' + cp -rf ${./.} "$out/" + chmod -R +w "$out" + echo 'bootstrapJs was set to: ${bootstrapJs}' + # Apply project ID to configs + if [ -z '${bootstrapJs}' ] || [ '${bootstrapJs}' = 'false' ] + then + sed -e 's//${projectId}/' ${.idx/dev.nix} > "$out/.idx/dev.nix" + else + sed -e 's//${projectId}/' ${.idx/dev.nix} | sed -e 's/terraform init/# terraform init/' | sed -e 's/terraform apply/# terraform apply/' > "$out/.idx/dev.nix" + echo '${bootstrapJs}' > "$out/web/bootstrap.js" + echo '{"projects":{"default":"${projectId}"}}' > "$out/.firebaserc" + fi + # Remove the template files themselves and any connection to the template's + # Git repository + rm -rf "$out/.git" "$out/idx-template".{nix,json} "$out/node_modules" + ''; +} \ No newline at end of file diff --git a/firebase_ai_logic_showcase/ios/.gitignore b/firebase_ai_logic_showcase/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/firebase_ai_logic_showcase/ios/Flutter/AppFrameworkInfo.plist b/firebase_ai_logic_showcase/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..1dc6cf7 --- /dev/null +++ b/firebase_ai_logic_showcase/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 + 13.0 + + diff --git a/firebase_ai_logic_showcase/ios/Flutter/Debug.xcconfig b/firebase_ai_logic_showcase/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/firebase_ai_logic_showcase/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/firebase_ai_logic_showcase/ios/Flutter/Release.xcconfig b/firebase_ai_logic_showcase/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/firebase_ai_logic_showcase/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/firebase_ai_logic_showcase/ios/Podfile b/firebase_ai_logic_showcase/ios/Podfile new file mode 100644 index 0000000..236ec2d --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '15.0' #FlutterFire plugins require minimum of iOS 15 https://firebase.google.com/docs/flutter/setup?platform=ios + +# 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/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.pbxproj b/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..0f11db6 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,753 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 2E93BD09C0B0FFDB52311EAA /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 66B6A3C553F8B7E90334CF84 /* Pods_RunnerTests.framework */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3760F5D70F4BD5A3F3B08C19 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 12DB00086E2FD750FC10BCD2 /* Pods_Runner.framework */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + EF7DAC43BEEDC80B2E3D9979 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9F4B329A2B7756919F9BCADD /* GoogleService-Info.plist */; }; +/* 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 */ + 12DB00086E2FD750FC10BCD2 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 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 = ""; }; + 2B33F176EE317BBD897D8C8E /* 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 = ""; }; + 2D957529EFAEE0BDD3A76F18 /* 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 = ""; }; + 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 = ""; }; + 66B6A3C553F8B7E90334CF84 /* 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 = ""; }; + 8E6CD9C6541A3CF46C64E1C9 /* 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 = ""; }; + 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 = ""; }; + 9F4B329A2B7756919F9BCADD /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; + BA6D30E40E8F68B06E56229C /* 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 = ""; }; + E9425AF2FD1A44E278C01648 /* 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 = ""; }; + F9CFD1A5C79D4AABA9C9A108 /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 37FD498E274777199498BC21 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2E93BD09C0B0FFDB52311EAA /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3760F5D70F4BD5A3F3B08C19 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 3044D5D4F7C58D0A98D41E15 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 12DB00086E2FD750FC10BCD2 /* Pods_Runner.framework */, + 66B6A3C553F8B7E90334CF84 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 6B0AAB17435D3F0DE252CF55 /* Pods */ = { + isa = PBXGroup; + children = ( + BA6D30E40E8F68B06E56229C /* Pods-Runner.debug.xcconfig */, + F9CFD1A5C79D4AABA9C9A108 /* Pods-Runner.release.xcconfig */, + 8E6CD9C6541A3CF46C64E1C9 /* Pods-Runner.profile.xcconfig */, + 2B33F176EE317BBD897D8C8E /* Pods-RunnerTests.debug.xcconfig */, + E9425AF2FD1A44E278C01648 /* Pods-RunnerTests.release.xcconfig */, + 2D957529EFAEE0BDD3A76F18 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + 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 */, + 6B0AAB17435D3F0DE252CF55 /* Pods */, + 3044D5D4F7C58D0A98D41E15 /* Frameworks */, + 9F4B329A2B7756919F9BCADD /* GoogleService-Info.plist */, + ); + 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 = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 04715B1AF939C799C4D37CEC /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + 37FD498E274777199498BC21 /* 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 = ( + E56B7B1A5FD7695313B2FE04 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + E65799E83B05311337559E13 /* [CP] Embed Pods Frameworks */, + F554392F020C8FA33594A554 /* [CP] Copy Pods Resources */, + ); + 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 */, + EF7DAC43BEEDC80B2E3D9979 /* GoogleService-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 04715B1AF939C799C4D37CEC /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + E56B7B1A5FD7695313B2FE04 /* [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; + }; + E65799E83B05311337559E13 /* [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; + }; + F554392F020C8FA33594A554 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = S8QB4VV633; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.multimodalAiPrototype; + 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 = 2B33F176EE317BBD897D8C8E /* 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.multimodalAiPrototype.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 = E9425AF2FD1A44E278C01648 /* 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.multimodalAiPrototype.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 = 2D957529EFAEE0BDD3A76F18 /* 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.multimodalAiPrototype.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = S8QB4VV633; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.multimodalAiPrototype; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = S8QB4VV633; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.multimodalAiPrototype; + 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/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/firebase_ai_logic_showcase/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/firebase_ai_logic_showcase/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..e3773d4 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/firebase_ai_logic_showcase/ios/Runner.xcworkspace/contents.xcworkspacedata b/firebase_ai_logic_showcase/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/firebase_ai_logic_showcase/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/firebase_ai_logic_showcase/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/firebase_ai_logic_showcase/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/firebase_ai_logic_showcase/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/firebase_ai_logic_showcase/ios/Runner/AppDelegate.swift b/firebase_ai_logic_showcase/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..6266644 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/firebase_ai_logic_showcase/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/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/firebase_ai_logic_showcase/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/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/firebase_ai_logic_showcase/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/firebase_ai_logic_showcase/ios/Runner/Base.lproj/LaunchScreen.storyboard b/firebase_ai_logic_showcase/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/firebase_ai_logic_showcase/ios/Runner/Base.lproj/Main.storyboard b/firebase_ai_logic_showcase/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/firebase_ai_logic_showcase/ios/Runner/Info.plist b/firebase_ai_logic_showcase/ios/Runner/Info.plist new file mode 100644 index 0000000..31531a5 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner/Info.plist @@ -0,0 +1,55 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Multimodal Ai Prototype + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + flutter_firebase_ai_sample + 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 + + NSMicrophoneUsageDescription + Need to record user voice to communicate with Gemini + NSCameraUsageDescription + Need camera to show Gemini live video stream + NSPhotoLibraryUsageDescription + Need pick images in chat + + diff --git a/firebase_ai_logic_showcase/ios/Runner/Runner-Bridging-Header.h b/firebase_ai_logic_showcase/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/firebase_ai_logic_showcase/ios/RunnerTests/RunnerTests.swift b/firebase_ai_logic_showcase/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/firebase_ai_logic_showcase/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/firebase_ai_logic_showcase/lib/demos/chat/chat_demo.dart b/firebase_ai_logic_showcase/lib/demos/chat/chat_demo.dart new file mode 100644 index 0000000..5ee5670 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/chat/chat_demo.dart @@ -0,0 +1,247 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:typed_data'; +import 'dart:developer'; +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:permission_handler/permission_handler.dart'; +import 'package:firebase_ai/firebase_ai.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:image_picker/image_picker.dart'; +import '../../shared/ui/app_frame.dart'; +import '../../shared/ui/app_spacing.dart'; +import './ui_components/ui_components.dart'; +import './firebaseai_chat_service.dart'; +import 'ui_components/model_picker.dart'; +import './models/models.dart'; + +class ChatDemo extends ConsumerStatefulWidget { + const ChatDemo({super.key}); + + @override + ConsumerState createState() => _ChatDemoState(); +} + +class _ChatDemoState extends ConsumerState { + // Service for interacting with the Gemini API. + late final ChatService _chatService; + + // UI State + final List _messages = []; + final TextEditingController _userTextInputController = + TextEditingController(); + Uint8List? _attachment; + final ScrollController _scrollController = ScrollController(); + bool _loading = false; + OverlayPortalController opController = OverlayPortalController(); + + @override + void initState() { + super.initState(); + _chatService = ChatService(ref); + _chatService.init(); + _userTextInputController.text = geminiModels.selectedModel.defaultPrompt; + + WidgetsBinding.instance.addPostFrameCallback((_) { + opController.show(); + }); + } + + @override + void didChangeDependencies() { + requestPermissions(); + super.didChangeDependencies(); + } + + @override + void dispose() { + _scrollController.dispose(); + _userTextInputController.dispose(); + super.dispose(); + } + + Future requestPermissions() async { + if (!kIsWeb) { + await Permission.manageExternalStorage.request(); + } + } + + void _scrollToEnd() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_scrollController.hasClients) { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } + }); + } + + void _pickImage() async { + final pickedImage = await ImagePicker().pickImage( + source: ImageSource.gallery, + ); + + if (pickedImage != null) { + final imageBytes = await pickedImage.readAsBytes(); + setState(() { + _attachment = imageBytes; + }); + log('attachment saved!'); + } + } + + void sendMessage(String text) async { + if (text.isEmpty) return; + + setState(() { + _loading = true; + }); + + // Add user message to UI + final userMessageText = text.trim(); + final userAttachment = _attachment; + _messages.add( + MessageData( + text: userMessageText, + image: userAttachment != null ? Image.memory(userAttachment) : null, + fromUser: true, + ), + ); + setState(() { + _attachment = null; + _userTextInputController.clear(); + }); + _scrollToEnd(); + + // Construct the Content object for the service + final content = (userAttachment != null) + ? Content.multi([ + TextPart(userMessageText), + InlineDataPart('image/jpeg', userAttachment), + ]) + : Content.text(userMessageText); + + // Call the service and handle the response + try { + final chatResponse = await _chatService.sendMessage(content); + _messages.add( + MessageData( + text: chatResponse.text, + image: chatResponse.image, + fromUser: false, + ), + ); + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(e.toString()), + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); + } + } finally { + setState(() { + _loading = false; + }); + _scrollToEnd(); + } + } + + void showModelPicker() { + opController.hide(); + showDialog( + context: context, + builder: (context) { + return ModelPicker( + selectedModel: geminiModels.selectedModel, + onSelected: (value) { + _chatService.changeModel(value); + setState(() { + _userTextInputController.text = + geminiModels.selectedModel.defaultPrompt; + _messages.clear(); + }); + }, + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.transparent, + title: const Text('Chat Demo'), + actions: [ + OverlayPortal( + controller: opController, + child: IconButton( + onPressed: showModelPicker, + icon: Icon(Icons.settings_outlined), + ), + overlayChildBuilder: (context) { + return Positioned( + right: 0, + top: 40, + child: Dialog( + insetAnimationDuration: Duration(milliseconds: 2000), + constraints: BoxConstraints(maxWidth: 500), + insetPadding: EdgeInsets.all(8), + child: Padding( + padding: EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [Text('Try another model!')], + ), + ), + ), + ); + }, + ), + ], + ), + body: AppFrame( + child: Padding( + padding: const EdgeInsets.all(AppSpacing.s16), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: MessageListView( + messages: _messages, + scrollController: _scrollController, + ), + ), + if (_loading) const LinearProgressIndicator(), + AttachmentPreview(attachment: _attachment), + ], + ), + ), + ), + ), + bottomNavigationBar: MessageInputBar( + textController: _userTextInputController, + loading: _loading, + sendMessage: sendMessage, + onPickImagePressed: _pickImage, + ), + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/chat/firebaseai_chat_service.dart b/firebase_ai_logic_showcase/lib/demos/chat/firebaseai_chat_service.dart new file mode 100644 index 0000000..75b5b0a --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/chat/firebaseai_chat_service.dart @@ -0,0 +1,121 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:developer'; +import 'package:firebase_ai/firebase_ai.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../shared/app_state.dart'; +import '../../shared/firebaseai_imagen_service.dart'; +import './models/models.dart'; + +/// A service that handles all communication with the Firebase AI Gemini API +/// for the Chat Demo. +/// +/// This service demonstrates how to use the `startChat()` method on a +/// `GenerativeModel` to create a persistent conversation. The `ChatSession` +/// object automatically handles the conversation history, making it easy to +/// build multi-turn chat experiences. +/// +/// For more information, see the official documentation: +/// https://firebase.google.com/docs/ai-logic/chat?api=dev +class ChatService { + final WidgetRef _ref; + ChatService(this._ref); + + GeminiModel? _gemini = geminiModels.selectedModel; + late ChatSession _chat; + + void init() { + var gemini = _gemini; + if (gemini != null) { + _chat = gemini.model.startChat(); + } + } + + void changeModel(String modelName) { + _gemini = geminiModels.selectModel(modelName); + init(); + } + + Future sendMessage(Content message) async { + try { + var response = await _chat.sendMessage(message); + + if (response.functionCalls.isNotEmpty) { + return _handleFunctionCall(response.functionCalls); + } else { + if (response.inlineDataParts.isNotEmpty) { + final imageBytes = response.inlineDataParts.first.bytes; + var image = Image.memory(imageBytes); + return ChatResponse(text: response.text, image: image); + } + + return ChatResponse(text: response.text); + } + } catch (e) { + log('Error sending message: $e'); + rethrow; + } + } + + Future _handleFunctionCall( + Iterable functionCalls, + ) async { + var functionCall = functionCalls.first; + log("Gemini made a function call: ${functionCall.name}"); + + switch (functionCall.name) { + case 'SetAppColor': + final response = await _handleSetAppColor(functionCall); + return ChatResponse(text: response.text); + case 'GenerateImage': + return await _handleGenerateImage(functionCall); + default: + final response = await _chat.sendMessage( + Content.text( + 'Function Call name was not found! Please try another function call.', + ), + ); + return ChatResponse(text: response.text); + } + } + + Future _handleSetAppColor( + FunctionCall functionCall, + ) async { + log('Set app color!'); + int red = functionCall.args['red']! as int; + int green = functionCall.args['green']! as int; + int blue = functionCall.args['blue']! as int; + var newSeedColor = Color.fromRGBO(red, green, blue, 1); + var executedFunctionCall = _ref + .read(appStateProvider) + .setAppColor(newSeedColor); + return await _chat.sendMessage(Content.text(executedFunctionCall)); + } + + Future _handleGenerateImage(FunctionCall functionCall) async { + log('Generate image!'); + String description = functionCall.args['description']! as String; + var imageBytes = await ImageGenerationService().generateImage(description); + var response = await _chat.sendMessage( + Content.text( + 'Successfully generated an image of $description! Please send back a message to include with the image.', + ), + ); + var responseImage = Image.memory(imageBytes); + return ChatResponse(text: response.text, image: responseImage); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/chat/models/chat_response.dart b/firebase_ai_logic_showcase/lib/demos/chat/models/chat_response.dart new file mode 100644 index 0000000..ebdb9de --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/chat/models/chat_response.dart @@ -0,0 +1,9 @@ +import 'package:flutter/material.dart'; + +/// A simple container for the response from the ChatService. +class ChatResponse { + final String? text; + final Image? image; + + ChatResponse({this.text, this.image}); +} diff --git a/firebase_ai_logic_showcase/lib/demos/chat/models/gemini_model.dart b/firebase_ai_logic_showcase/lib/demos/chat/models/gemini_model.dart new file mode 100644 index 0000000..8b892af --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/chat/models/gemini_model.dart @@ -0,0 +1,71 @@ +import 'package:firebase_ai/firebase_ai.dart'; +import '../../../shared/function_calling/tools.dart'; + +var geminiModels = GeminiModels(); + +class GeminiModel { + final String name; + final String description; + final GenerativeModel model; + final String defaultPrompt; + + GeminiModel({ + required this.name, + required this.description, + required this.model, + required this.defaultPrompt, + }); +} + +class GeminiModels { + String selectedModelName = 'gemini-2.5-flash-image-preview'; + GeminiModel get selectedModel => models[selectedModelName]!; + + /// A map of Gemini models that can be used in the Chat Demo. + Map models = { + 'gemini-2.5-flash': GeminiModel( + name: 'gemini-2.5-flash', + description: + 'Our thinking model that offers great, well-rounded capabilities. It\'s designed to offer a balance between price and performance.', + model: FirebaseAI.googleAI().generativeModel( + model: 'gemini-2.5-flash', + tools: [ + Tool.functionDeclarations([setAppColorTool, generateImageTool]), + ], + generationConfig: GenerationConfig( + responseModalities: [ResponseModalities.text], + ), + ), + defaultPrompt: 'Hey Gemini! Can you set the app color to purple?', + ), + 'gemini-2.5-flash-image-preview': GeminiModel( + name: 'gemini-2.5-flash-image-preview', + description: + 'Our standard Flash model upgraded for rapid creative workflows with image generation and conversational, multi-turn editing capabilities.', + model: FirebaseAI.googleAI().generativeModel( + model: 'gemini-2.5-flash-image-preview', + generationConfig: GenerationConfig( + responseModalities: [ + ResponseModalities.text, + ResponseModalities.image, + ], + ), + ), + defaultPrompt: + 'Hot air balloons rising over the San Francisco Bay at golden hour ' + 'with a view of the Golden Gate Bridge. Make it anime style.', + ), + }; + + GeminiModel selectModel(String modelName) { + if (models.containsKey(modelName)) { + selectedModelName = modelName; + } else { + throw Exception('Model $modelName not found'); + } + return selectedModel; + } + + List get modelNames => models.keys.toList(); + GeminiModel operator [](String name) => models[name]!; +} diff --git a/firebase_ai_logic_showcase/lib/demos/chat/models/models.dart b/firebase_ai_logic_showcase/lib/demos/chat/models/models.dart new file mode 100644 index 0000000..bebb95e --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/chat/models/models.dart @@ -0,0 +1,2 @@ +export './chat_response.dart'; +export './gemini_model.dart'; diff --git a/firebase_ai_logic_showcase/lib/demos/chat/ui_components/attachment_preview.dart b/firebase_ai_logic_showcase/lib/demos/chat/ui_components/attachment_preview.dart new file mode 100644 index 0000000..18fed23 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/chat/ui_components/attachment_preview.dart @@ -0,0 +1,47 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import '../../../shared/ui/app_spacing.dart'; + +class AttachmentPreview extends StatelessWidget { + final Uint8List? attachment; + + const AttachmentPreview({super.key, this.attachment}); + + @override + Widget build(BuildContext context) { + return attachment != null + ? Row( + children: [ + Padding( + padding: const EdgeInsets.only(top: AppSpacing.s16), + child: Container( + height: 95, + width: 95, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(AppSpacing.s8), + image: DecorationImage( + fit: BoxFit.cover, + image: MemoryImage(attachment!), + ), + ), + ), + ), + ], + ) + : Container(); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/chat/ui_components/message_bubble.dart b/firebase_ai_logic_showcase/lib/demos/chat/ui_components/message_bubble.dart new file mode 100644 index 0000000..def2442 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/chat/ui_components/message_bubble.dart @@ -0,0 +1,70 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import './message_widget.dart'; + +class MessageBubble extends StatelessWidget { + final MessageData message; + + const MessageBubble({super.key, required this.message}); + + @override + Widget build(BuildContext context) { + final isFromUser = message.fromUser ?? false; + return ListTile( + minVerticalPadding: 4, + shape: RoundedSuperellipseBorder( + borderRadius: BorderRadiusGeometry.circular(16), + ), + contentPadding: isFromUser + ? EdgeInsets.only(left: 16, top: 8, right: 8, bottom: 8) + : EdgeInsets.only(left: 8, top: 8, right: 16, bottom: 8), + leading: (!isFromUser) + ? CircleAvatar( + backgroundColor: Colors.transparent, + child: Image.asset('assets/gemini-logo.png'), + ) + : null, + title: Container( + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + color: isFromUser + ? Theme.of(context).colorScheme.surfaceBright + : Theme.of(context).colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + children: [ + if (message.text != null) + Align( + alignment: Alignment.centerLeft, + child: MarkdownBody(data: message.text!), + ), + if (message.image != null) + Padding( + padding: EdgeInsets.only(top: 8), + child: ClipRSuperellipse( + borderRadius: BorderRadius.circular(16), + child: message.image!, + ), + ), + ], + ), + ), + ).animate().fadeIn().slideY().scaleXY(); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/chat/ui_components/message_input_bar.dart b/firebase_ai_logic_showcase/lib/demos/chat/ui_components/message_input_bar.dart new file mode 100644 index 0000000..1ecd2d6 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/chat/ui_components/message_input_bar.dart @@ -0,0 +1,93 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import '../../../shared/ui/app_spacing.dart'; + +class MessageInputBar extends StatelessWidget { + final TextEditingController textController; + final bool loading; + final void Function(String) sendMessage; + final VoidCallback onPickImagePressed; + + const MessageInputBar({ + super.key, + required this.textController, + required this.loading, + required this.sendMessage, + required this.onPickImagePressed, + }); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Container( + padding: const EdgeInsets.all(AppSpacing.s16), + decoration: BoxDecoration( + border: Border( + top: BorderSide( + width: 1, + color: Theme.of(context).colorScheme.outline.withAlpha(125), + ), + ), + ), + child: Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.maybeViewInsetsOf(context)?.bottom ?? 0, + ), + child: SafeArea( + child: Row( + children: [ + IconButton( + onPressed: onPickImagePressed, + icon: const Icon(Icons.image), + ), + const SizedBox.square(dimension: AppSpacing.s8), + Expanded( + child: TextField( + onTapOutside: (PointerDownEvent event) { + FocusManager.instance.primaryFocus?.unfocus(); + }, + controller: textController, + minLines: 2, + maxLines: 2, + decoration: InputDecoration( + border: OutlineInputBorder( + borderSide: const BorderSide(width: 0), + borderRadius: BorderRadius.all( + Radius.circular(AppSpacing.s8), + ), + ), + filled: true, + fillColor: Theme.of( + context, + ).colorScheme.surfaceContainerHigh, + ), + ), + ), + const SizedBox.square(dimension: AppSpacing.s16), + IconButton.filled( + onPressed: !loading + ? () => sendMessage(textController.text) + : null, + icon: const Icon(Icons.arrow_upward_rounded), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/chat/ui_components/message_list_view.dart b/firebase_ai_logic_showcase/lib/demos/chat/ui_components/message_list_view.dart new file mode 100644 index 0000000..b78d8a0 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/chat/ui_components/message_list_view.dart @@ -0,0 +1,40 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import './message_bubble.dart'; +import './message_widget.dart'; + +class MessageListView extends StatelessWidget { + final List messages; + final ScrollController scrollController; + + const MessageListView({ + super.key, + required this.messages, + required this.scrollController, + }); + + @override + Widget build(BuildContext context) { + return ListView.builder( + controller: scrollController, + itemCount: messages.length, + itemBuilder: (context, idx) { + final message = messages[idx]; + return MessageBubble(message: message); + }, + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/chat/ui_components/message_widget.dart b/firebase_ai_logic_showcase/lib/demos/chat/ui_components/message_widget.dart new file mode 100644 index 0000000..92fdf86 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/chat/ui_components/message_widget.dart @@ -0,0 +1,78 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:firebase_ai/firebase_ai.dart'; +import '../../../shared/ui/app_spacing.dart'; + +class FunctionCallResponse { + final GenerateContentResponse? response; + final Image? image; + + FunctionCallResponse(this.response, this.image); +} + +class MessageData { + MessageData({this.image, this.text, this.fromUser}); + final Image? image; + final String? text; + final bool? fromUser; +} + +class MessageWidget extends StatelessWidget { + final Image? image; + final String? text; + final bool isFromUser; + + const MessageWidget({ + super.key, + this.image, + this.text, + required this.isFromUser, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: isFromUser + ? MainAxisAlignment.end + : MainAxisAlignment.start, + children: [ + Flexible( + child: Container( + constraints: const BoxConstraints(maxWidth: 600), + decoration: BoxDecoration( + color: isFromUser + ? Theme.of(context).colorScheme.primaryContainer + : Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(AppSpacing.s16), + ), + padding: const EdgeInsets.symmetric( + vertical: AppSpacing.s16, + horizontal: AppSpacing.s24, + ), + margin: const EdgeInsets.only(bottom: AppSpacing.s8), + child: Column( + children: [ + if (text case final text?) MarkdownBody(data: text), + if (image case final image?) image, + ], + ), + ), + ), + ], + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/chat/ui_components/model_picker.dart b/firebase_ai_logic_showcase/lib/demos/chat/ui_components/model_picker.dart new file mode 100644 index 0000000..ab7218e --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/chat/ui_components/model_picker.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import '../../../shared/ui/app_spacing.dart'; +import '../../../shared/ui/blaze_warning.dart'; +import '../models/gemini_model.dart'; + +class ModelPicker extends StatefulWidget { + const ModelPicker({ + required this.selectedModel, + required this.onSelected, + super.key, + }); + + final GeminiModel selectedModel; + final Function(String value) onSelected; + + @override + State createState() => _ModelPickerState(); +} + +class _ModelPickerState extends State { + late String _selectedModelName; + late String _selectedModelDescription; + + @override + void initState() { + super.initState(); + _selectedModelName = widget.selectedModel.name; + _selectedModelDescription = widget.selectedModel.description; + } + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Theme.of(context).colorScheme.surface, + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: 600), + child: Padding( + padding: const EdgeInsets.all(AppSpacing.s16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + DropdownMenu( + label: const Text('Select a Gemini Model'), + initialSelection: _selectedModelName, + inputDecorationTheme: InputDecorationTheme( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + ), + fillColor: Theme.of(context).colorScheme.primaryContainer, + ), + dropdownMenuEntries: geminiModels.models.entries + .map( + (entry) => + DropdownMenuEntry(value: entry.key, label: entry.key), + ) + .toList(), + onSelected: (value) { + if (value != null) { + setState(() { + _selectedModelName = value; + _selectedModelDescription = + geminiModels[_selectedModelName].description; + }); + widget.onSelected(value); + } + }, + ), + const SizedBox.square(dimension: AppSpacing.s8), + Padding( + padding: const EdgeInsets.all(AppSpacing.s8), + child: Text( + textAlign: TextAlign.center, + _selectedModelDescription, + ), + ), + if (_selectedModelName.contains('preview')) BlazeWarning(), + ], + ), + ), + ), + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/chat/ui_components/ui_components.dart b/firebase_ai_logic_showcase/lib/demos/chat/ui_components/ui_components.dart new file mode 100644 index 0000000..8572431 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/chat/ui_components/ui_components.dart @@ -0,0 +1,18 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export 'attachment_preview.dart'; +export 'message_input_bar.dart'; +export 'message_list_view.dart'; +export 'message_widget.dart'; diff --git a/firebase_ai_logic_showcase/lib/demos/imagen/imagen_demo.dart b/firebase_ai_logic_showcase/lib/demos/imagen/imagen_demo.dart new file mode 100644 index 0000000..5d32ba9 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/imagen/imagen_demo.dart @@ -0,0 +1,95 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import 'dart:typed_data'; +import '../../shared/ui/app_frame.dart'; +import '../../shared/ui/app_spacing.dart'; +import '../../shared/firebaseai_imagen_service.dart'; +import './ui_components/ui_components.dart'; + +class ImageGenerationDemo extends StatefulWidget { + const ImageGenerationDemo({super.key}); + + @override + State createState() => _ImageGenerationDemoState(); +} + +class _ImageGenerationDemoState extends State { + // Service for interacting with the Gemini API. + final _imagenService = ImageGenerationService(); + + // UI State + bool _loading = false; + List images = []; + TextEditingController promptController = TextEditingController( + text: + 'Hot air balloons rising over the San Francisco Bay at golden hour ' + 'with a view of the Golden Gate Bridge. Make it anime style.', + ); + + void generateImages(BuildContext context, String prompt) async { + setState(() { + _loading = true; + images = []; // Clear previous images while loading + }); + + try { + final image = await _imagenService.generateImage(prompt); + setState(() { + images = [image]; + }); + } catch (e) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(e.toString()), + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); + } finally { + setState(() { + _loading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('Image Generation Demo')), + body: SingleChildScrollView( + padding: EdgeInsets.only( + bottom: MediaQuery.viewInsetsOf(context).bottom, + ), + child: AppFrame( + child: Padding( + padding: const EdgeInsets.all(AppSpacing.s8), + child: Column( + children: [ + ImageDisplay(loading: _loading, images: images), + const SizedBox.square(dimension: AppSpacing.s8), + PromptInput( + promptController: promptController, + loading: _loading, + generateImages: generateImages, + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/imagen/ui_components/image_display.dart b/firebase_ai_logic_showcase/lib/demos/imagen/ui_components/image_display.dart new file mode 100644 index 0000000..01224ab --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/imagen/ui_components/image_display.dart @@ -0,0 +1,67 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:typed_data'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import '../../../shared/ui/app_spacing.dart'; +import '../../../shared/ui/blaze_warning.dart'; + +class ImageDisplay extends StatelessWidget { + final bool loading; + final List images; + + const ImageDisplay({super.key, required this.loading, required this.images}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(AppSpacing.s4), + child: LayoutBuilder( + builder: (context, constraints) { + return ConstrainedBox( + constraints: BoxConstraints.loose( + Size(double.infinity, constraints.maxWidth), + ), + child: Center( + child: loading + ? CircularProgressIndicator() + : images.isEmpty + ? Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyLarge, + 'Write a prompt below to generate images.', + ), + SizedBox.square(dimension: AppSpacing.s8), + BlazeWarning(), + ], + ) + : CarouselView.weighted( + enableSplash: false, + itemSnapping: true, + flexWeights: [1, 6, 1], + children: images + .map((image) => Image.memory(image)) + .toList(), + ), + ), + ); + }, + ), + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/imagen/ui_components/prompt_input.dart b/firebase_ai_logic_showcase/lib/demos/imagen/ui_components/prompt_input.dart new file mode 100644 index 0000000..b203ba2 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/imagen/ui_components/prompt_input.dart @@ -0,0 +1,92 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import '../../../shared/ui/app_spacing.dart'; + +class PromptInput extends StatelessWidget { + final TextEditingController promptController; + final bool loading; + final void Function(BuildContext, String) generateImages; + + const PromptInput({ + super.key, + required this.promptController, + required this.loading, + required this.generateImages, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: AppSpacing.s8), + child: TextField( + decoration: InputDecoration( + label: const Text('Prompt'), + fillColor: Theme.of(context).colorScheme.onSecondaryFixed, + filled: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppSpacing.s16), + ), + ), + maxLines: 4, + controller: promptController, + enabled: !loading, + onTap: () { + promptController.selection = TextSelection( + baseOffset: 0, + extentOffset: promptController.text.length, + ); + }, + ), + ), + ), + Padding( + padding: const EdgeInsets.all(AppSpacing.s8), + child: ElevatedButton( + style: ButtonStyle( + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppSpacing.s16), + ), + ), + backgroundColor: WidgetStatePropertyAll( + Theme.of(context).colorScheme.primaryContainer, + ), + ), + onPressed: loading + ? null + : () => generateImages(context, promptController.text), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: AppSpacing.s24, + horizontal: 0, + ), + child: Column( + children: [ + const Icon(size: 32, Icons.brush), + const SizedBox.square(dimension: AppSpacing.s8), + const Text(textAlign: TextAlign.center, 'Create\nImage'), + ], + ), + ), + ), + ), + ], + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/imagen/ui_components/ui_components.dart b/firebase_ai_logic_showcase/lib/demos/imagen/ui_components/ui_components.dart new file mode 100644 index 0000000..759ffad --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/imagen/ui_components/ui_components.dart @@ -0,0 +1,16 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export 'image_display.dart'; +export 'prompt_input.dart'; diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/firebaseai_live_api_service.dart b/firebase_ai_logic_showcase/lib/demos/live_api/firebaseai_live_api_service.dart new file mode 100644 index 0000000..c502657 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/firebaseai_live_api_service.dart @@ -0,0 +1,225 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; +import 'dart:developer'; +import 'dart:typed_data'; +import 'package:firebase_ai/firebase_ai.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../shared/app_state.dart'; +import '../../shared/firebaseai_imagen_service.dart'; +import '../../shared/function_calling/tools.dart'; +import 'utilities/audio_output.dart'; + +/// A service that handles all communication with the Firebase AI Gemini Live API. +/// +/// This service demonstrates how to use the `liveGenerativeModel()` to create +/// a real-time, bidirectional audio & video stream with Gemini. It manages the +/// `LiveSession` and processes streaming responses, including tool calls. +/// +/// For more information, see the official documentation: +/// https://firebase.google.com/docs/ai-logic/live-api?api=dev +class LiveApiService { + final AudioOutput _audioOutput; + final WidgetRef _ref; + // Callbacks for UI updates handled by setState + final void Function(bool isLoading) onImageLoadingChange; + final void Function(Uint8List imageBytes) onImageGenerated; + final void Function(String error) onError; + + LiveApiService({ + required AudioOutput audioOutput, + required WidgetRef ref, + required this.onImageLoadingChange, + required this.onImageGenerated, + required this.onError, + }) : _audioOutput = audioOutput, + _ref = ref; + + final LiveGenerativeModel + _liveModel = FirebaseAI.googleAI().liveGenerativeModel( + systemInstruction: Content.text( + 'You are a helpful assisant. If you have a tool to help the user, please use it.', + ), + model: 'gemini-2.0-flash-live-001', + liveGenerationConfig: LiveGenerationConfig( + speechConfig: SpeechConfig(voiceName: 'fenrir'), + responseModalities: [ResponseModalities.audio], + ), + tools: [ + Tool.functionDeclarations([ + setAppColorTool, + // Gemini Flash Image currently requires the pay-as-you-go Blaze plan. + generateImageTool, + ]), + ], + ); + + late LiveSession _session; + bool _liveSessionIsOpen = false; + + Future connect() async { + if (_liveSessionIsOpen) return; + try { + _session = await _liveModel.connect(); + _liveSessionIsOpen = true; + unawaited(processMessagesContinuously()); + } catch (e) { + log('Error connecting to live session: $e'); + onError('Failed to start the call. Please try again.'); + } + } + + Future close() async { + if (!_liveSessionIsOpen) return; + try { + await _session.close(); + } catch (e) { + log('Error closing live session: $e'); + // Don't necessarily need to show an error to the user on close. + } finally { + _liveSessionIsOpen = false; + } + } + + bool get isSessionOpen => _liveSessionIsOpen; + + void sendMediaStream(Stream stream) { + if (!_liveSessionIsOpen) return; + _session.sendMediaStream(stream); + } + + Future processMessagesContinuously() async { + try { + await for (final response in _session.receive()) { + LiveServerMessage message = response.message; + await _handleLiveServerMessage(message); + } + log('Live session receive stream completed.'); + } catch (e) { + log('Error receiving live session messages: $e'); + onError('Something went wrong during the call. Please try again.'); + } + } + + Future _handleLiveServerMessage(LiveServerMessage response) async { + if (response is LiveServerContent) { + if (response.modelTurn != null) { + await _handleLiveServerContent(response); + } + if (response.turnComplete != null && response.turnComplete!) { + await _handleTurnComplete(); + } + if (response.interrupted != null && response.interrupted!) { + log('Interrupted: $response'); + } + } + + if (response is LiveServerToolCall && response.functionCalls != null) { + await _handleLiveServerToolCall(response); + } + } + + Future _handleLiveServerContent(LiveServerContent response) async { + final partList = response.modelTurn?.parts; + if (partList != null) { + for (final part in partList) { + switch (part) { + case TextPart textPart: + await _handleTextPart(textPart); + case InlineDataPart inlineDataPart: + await _handleInlineDataPart(inlineDataPart); + default: + log('Received part with type ${part.runtimeType}'); + } + } + } + } + + Future _handleInlineDataPart(InlineDataPart part) async { + if (part.mimeType.startsWith('audio')) { + _audioOutput.addDataToAudioStream(part.bytes); + } + } + + Future _handleTextPart(TextPart part) async { + log('Text message from Gemini: ${part.text}'); + } + + Future _handleTurnComplete() async { + log('Model is done generating. Turn complete!'); + final halfSecondOfSilence = Uint8List(24000); + _audioOutput.addDataToAudioStream(halfSecondOfSilence); + } + + Future _handleLiveServerToolCall(LiveServerToolCall response) async { + var functionCalls = response.functionCalls; + if (functionCalls == null || functionCalls.isEmpty) return; + + // The API currently only supports one function call per turn. + var functionCall = functionCalls.first; + log("Gemini made a function call: ${functionCall.name}"); + + switch (functionCall.name) { + case 'GenerateImage': + await _handleGenerateImage(functionCall); + break; + case 'SetAppColor': + _handleSetAppColor(functionCall); + break; + default: + log('Unknown function call: ${functionCall.name}'); + } + } + + Future _handleGenerateImage(FunctionCall functionCall) async { + onImageLoadingChange(true); + try { + final imageDescription = functionCall.args['description']?.toString(); + if (imageDescription == null) { + onError('Image generation failed: No description provided.'); + return; + } + final image = await ImageGenerationService().generateImage( + imageDescription, + ); + onImageGenerated(image); + } catch (e) { + log('Error generating image: $e'); + onError('Sorry, the image could not be generated.'); + } finally { + onImageLoadingChange(false); + } + } + + void _handleSetAppColor(FunctionCall functionCall) { + try { + final red = functionCall.args['red']! as int; + final green = functionCall.args['green']! as int; + final blue = functionCall.args['blue']! as int; + final newSeedColor = Color.fromRGBO(red, green, blue, 1); + _ref.read(appStateProvider).setAppColor(newSeedColor); + } catch (e) { + log('Error setting app color from tool call: $e'); + onError('Sorry, there was an error applying the color.'); + } + } + + void dispose() { + if (_liveSessionIsOpen) { + unawaited(close()); + } + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/live_api_demo.dart b/firebase_ai_logic_showcase/lib/demos/live_api/live_api_demo.dart new file mode 100644 index 0000000..3fc5fac --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/live_api_demo.dart @@ -0,0 +1,283 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; +import 'dart:developer'; +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:firebase_ai/firebase_ai.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'ui_components/ui_components.dart'; +import 'utilities/utilities.dart'; +import 'firebaseai_live_api_service.dart'; + +class LiveAPIDemo extends ConsumerStatefulWidget { + const LiveAPIDemo({super.key}); + + @override + ConsumerState createState() => _LiveAPIDemoState(); +} + +/// The main state for the Live API demo. +/// +/// This stateful widget orchestrates the UI and manages the state for the demo, +/// including handling user input, managing the call lifecycle, and coordinating +/// with the [LiveApiService] and I/O utilities. +class _LiveAPIDemoState extends ConsumerState { + // Service for interacting with the Gemini API via Firebase AI. + late final LiveApiService _liveApiService; + + // Utilities for handling device I/O. + late final AudioInput _audioInput = AudioInput(); + late final AudioOutput _audioOutput = AudioOutput(); + late final VideoInput _videoInput = VideoInput(); + + // Initialization flags. + bool _audioIsInitialized = false; + bool _videoIsInitialized = false; + + // UI State flags. + bool _isConnecting = false; // True when setting up the Gemini session. + bool _isCallActive = false; // True when the audio stream is active. + bool _cameraIsActive = false; // True when sending video to Gemini. + bool _loadingImage = false; // True when waiting for an image to be generated. + + @override + void initState() { + super.initState(); + _liveApiService = LiveApiService( + audioOutput: _audioOutput, + ref: ref, // Pass the ref to the service + onImageLoadingChange: _onImageLoadingChange, + onImageGenerated: _onImageGenerated, + onError: _showErrorSnackBar, + ); + + WidgetsBinding.instance.addPostFrameCallback((_) { + _initializeAudio(); + _initializeVideo(); + }); + } + + @override + void dispose() { + _audioInput.dispose(); + _audioOutput.dispose(); + _videoInput.dispose(); + _liveApiService.dispose(); + super.dispose(); + } + + void _showErrorSnackBar(String message) { + if (!mounted) return; + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(message))); + } + + //================================================================================ + // UI Callbacks + //================================================================================ + + void _onImageLoadingChange(bool isLoading) { + setState(() { + _loadingImage = isLoading; + }); + } + + void _onImageGenerated(Uint8List imageBytes) { + if (!mounted) return; + showDialog( + context: context, + builder: (context) { + return GeneratedImageDialog(imageBytes: imageBytes); + }, + ); + } + + //================================================================================ + // Call Lifecycle + //================================================================================ + + void toggleCall() async { + _isCallActive ? await stopCall() : await startCall(); + } + + Future startCall() async { + // Initialize the camera controller here to ensure it's fresh for each call. + // This prevents a bug where the camera preview freezes on subsequent calls. + if (_videoIsInitialized) { + await _videoInput.initializeCameraController(); + } + + setState(() { + _isConnecting = true; + }); + + await _liveApiService.connect(); + + setState(() { + _isConnecting = false; + }); + + var audioInputStream = await _audioInput.startRecordingStream(); + log('Audio input stream is recording!'); + + await _audioOutput.playStream(); + log('Audio output stream is playing!'); + + setState(() { + _isCallActive = true; + }); + + _liveApiService.sendMediaStream( + audioInputStream.map((data) { + return InlineDataPart('audio/pcm', data); + }), + ); + } + + Future stopCall() async { + if (_cameraIsActive) { + stopVideoStream(); + } + await _audioInput.stopRecording(); + await _audioOutput.stopStream(); + + setState(() { + _isConnecting = true; + }); + + await _liveApiService.close(); + + setState(() { + _isConnecting = false; + _isCallActive = false; + }); + } + + //================================================================================ + // I/O Initialization and Control + //================================================================================ + + Future _initializeAudio() async { + try { + await _audioInput.init(); // Initialize Audio Input + await _audioOutput.init(); // Initialize Audio Output + + setState(() { + _audioIsInitialized = true; + }); + } catch (e) { + log("Error during audio initialization: $e"); + if (!mounted) return; + + var errorSnackBar = SnackBar( + content: const Text('Oops! Something went wrong with the audio setup.'), + action: SnackBarAction(label: 'Retry', onPressed: _initializeAudio), + ); + ScaffoldMessenger.of(context).showSnackBar(errorSnackBar); + } + } + + Future _initializeVideo() async { + try { + await _videoInput.init(); + setState(() { + _videoIsInitialized = true; + }); + } catch (e) { + log("Error during video initialization: $e"); + } + } + + void startVideoStream() { + if (!_videoIsInitialized || !_isCallActive || _cameraIsActive) { + return; + } + + Stream imageStream = _videoInput.startStreamingImages(); + + _liveApiService.sendMediaStream( + imageStream.map((data) { + return InlineDataPart("image/jpeg", data); + }), + ); + + setState(() { + _cameraIsActive = true; + }); + } + + void stopVideoStream() async { + await _videoInput.stopStreamingImages(); + setState(() { + _cameraIsActive = false; + }); + } + + void toggleVideoStream() async { + _cameraIsActive ? stopVideoStream() : startVideoStream(); + } + + Future toggleMuteInput() async { + await _audioInput.togglePauseRecording(); + setState(() {}); // Rebuild mute button icon + } + + //================================================================================ + // Build Method + //================================================================================ + + @override + Widget build(BuildContext context) { + final audioInput = _audioInput; + final videoInput = _videoInput; + + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.surface, + appBar: const LiveApiDemoAppBar(), + body: LiveApiBody( + cameraIsActive: _cameraIsActive, + cameraController: videoInput.controllerInitialized + ? videoInput.cameraController + : null, + settingUpLiveSession: _isConnecting, + loadingImage: _loadingImage, + ), + bottomNavigationBar: BottomBar( + children: [ + FlipCameraButton( + onPressed: _cameraIsActive && videoInput.cameras.length > 1 + ? videoInput.flipCamera + : null, + ), + VideoButton(isActive: _cameraIsActive, onPressed: toggleVideoStream), + AudioVisualizer( + audioStreamIsActive: _isCallActive, + amplitudeStream: audioInput.amplitudeStream, + ), + MuteButton( + isMuted: audioInput.isPaused, + onPressed: _isCallActive ? toggleMuteInput : null, + ), + CallButton( + isActive: _isCallActive, + onPressed: _audioIsInitialized ? toggleCall : null, + ), + ], + ), + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/audio_visualizer.dart b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/audio_visualizer.dart new file mode 100644 index 0000000..bbb1bd8 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/audio_visualizer.dart @@ -0,0 +1,40 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import 'package:waveform_flutter/waveform_flutter.dart'; +import 'sound_waves.dart'; + +class AudioVisualizer extends StatelessWidget { + const AudioVisualizer({ + super.key, + required this.audioStreamIsActive, + this.amplitudeStream, + }); + + final bool audioStreamIsActive; + final Stream? amplitudeStream; + + @override + Widget build(BuildContext context) { + return (audioStreamIsActive && amplitudeStream != null) + ? Expanded( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: Soundwaves(amplitudeStream: amplitudeStream!), + ), + ) + : const Spacer(); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/bottom_bar.dart b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/bottom_bar.dart new file mode 100644 index 0000000..3d363ce --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/bottom_bar.dart @@ -0,0 +1,146 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import '../../../shared/ui/app_spacing.dart'; + +class BottomBar extends StatelessWidget { + const BottomBar({required this.children, super.key}); + + final List children; + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Container( + decoration: BoxDecoration( + border: Border( + top: BorderSide( + color: Theme.of(context).colorScheme.surfaceContainer, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.all(AppSpacing.s16), + child: Row(children: children), + ), + ), + ); + } +} + +class FlipCameraButton extends StatelessWidget { + const FlipCameraButton({this.onPressed, super.key}); + + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(AppSpacing.s4), + child: IconButton.filledTonal( + onPressed: onPressed, + icon: const Padding( + padding: EdgeInsets.all(AppSpacing.s4), + child: Icon(Icons.flip_camera_ios_outlined), + ), + ), + ); + } +} + +class VideoButton extends StatelessWidget { + const VideoButton({required this.isActive, this.onPressed, super.key}); + + final bool isActive; + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(AppSpacing.s4), + child: IconButton.filledTonal( + style: isActive + ? ButtonStyle( + backgroundColor: WidgetStateProperty.all( + const Color.fromARGB(240, 238, 255, 244), + ), + iconColor: WidgetStateProperty.all(Colors.black87), + ) + : const ButtonStyle(backgroundColor: null), + onPressed: onPressed, + icon: const Padding( + padding: EdgeInsets.all(AppSpacing.s4), + child: Icon(Icons.video_call_rounded), + ), + ), + ); + } +} + +class MuteButton extends StatelessWidget { + const MuteButton({required this.isMuted, this.onPressed, super.key}); + + final bool isMuted; + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(AppSpacing.s4), + child: IconButton.filledTonal( + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + isMuted ? null : const Color.fromARGB(240, 238, 255, 244), + ), + ), + onPressed: onPressed, + icon: Padding( + padding: const EdgeInsets.all(AppSpacing.s4), + child: isMuted + ? const Icon(Icons.mic_off) + : const Icon(color: Colors.black87, Icons.mic_none), + ), + ), + ); + } +} + +class CallButton extends StatelessWidget { + const CallButton({required this.isActive, this.onPressed, super.key}); + + final bool isActive; + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(AppSpacing.s4), + child: IconButton.filledTonal( + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + isActive + ? const Color.fromARGB(255, 199, 39, 27) + : Colors.green[500], + ), + ), + onPressed: onPressed, + icon: Padding( + padding: const EdgeInsets.all(AppSpacing.s4), + child: Icon(isActive ? Icons.phone_disabled_outlined : Icons.phone), + ), + ), + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/camera_previews.dart b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/camera_previews.dart new file mode 100644 index 0000000..9715c65 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/camera_previews.dart @@ -0,0 +1,91 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:camera/camera.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:flutter/material.dart'; +import '../../../shared/ui/app_spacing.dart'; + +class SquareCameraPreview extends StatelessWidget { + const SquareCameraPreview({required this.controller, super.key}); + + final CameraController controller; + + @override + Widget build(BuildContext context) { + return Center( + child: Container( + width: 352.0, // Adjusted from 350 to be a multiple of 4 + height: 352.0, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(AppSpacing.s16), + ), + child: AspectRatio( + aspectRatio: 1, + child: ClipRRect( + borderRadius: const BorderRadius.all( + Radius.circular(AppSpacing.s16), + ), + // The camera preview is often not a square. To fill the 1:1 aspect + // ratio, we scale the preview to cover the area and clip it. + child: Transform.scale( + scale: controller.value.aspectRatio / 1, + child: Center(child: CameraPreview(controller)), + ), + ), + ), + ), + ); + } +} + +class FullCameraPreview extends StatefulWidget { + const FullCameraPreview({required this.controller, super.key}); + + final CameraController controller; + + @override + State createState() => _FullCameraPreviewState(); +} + +class _FullCameraPreviewState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animController; + + @override + void initState() { + super.initState(); + _animController = AnimationController( + vsync: this, // the SingleTickerProviderStateMixin + duration: const Duration(seconds: 1), + ); + } + + @override + void dispose() { + super.dispose(); + _animController.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(AppSpacing.s16), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(AppSpacing.s16)), + child: CameraPreview(widget.controller), + ), + ).animate(controller: _animController).scaleXY().fadeIn(); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/generated_image_dialog.dart b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/generated_image_dialog.dart new file mode 100644 index 0000000..9592813 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/generated_image_dialog.dart @@ -0,0 +1,52 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:typed_data'; +import 'package:flutter/material.dart'; + +class GeneratedImageDialog extends StatelessWidget { + const GeneratedImageDialog({super.key, required this.imageBytes}); + + final Uint8List imageBytes; + + @override + Widget build(BuildContext context) { + return Dialog( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Center(child: Image.memory(imageBytes)), + ), + Align( + alignment: Alignment.topRight, + child: Padding( + padding: const EdgeInsets.all(8), + child: IconButton.filled( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.close), + ), + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/live_api_body.dart b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/live_api_body.dart new file mode 100644 index 0000000..7e1dd58 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/live_api_body.dart @@ -0,0 +1,62 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; +import '../../../shared/ui/app_spacing.dart'; +import 'camera_previews.dart'; +import 'sound_waves.dart'; + +class LiveApiBody extends StatelessWidget { + const LiveApiBody({ + super.key, + required this.cameraIsActive, + this.cameraController, + required this.settingUpLiveSession, + required this.loadingImage, + }); + + final bool cameraIsActive; + final CameraController? cameraController; + final bool settingUpLiveSession; + final bool loadingImage; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Expanded( + child: cameraIsActive && cameraController != null + ? Center(child: FullCameraPreview(controller: cameraController!)) + : CenterCircle( + child: Padding( + padding: const EdgeInsets.all(AppSpacing.s60), + child: settingUpLiveSession + ? const CircularProgressIndicator() + : Image.asset('assets/gemini-logo.png'), + ), + ), + ), + if (loadingImage) + const Column( + children: [ + Text('Beep. Boop. Bop. Generating your image...'), + SizedBox.square(dimension: AppSpacing.s8), + LinearProgressIndicator(semanticsLabel: 'Generating image...'), + ], + ), + ], + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/live_demo_app_bar.dart b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/live_demo_app_bar.dart new file mode 100644 index 0000000..4cf3bdb --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/live_demo_app_bar.dart @@ -0,0 +1,30 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import 'package:flutter/material.dart'; + +class LiveApiDemoAppBar extends StatelessWidget implements PreferredSizeWidget { + const LiveApiDemoAppBar({super.key}); + + @override + Widget build(BuildContext context) { + return AppBar( + backgroundColor: Colors.transparent, + leadingWidth: 100, + title: const Text('Gemini Live API Demo'), + ); + } + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); +} \ No newline at end of file diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/sound_waves.dart b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/sound_waves.dart new file mode 100644 index 0000000..a976380 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/sound_waves.dart @@ -0,0 +1,120 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:waveform_flutter/waveform_flutter.dart'; +import '../../../shared/ui/app_spacing.dart'; + +class CenterCircle extends StatelessWidget { + const CenterCircle({required this.child, super.key}); + + final Widget child; + + @override + Widget build(BuildContext context) { + return Center( + child: CustomPaint( + size: const Size(160, 160), + painter: NestedCirclesPainter( + color: Theme.of(context).colorScheme.primary, + strokeWidth: 1.0, + gapBetweenCircles: 4.0, + ), + child: child, + ), + ); + } +} + +class Soundwaves extends StatelessWidget { + const Soundwaves({required this.amplitudeStream, super.key}); + + final Stream amplitudeStream; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.s16), + child: SizedBox( + height: 48, + child: AnimatedWaveList( + stream: amplitudeStream, + barBuilder: (animation, amplitude) { + return WaveFormBar( + amplitude: amplitude, + animation: animation, + color: Theme.of(context).colorScheme.primary, + ); + }, + ), + ), + ); + } +} + +// Custom Painter for drawing two nested circles +class NestedCirclesPainter extends CustomPainter { + final Color color; + final double strokeWidth; + final double gapBetweenCircles; // The space between the two circles + + NestedCirclesPainter({ + this.color = Colors.white54, // Default color for the circles + this.strokeWidth = 1.5, // Default stroke width for both circles + this.gapBetweenCircles = 4.0, // Default gap between the circles + }); + + @override + void paint(Canvas canvas, Size size) { + // Calculate the center of the drawing area + final Offset center = Offset(size.width / 2, size.height / 2); + + // Configure the paint properties (same for both circles) + final Paint paint = Paint() + ..color = color + .withValues(alpha: 0.7) // Make circles slightly transparent + ..strokeWidth = strokeWidth + ..style = PaintingStyle.stroke; // Draw the outline + + // Calculate the radius for the outer circle + // Ensure it fits within the bounds defined by 'size' + final double outerRadius = + min(size.width / 2, size.height / 2) - strokeWidth / 2; + + // Calculate the radius for the inner circle + final double innerRadius = + outerRadius - gapBetweenCircles - strokeWidth / 2; + + // Ensure inner radius is not negative + if (innerRadius > 0) { + // Draw the outer circle + canvas.drawCircle(center, outerRadius, paint); + // Draw the inner circle + canvas.drawCircle(center, innerRadius, paint); + } else { + // If the gap is too large, just draw the outer circle + canvas.drawCircle(center, outerRadius, paint); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + // Repaint only if properties change + return oldDelegate is NestedCirclesPainter && + (oldDelegate.color != color || + oldDelegate.strokeWidth != strokeWidth || + oldDelegate.gapBetweenCircles != gapBetweenCircles); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/ui_components.dart b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/ui_components.dart new file mode 100644 index 0000000..d53ca4a --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/ui_components.dart @@ -0,0 +1,21 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export 'bottom_bar.dart'; +export 'camera_previews.dart'; +export 'sound_waves.dart'; +export 'live_api_body.dart'; +export 'audio_visualizer.dart'; +export 'live_demo_app_bar.dart'; +export 'generated_image_dialog.dart'; diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/utilities/audio_input.dart b/firebase_ai_logic_showcase/lib/demos/live_api/utilities/audio_input.dart new file mode 100644 index 0000000..bbdc482 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/utilities/audio_input.dart @@ -0,0 +1,118 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; +import 'dart:developer'; +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:record/record.dart'; +import 'package:waveform_flutter/waveform_flutter.dart' as wf; + +class AudioInput extends ChangeNotifier { + AudioRecorder? _recorder; + RecordConfig recordConfig = RecordConfig( + encoder: AudioEncoder.pcm16bits, + sampleRate: 24000, + numChannels: 1, + echoCancel: true, + noiseSuppress: true, + androidConfig: AndroidRecordConfig( + audioSource: AndroidAudioSource.voiceCommunication, + ), + iosConfig: IosRecordConfig(categoryOptions: []), + ); + bool isRecording = false; + bool isPaused = false; + late Stream audioStream; + Stream? amplitudeStream; + StreamSubscription? _amplitudeSubscription; + StreamController? _amplitudeStreamController; + + Future init() async { + _recorder = AudioRecorder(); + await checkPermission(); + } + + @override + void dispose() { + _recorder?.dispose(); + super.dispose(); + } + + Future checkPermission() async { + final hasPermission = await _recorder!.hasPermission(); + if (!hasPermission) { + throw MicrophonePermissionDeniedException( + 'This app does not have microphone permissions. Please enable it.', + ); + } + } + + Future> startRecordingStream() async { + final devices = await _recorder!.listInputDevices(); + log(devices.toString()); + audioStream = (await _recorder!.startStream( + recordConfig, + )).asBroadcastStream(); + _amplitudeStreamController = StreamController.broadcast(); + _amplitudeSubscription = _recorder! + .onAmplitudeChanged(const Duration(milliseconds: 100)) + .listen((amp) { + _amplitudeStreamController?.add( + wf.Amplitude(current: amp.current, max: amp.max), + ); + }); + amplitudeStream = _amplitudeStreamController?.stream; + isRecording = true; + //log("${isRecording ? "Is" : "Not"} Recording"); + notifyListeners(); + return audioStream; + } + + Future stopRecording() async { + await _recorder!.stop(); + isRecording = false; + await _amplitudeSubscription?.cancel(); + await _amplitudeStreamController?.close(); + amplitudeStream = null; + _recorder?.dispose(); + _recorder = AudioRecorder(); + //log("${isRecording ? "Is" : "Not"} Recording"); + notifyListeners(); + } + + Future togglePauseRecording() async { + isPaused ? await _recorder!.resume() : await _recorder!.pause(); + isPaused = !isPaused; + notifyListeners(); + return; + } +} + +/// An exception thrown when microphone permission is denied or not granted. +class MicrophonePermissionDeniedException implements Exception { + /// The optional message associated with the permission denial. + final String? message; + + /// Creates a new [MicrophonePermissionDeniedException] with an optional [message]. + MicrophonePermissionDeniedException([this.message]); + + @override + String toString() { + if (message == null) { + return 'MicrophonePermissionDeniedException'; + } + return 'MicrophonePermissionDeniedException: $message'; + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/utilities/audio_output.dart b/firebase_ai_logic_showcase/lib/demos/live_api/utilities/audio_output.dart new file mode 100644 index 0000000..052304d --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/utilities/audio_output.dart @@ -0,0 +1,98 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:developer'; +import 'dart:typed_data'; +import 'package:flutter_soloud/flutter_soloud.dart'; + +class AudioOutput { + var initialized = false; + AudioSource? stream; + SoundHandle? handle; + final int sampleRate = 24000; + final Channels channels = Channels.mono; + final BufferType format = BufferType.s16le; // pcm16bits + + Future init() async { + if (initialized) { + return; + } + + /// Initialize the player (singleton). + await SoLoud.instance.init(sampleRate: sampleRate, channels: channels); + initialized = true; + } + + Future dispose() async { + if (initialized) { + SoLoud.instance.disposeAllSources(); + SoLoud.instance.deinit(); + initialized = false; + } + } + + SoLoud get instance => SoLoud.instance; + + AudioSource? setupNewStream() { + if (!SoLoud.instance.isInitialized) { + return null; + } + + stream = SoLoud.instance.setBufferStream( + bufferingType: BufferingType.released, + bufferingTimeNeeds: 0, + sampleRate: sampleRate, + channels: channels, + format: format, + onBuffering: (isBuffering, handle, time) { + log('Buffering: $isBuffering, Time: $time'); + }, + ); + log("New audio output stream buffer created."); + return stream; + } + + Future playStream() async { + var myStream = setupNewStream(); + if (!SoLoud.instance.isInitialized || myStream == null) { + return null; + } + // Play audio stream + handle = await SoLoud.instance.play(myStream); + stream = myStream; + return stream; + } + + void addDataToAudioStream(Uint8List audioChunk) { + var currentStream = stream; + if (currentStream != null) { + SoLoud.instance.addAudioDataStream(currentStream, audioChunk); + } + } + + Future stopStream() async { + var currentStream = stream; + var currentHandle = handle; + + // Stream doesn't exist or handle is not valid - so nothing to stop. + if (currentStream == null || + currentHandle == null || + !SoLoud.instance.getIsValidVoiceHandle(currentHandle)) { + return; + } + // End data to stream & stop currently playing sound from handle + SoLoud.instance.setDataIsEnded(currentStream); + await SoLoud.instance.stop(currentHandle); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/utilities/utilities.dart b/firebase_ai_logic_showcase/lib/demos/live_api/utilities/utilities.dart new file mode 100644 index 0000000..a19db32 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/utilities/utilities.dart @@ -0,0 +1,17 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export './audio_input.dart'; +export './audio_output.dart'; +export './video_input.dart'; diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/utilities/video_input.dart b/firebase_ai_logic_showcase/lib/demos/live_api/utilities/video_input.dart new file mode 100644 index 0000000..07891a6 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/utilities/video_input.dart @@ -0,0 +1,135 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:developer'; +import 'dart:async'; +import 'dart:typed_data'; +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; + +class VideoInput extends ChangeNotifier { + late List _cameras; + CameraController? _cameraController; + CameraDescription? _selectedCamera; + bool controllerInitialized = false; + Timer? _captureTimer; + StreamController _imageStreamController = StreamController(); + bool _isStreaming = false; + + List get cameras => _cameras; + CameraController? get cameraController => _cameraController; + + Future init() async { + try { + _cameras = await availableCameras(); + if (_cameras.isNotEmpty) { + _selectedCamera = _cameras[0]; + } + } catch (e) { + log('Error getting available cameras: $e'); + } + } + + @override + void dispose() { + super.dispose(); + stopStreamingImages(); + if (controllerInitialized && _cameraController != null) { + _cameraController!.dispose(); + } + } + + Future initializeCameraController() async { + var cameraController = _cameraController; + if (controllerInitialized && cameraController != null) { + await cameraController.dispose(); + controllerInitialized = false; + } + + if (_selectedCamera == null) { + log("No camera selected or available."); + return; + } + + _cameraController = CameraController( + _selectedCamera!, + ResolutionPreset.veryHigh, + enableAudio: false, + imageFormatGroup: ImageFormatGroup.jpeg, + ); + try { + await _cameraController!.initialize(); + controllerInitialized = true; + notifyListeners(); + } catch (e) { + log('Error initializing camera: $e'); + } + } + + Stream startStreamingImages() { + if (_cameraController == null || !_cameraController!.value.isInitialized) { + throw ErrorSummary('Unable to start image stream'); + } + + _captureTimer = Timer.periodic( + const Duration(seconds: 1), // Capture images at 1 frame per second + (timer) async { + if (_cameraController == null || + !_cameraController!.value.isInitialized || + !_isStreaming) { + log("Stopping timer due to invalid state."); + stopStreamingImages(); + return; + } + + try { + // Prevent taking picture if already taking one + if (_cameraController!.value.isTakingPicture) { + return; + } + log("Taking picture..."); + final XFile imageFile = await _cameraController!.takePicture(); + Uint8List imageBytes = await imageFile.readAsBytes(); + _imageStreamController.add(imageBytes); + } catch (e) { + log('Error taking picture: $e'); + } + }, + ); + _isStreaming = true; + return _imageStreamController.stream; + } + + /// Stops the periodic image capture and closes the stream. + Future stopStreamingImages() async { + if (!_isStreaming) { + return; // Nothing to stop + } + _captureTimer?.cancel(); + await _imageStreamController.close(); + _imageStreamController = StreamController(); + _isStreaming = false; + } + + Future flipCamera() async { + if (_cameras.length > 1) { + final otherCamera = _cameras.firstWhere( + (camera) => camera.lensDirection != _selectedCamera?.lensDirection, + orElse: () => _cameras[0], + ); + _selectedCamera = otherCamera; + await initializeCameraController(); + } + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/multimodal/firebaseai_multimodal_service.dart b/firebase_ai_logic_showcase/lib/demos/multimodal/firebaseai_multimodal_service.dart new file mode 100644 index 0000000..39170e8 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/multimodal/firebaseai_multimodal_service.dart @@ -0,0 +1,53 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:developer'; +import 'package:firebase_ai/firebase_ai.dart'; +import 'models/attachment.dart'; + +/// A service that handles all communication with the Firebase AI Gemini API +/// for the Multimodal demo. +/// +/// This service demonstrates how to use the `generateContent()` method on a +/// `GenerativeModel` to provide multimodal input, combining text and file +/// data (like images or PDFs) in a single prompt. +/// +/// For more informations, see the official documentation: +/// https://firebase.google.com/docs/ai-logic/generate-text?api=dev#base64 +class MultimodalService { + final _model = FirebaseAI.googleAI().generativeModel( + model: 'gemini-2.5-flash', + ); + + /// Generates text content from a text prompt and a file attachment. + /// + /// Throws an exception if the API call fails. + Future generateContent(String prompt, Attachment attachment) async { + try { + final attachmentPart = InlineDataPart( + attachment.mimeType, + attachment.fileBytes, + ); + + final response = await _model.generateContent([ + Content.multi([TextPart(prompt), attachmentPart]), + ]); + + return response.text; + } catch (e) { + log('Error generating content: $e'); + rethrow; + } + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/multimodal/models/attachment.dart b/firebase_ai_logic_showcase/lib/demos/multimodal/models/attachment.dart new file mode 100644 index 0000000..f1c2fd3 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/multimodal/models/attachment.dart @@ -0,0 +1,27 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:typed_data'; + +class Attachment { + String fileName; + String mimeType; + Uint8List fileBytes; + + Attachment({ + required this.fileName, + required this.mimeType, + required this.fileBytes, + }); +} diff --git a/firebase_ai_logic_showcase/lib/demos/multimodal/multimodal_demo.dart b/firebase_ai_logic_showcase/lib/demos/multimodal/multimodal_demo.dart new file mode 100644 index 0000000..d1d95a9 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/multimodal/multimodal_demo.dart @@ -0,0 +1,125 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import 'dart:developer' as dev; +import '../../shared/ui/app_frame.dart'; +import '../../shared/ui/app_spacing.dart'; +import './models/attachment.dart'; +import './firebaseai_multimodal_service.dart'; +import './ui_components/ui_components.dart'; +import 'utilities/file_picker_utility.dart'; + +class MultimodalDemo extends StatefulWidget { + const MultimodalDemo({super.key}); + + @override + State createState() => _MultimodalDemoState(); +} + +class _MultimodalDemoState extends State { + // Service for interacting with the Gemini API. + final _multimodalService = MultimodalService(); + + // UI State + bool _loading = false; + TextEditingController promptController = TextEditingController( + text: 'Please analyze this file and explain it to me like I\'m 5.', + ); + Attachment? _attachment; + ExpansibleController promptTileController = ExpansibleController(); + String? outputText; + + void _pickFile() async { + final newAttachment = await FilePickerService().pickFile(context); + if (newAttachment != null) { + setState(() { + _attachment = newAttachment; + }); + } + } + + void askGemini() async { + setState(() { + _loading = true; + }); + + var attachment = _attachment; + var prompt = promptController.text.trim(); + + if (attachment == null || prompt.isEmpty) { + setState(() { + _loading = false; + }); + return; + } + + promptTileController.collapse(); + + try { + outputText = await _multimodalService.generateContent(prompt, attachment); + } catch (e) { + dev.log(e.toString()); + outputText = 'Oops, sorry there was an error processing that file.'; + promptTileController.expand(); + } finally { + setState(() { + _loading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('Multimodal Demo')), + body: SingleChildScrollView( + keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, + padding: EdgeInsets.only( + bottom: MediaQuery.viewInsetsOf(context).bottom, + ), + child: AppFrame( + child: Padding( + padding: const EdgeInsets.fromLTRB( + AppSpacing.s8, + 0, + AppSpacing.s8, + AppSpacing.s8, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox.square(dimension: AppSpacing.s16), + FilePromptInput( + promptController: promptController, + tileController: promptTileController, + loading: _loading, + attachment: _attachment, + askGemini: askGemini, + onPickFilePressed: _pickFile, + onAttachmentChanged: (attachment) { + setState(() { + _attachment = attachment; + }); + }, + ), + OutputDisplay(loading: _loading, outputText: outputText), + ], + ), + ), + ), + ), + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/attachment_view.dart b/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/attachment_view.dart new file mode 100644 index 0000000..f4ac3d3 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/attachment_view.dart @@ -0,0 +1,80 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import 'package:thumbnailer/thumbnailer.dart'; +import '../../../shared/ui/app_spacing.dart'; +import '../models/attachment.dart'; + +class AttachmentView extends StatelessWidget { + final Attachment? attachment; + + const AttachmentView({super.key, this.attachment}); + + @override + Widget build(BuildContext context) { + return attachment == null + ? Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + size: 81, + color: Theme.of(context).colorScheme.outline, + Icons.attach_file, + ), + const SizedBox.square(dimension: AppSpacing.s16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.s16), + child: Text( + 'Select a file', + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface, + ), + textAlign: TextAlign.center, + ), + ), + ], + ) + : Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Thumbnail( + decoration: WidgetDecoration( + wrapperSize: 90, + iconColor: Theme.of(context).colorScheme.primaryFixed, + ), + mimeType: attachment!.mimeType, + onlyIcon: true, + dataResolver: () async { + return attachment!.fileBytes; + }, + widgetSize: 200, + ), + const SizedBox.square(dimension: AppSpacing.s16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.s16), + child: Text( + attachment!.fileName, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface, + ), + textAlign: TextAlign.center, + ), + ), + ], + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/dashed_border_painter.dart b/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/dashed_border_painter.dart new file mode 100644 index 0000000..73b7ce9 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/dashed_border_painter.dart @@ -0,0 +1,69 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math'; +import 'package:flutter/material.dart'; + +class DashedBorderPainter extends CustomPainter { + final Color color; + final double strokeWidth; + final double dashWidth; + final double dashSpace; + final Radius radius; + + DashedBorderPainter({ + required this.color, + this.strokeWidth = 1.0, + this.dashWidth = 8.0, + this.dashSpace = 8.0, + this.radius = const Radius.circular(0), + }); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..strokeWidth = strokeWidth + ..style = PaintingStyle.stroke; + + final path = Path() + ..addRRect( + RRect.fromRectAndRadius( + Rect.fromLTWH(0, 0, size.width, size.height), + radius, + ), + ); + + final dashPath = Path(); + double distance = 0.0; + + for (final pathMetric in path.computeMetrics()) { + while (distance < pathMetric.length) { + dashPath.addPath( + pathMetric.extractPath( + distance, + min(distance + dashWidth, pathMetric.length), + ), + Offset.zero, + ); + distance += dashWidth + dashSpace; + } + } + + canvas.drawPath(dashPath, paint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/file_prompt_input.dart b/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/file_prompt_input.dart new file mode 100644 index 0000000..0c9e99f --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/file_prompt_input.dart @@ -0,0 +1,178 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import '../../../shared/ui/app_spacing.dart'; +import '../models/attachment.dart'; +import './attachment_view.dart'; +import './dashed_border_painter.dart'; + +class FilePromptInput extends StatelessWidget { + final TextEditingController promptController; + final ExpansibleController tileController; + final bool loading; + final Attachment? attachment; + final void Function() askGemini; + final void Function(Attachment?) onAttachmentChanged; + final VoidCallback onPickFilePressed; + + const FilePromptInput({ + super.key, + required this.promptController, + required this.tileController, + required this.loading, + required this.attachment, + required this.askGemini, + required this.onAttachmentChanged, + required this.onPickFilePressed, + }); + + @override + Widget build(BuildContext context) { + return ExpansionTile( + controller: tileController, + collapsedShape: RoundedSuperellipseBorder( + borderRadius: BorderRadius.circular(AppSpacing.s16), + ), + shape: RoundedSuperellipseBorder( + borderRadius: BorderRadius.circular(AppSpacing.s16), + ), + title: Text( + style: Theme.of(context).textTheme.titleMedium, + 'File & Prompt', + ), + initiallyExpanded: true, + collapsedBackgroundColor: Theme.of(context).colorScheme.primaryContainer, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + vertical: AppSpacing.s16, + horizontal: AppSpacing.s24, + ), + child: Stack( + children: [ + Center( + child: GestureDetector( + onTap: onPickFilePressed, + child: CustomPaint( + painter: DashedBorderPainter( + color: Theme.of(context).colorScheme.outline, + strokeWidth: attachment == null ? 4 : 0, + radius: Radius.circular(AppSpacing.s16), + ), + child: Container( + width: 240, + height: 240, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(AppSpacing.s16), + ), + child: AttachmentView(attachment: attachment), + ), + ), + ), + ), + if (attachment != null) + Column( + children: [ + Center( + child: SizedBox( + width: 240, + height: 240, + child: Align( + alignment: Alignment.topRight, + child: Padding( + padding: const EdgeInsets.all(AppSpacing.s8), + child: IconButton( + onPressed: () => onAttachmentChanged(null), + icon: Icon( + size: 32, + color: Theme.of(context).colorScheme.error, + Icons.close, + ), + ), + ), + ), + ), + ), + ], + ), + ], + ), + ), + const SizedBox.square(dimension: AppSpacing.s24), + Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: AppSpacing.s8), + child: TextField( + decoration: InputDecoration( + label: const Text('Prompt'), + fillColor: Theme.of(context).colorScheme.onSecondaryFixed, + filled: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppSpacing.s16), + ), + ), + maxLines: 4, + controller: promptController, + enabled: !loading, + onTap: () { + promptController.selection = TextSelection( + baseOffset: 0, + extentOffset: promptController.text.length, + ); + }, + ), + ), + ), + Padding( + padding: const EdgeInsets.all(AppSpacing.s8), + child: ElevatedButton( + style: ButtonStyle( + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppSpacing.s16), + ), + ), + backgroundColor: WidgetStatePropertyAll( + Theme.of(context).colorScheme.primaryContainer, + ), + ), + onPressed: loading ? null : askGemini, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: AppSpacing.s24, + horizontal: 0, + ), + child: Column( + children: [ + SizedBox( + width: 42, + height: 42, + child: Image.asset('assets/gemini-logo.png'), + ), + const SizedBox.square(dimension: AppSpacing.s4), + const Text(textAlign: TextAlign.center, 'Ask\nGemini'), + ], + ), + ), + ), + ), + ], + ), + ], + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/output_display.dart b/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/output_display.dart new file mode 100644 index 0000000..0794462 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/output_display.dart @@ -0,0 +1,44 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import '../../../shared/ui/app_spacing.dart'; + +class OutputDisplay extends StatelessWidget { + final bool loading; + final String? outputText; + + const OutputDisplay({super.key, required this.loading, this.outputText}); + + @override + Widget build(BuildContext context) { + return loading + ? Padding( + padding: const EdgeInsets.all(AppSpacing.s8), + child: const LinearProgressIndicator(), + ) + : outputText != null + ? Padding( + padding: const EdgeInsets.fromLTRB( + AppSpacing.s24, + AppSpacing.s24, + AppSpacing.s24, + AppSpacing.s48, + ), + child: MarkdownBody(data: outputText!), + ) + : Container(); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/ui_components.dart b/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/ui_components.dart new file mode 100644 index 0000000..0f14a00 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/ui_components.dart @@ -0,0 +1,18 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export 'attachment_view.dart'; +export 'dashed_border_painter.dart'; +export 'file_prompt_input.dart'; +export 'output_display.dart'; diff --git a/firebase_ai_logic_showcase/lib/demos/multimodal/utilities/file_picker_utility.dart b/firebase_ai_logic_showcase/lib/demos/multimodal/utilities/file_picker_utility.dart new file mode 100644 index 0000000..2395c8b --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/multimodal/utilities/file_picker_utility.dart @@ -0,0 +1,158 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:io'; +import 'dart:developer'; +import 'package:cross_file/cross_file.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:mime/mime.dart'; +import '../../../shared/ui/app_spacing.dart'; +import '../models/attachment.dart'; + +const List imageFileTypes = ['png', 'jpeg', 'webp', 'jpg']; +const List videoFileTypes = [ + 'flv', + 'mov', + 'mpeg', + 'mpegps', + 'mpg', + 'mp4', + 'webm', + 'wmv', + '3gpp', +]; +const List audioFileTypes = [ + 'aac', + 'flac', + 'mp3', + 'm4a', + 'mpeg', + 'mpga', + 'mp4', + 'opus', + 'pcm', + 'wav', + 'webm', +]; +const List textFileTypes = ['pdf', 'txt']; + +class FilePickerService { + Future pickFile(BuildContext context) async { + final String? source = await _showFileSourcePicker(context); + if (source == null) return null; + + final FilePickerResult? result = await _pickFileFromSource(source); + if (result == null) return null; + + return _processFilePickerResult(result); + } + + Future _showFileSourcePicker(BuildContext context) async { + return await showModalBottomSheet( + backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, + context: context, + builder: (context) { + return Padding( + padding: const EdgeInsets.all(AppSpacing.s8), + child: SizedBox( + height: 240, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.s16, + vertical: AppSpacing.s8, + ), + child: Text( + style: Theme.of(context).textTheme.titleLarge, + 'File selection', + ), + ), + const Divider(), + if (!kIsWeb && Platform.isIOS) + ListTile( + leading: const Icon(size: 24, Icons.photo_library_rounded), + onTap: () { + Navigator.pop(context, 'Library'); + }, + title: Padding( + padding: const EdgeInsets.only(left: AppSpacing.s8), + child: Text( + style: Theme.of(context).textTheme.titleMedium, + 'Photos and videos', + ), + ), + ), + ListTile( + leading: const Icon(size: 24, Icons.folder), + onTap: () { + Navigator.pop(context, 'Files'); + }, + title: Padding( + padding: const EdgeInsets.only(left: AppSpacing.s8), + child: Text( + style: Theme.of(context).textTheme.titleMedium, + 'Browse files', + ), + ), + ), + ], + ), + ), + ); + }, + ); + } + + Future _pickFileFromSource(String source) async { + if (source == 'Files') { + return await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: [ + ...audioFileTypes, + ...imageFileTypes, + ...videoFileTypes, + ...textFileTypes, + ], + ); + } else { + return await FilePicker.platform.pickFiles(type: FileType.media); + } + } + + Future _processFilePickerResult(FilePickerResult result) async { + var file = XFile(result.files.single.path!); + String fileName = result.files.single.name; + Uint8List fileBytes = await file.readAsBytes(); + String? mimeType = lookupMimeType( + fileName, + headerBytes: fileBytes.sublist(0, 10), + ); + + if (mimeType != null) { + return Attachment( + fileName: fileName, + mimeType: mimeType, + fileBytes: fileBytes, + ); + } else { + // Could not determine the file type. + log('Could not determine MIME type for ${file.path}'); + } + return null; + } +} diff --git a/firebase_ai_logic_showcase/lib/firebase_options.dart b/firebase_ai_logic_showcase/lib/firebase_options.dart new file mode 100644 index 0000000..a2ed568 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/firebase_options.dart @@ -0,0 +1,75 @@ +import 'dart:js_interop'; + +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show TargetPlatform, defaultTargetPlatform, kIsWeb; + +extension type BootstrapFirebaseOptions._(JSObject _) implements JSObject { + external String get apiKey; + external String get authDomain; + external String get databaseURL; + external String get projectId; + external String get storageBucket; + external String get messagingSenderId; + external String get appId; + external String get measurementId; +} + +extension type BootstrapOptions._(JSObject _) implements JSObject { + external String get geminiApiKey; + external BootstrapFirebaseOptions get firebase; +} + +@JS() +// ignore: non_constant_identifier_names +external BootstrapOptions get APP_TEMPLATE_BOOTSTRAP; + +class DefaultFirebaseOptions { + + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for android - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.iOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for ios - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.macOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for macos - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + // ignore: no_default_cases + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static FirebaseOptions web = FirebaseOptions( + apiKey: APP_TEMPLATE_BOOTSTRAP.firebase.apiKey, + appId: APP_TEMPLATE_BOOTSTRAP.firebase.appId, + messagingSenderId: APP_TEMPLATE_BOOTSTRAP.firebase.messagingSenderId, + projectId: APP_TEMPLATE_BOOTSTRAP.firebase.projectId, + authDomain: APP_TEMPLATE_BOOTSTRAP.firebase.authDomain, + storageBucket: APP_TEMPLATE_BOOTSTRAP.firebase.storageBucket, + ); +} diff --git a/firebase_ai_logic_showcase/lib/flutter_firebase_ai_demo.dart b/firebase_ai_logic_showcase/lib/flutter_firebase_ai_demo.dart new file mode 100644 index 0000000..0a7025f --- /dev/null +++ b/firebase_ai_logic_showcase/lib/flutter_firebase_ai_demo.dart @@ -0,0 +1,215 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import 'package:url_launcher/link.dart'; +import 'shared/ui/app_frame.dart'; +import 'demos/chat/chat_demo.dart'; +import 'demos/multimodal/multimodal_demo.dart'; +import './demos/imagen/imagen_demo.dart'; +import './demos/live_api/live_api_demo.dart'; +import 'firebase_options.dart'; +import 'shared/ui/blaze_warning.dart'; + +class Demo { + final String name; + final String description; + final Widget icon; + final Widget page; + + Demo({ + required this.name, + required this.description, + required this.icon, + required this.page, + }); +} + +List demos = [ + Demo( + name: 'Gemini Live API', + description: 'Real-time bidirectional audio & video streaming with Gemini.', + icon: Icon(size: 32, Icons.video_call), + page: LiveAPIDemo(), + ), + Demo( + name: 'Multimodal Prompt', + description: + 'Ask a Gemini model about an image, audio, video, or PDF file.', + icon: Icon(size: 32, Icons.attach_file), + page: MultimodalDemo(), + ), + Demo( + name: 'Create & Edit Images with Nano Banana *', + description: + 'Chat with a Gemini model, including a chat history, tool calling, and even image generation.', + icon: Text(style: TextStyle(fontSize: 28), '🍌'), + page: ChatDemo(), + ), +]; + +class DemoHomeScreen extends StatelessWidget { + const DemoHomeScreen({super.key}); + + void showMoreInfo(BuildContext context) { + showModalBottomSheet( + context: context, + builder: (context) => SizedBox( + width: double.infinity, + child: Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Text('Questions or Feedback?'), + actions: [ + IconButton( + icon: Icon(Icons.close), + onPressed: () => Navigator.pop(context), + ), + ], + ), + body: Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: 400), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: Theme.of(context).textTheme.bodyMedium, + text: + 'Have features you want to see in the app? Please file an issue for us at: ', + children: [ + WidgetSpan( + baseline: TextBaseline.ideographic, + alignment: PlaceholderAlignment.top, + child: Link( + uri: Uri.parse( + 'https://github.com/flutter/demos/issues', + ), + target: LinkTarget.blank, + builder: (context, followLink) => GestureDetector( + onTap: followLink, + child: Text( + style: Theme.of(context).textTheme.bodyMedium! + .copyWith( + fontWeight: FontWeight.bold, + height: 1.15, + decoration: TextDecoration.underline, + color: Theme.of( + context, + ).colorScheme.primary, + ), + 'github.com/flutter/demos/issues', + ), + ), + ), + ), + TextSpan(text: '.'), + ], + ), + ), + SizedBox.square(dimension: 32), + Text( + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + 'This app was made with ❤️\nby the Flutter & Firebase AI Logic Teams', + ), + ], + ), + ), + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.transparent, + leading: Padding( + padding: EdgeInsets.fromLTRB(16, 8, 4, 8), + child: Image.asset('assets/firebase-ai-logic.png'), + ), + title: Text( + style: Theme.of(context).textTheme.titleLarge, + 'Flutter AI Playground', + ), + actions: [ + Padding( + padding: EdgeInsets.fromLTRB(4, 8, 16, 8), + child: IconButton( + icon: Icon(Icons.info_outline), + onPressed: () => showMoreInfo(context), + ), + ), + ], + ), + body: AppFrame( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 32, vertical: 16), + child: Text( + style: Theme.of(context).textTheme.bodyLarge, + textAlign: TextAlign.center, + "Build AI features in your Flutter apps – use the Firebase AI Logic SDK to access Google's AI models directly from your app.", + ), + ), + Expanded( + child: ListView.builder( + padding: EdgeInsets.all(8), + itemBuilder: (context, index) { + final demo = demos[index]; + + return Padding( + padding: EdgeInsets.all(8), + child: ListTile( + onTap: () => Navigator.push( + context, + MaterialPageRoute(builder: (context) => demo.page), + ), + shape: RoundedSuperellipseBorder( + borderRadius: BorderRadiusGeometry.circular(16), + ), + leading: demo.icon, + title: Text( + demo.name, + style: TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Text(demo.description), + tileColor: Theme.of(context).colorScheme.primaryContainer, + trailing: Icon( + Icons.arrow_forward, + color: Theme.of(context).colorScheme.primaryFixedDim, + ), + ), + ); + }, + itemCount: demos.length, + ), + ), + BlazeFooter(), + ], + ), + ), + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/main.dart b/firebase_ai_logic_showcase/lib/main.dart new file mode 100644 index 0000000..6cb44a6 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/main.dart @@ -0,0 +1,48 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'flutter_firebase_ai_demo.dart'; +import './firebase_options.dart'; +import 'shared/app_state.dart'; + +void main() async { + FirebaseOptions options = DefaultFirebaseOptions.currentPlatform; + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(options: options); + runApp(const ProviderScope(child: MyApp())); +} + +class MyApp extends ConsumerWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final appColor = ref.watch(appStateProvider).appColor; + return MaterialApp( + title: 'Flutter AI Playground', + home: DemoHomeScreen(), + theme: ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: appColor, + brightness: Brightness.dark, + dynamicSchemeVariant: DynamicSchemeVariant.tonalSpot, + ).copyWith(surface: appColor), + ), + debugShowCheckedModeBanner: false, + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/shared/app_state.dart b/firebase_ai_logic_showcase/lib/shared/app_state.dart new file mode 100644 index 0000000..a049d3a --- /dev/null +++ b/firebase_ai_logic_showcase/lib/shared/app_state.dart @@ -0,0 +1,30 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class AppState extends ChangeNotifier { + Color appColor = Color.fromARGB(255, 0, 32, 46); + + String setAppColor(Color color) { + appColor = color; + notifyListeners(); + return 'Color successfully changed to ${appColor.toString()}!'; + } +} + +final appStateProvider = ChangeNotifierProvider((ref) { + return AppState(); +}); diff --git a/firebase_ai_logic_showcase/lib/shared/firebaseai_imagen_service.dart b/firebase_ai_logic_showcase/lib/shared/firebaseai_imagen_service.dart new file mode 100644 index 0000000..3f0b663 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/shared/firebaseai_imagen_service.dart @@ -0,0 +1,48 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:developer'; +import 'dart:typed_data'; +import 'package:firebase_ai/firebase_ai.dart'; + +/// This service demonstrates how to use a Gemini Image model to generate images +/// from a text prompt. It showcases the text-to-image generation feature. +/// +/// For more information, see the official documentation: +/// https://firebase.google.com/docs/ai-logic/generate-images-imagen?api=dev +/// +/// This is a shared service, located in the /shared directory, because it is +/// used by multiple demos (Chat, Live API, and Imagen) to provide image +/// generation capabilities. +class ImageGenerationService { + final _model = FirebaseAI.googleAI().generativeModel( + model: 'gemini-2.5-flash-image-preview', + generationConfig: GenerationConfig( + responseModalities: [ResponseModalities.image], + ), + ); + + /// Generates a single image from a text prompt. + /// + /// Throws an exception if the API call fails, allowing the UI to handle it. + Future generateImage(String prompt) async { + try { + final res = await _model.generateContent([Content.text(prompt)]); + return res.inlineDataParts.first.bytes; + } catch (e) { + log('Error generating image: $e'); + rethrow; + } + } +} diff --git a/firebase_ai_logic_showcase/lib/shared/function_calling/tools.dart b/firebase_ai_logic_showcase/lib/shared/function_calling/tools.dart new file mode 100644 index 0000000..86f15ac --- /dev/null +++ b/firebase_ai_logic_showcase/lib/shared/function_calling/tools.dart @@ -0,0 +1,47 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:firebase_ai/firebase_ai.dart'; + +final generateImageTool = FunctionDeclaration( + 'GenerateImage', + 'Generate an image by describing it.', + parameters: { + 'description': Schema.string( + description: + 'A description of the image that you want to generate. Be as specific as possible. More specific is better. Describe the contents of the image, the style, and any other specifics about the image that is to be generated.', + ), + }, +); + +final setAppColorTool = FunctionDeclaration( + 'SetAppColor', + 'Set the app color. You must pick a color that matches the hue that the user requests. When talking with the user, use a human-friendly description of the color instead of RGB values.', + parameters: { + 'red': Schema.integer( + description: + "The desired app color's RGB RED channel value that can range from 0 to 46", + ), + 'green': Schema.integer( + description: + "The desired app color's RGB GREEN channel value that can range from 0 to 46", + ), + 'blue': Schema.integer( + description: + "The desired app color's RGB BLUE channel that can range from 0 to 46", + ), + }, +); + +// See "live_api" and "chat" demos for full implementation of function calling. diff --git a/firebase_ai_logic_showcase/lib/shared/ui/app_frame.dart b/firebase_ai_logic_showcase/lib/shared/ui/app_frame.dart new file mode 100644 index 0000000..25772fe --- /dev/null +++ b/firebase_ai_logic_showcase/lib/shared/ui/app_frame.dart @@ -0,0 +1,31 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; + +class AppFrame extends StatelessWidget { + const AppFrame({required this.child, super.key}); + + final Widget child; + + @override + Widget build(BuildContext context) { + return Center( + child: ConstrainedBox( + constraints: BoxConstraints.loose(Size(900, double.infinity)), + child: child, + ), + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/shared/ui/app_spacing.dart b/firebase_ai_logic_showcase/lib/shared/ui/app_spacing.dart new file mode 100644 index 0000000..8aeff26 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/shared/ui/app_spacing.dart @@ -0,0 +1,35 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// A class to hold consistent spacing values for the application, +/// following a 4dp grid. +class AppSpacing { + /// 4.0 + static const double s4 = 4.0; + + /// 8.0 + static const double s8 = 8.0; + + /// 16.0 + static const double s16 = 16.0; + + /// 24.0 + static const double s24 = 24.0; + + /// 48.0 + static const double s48 = 48.0; + + /// 60.0 + static const double s60 = 60.0; +} diff --git a/firebase_ai_logic_showcase/lib/shared/ui/blaze_warning.dart b/firebase_ai_logic_showcase/lib/shared/ui/blaze_warning.dart new file mode 100644 index 0000000..229020d --- /dev/null +++ b/firebase_ai_logic_showcase/lib/shared/ui/blaze_warning.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; +import 'package:url_launcher/link.dart'; +import '../../firebase_options.dart'; + +class BlazeWarning extends StatelessWidget { + const BlazeWarning({super.key}); + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: Theme.of(context).textTheme.bodyMedium, + text: + 'This demo includes some features that require an upgrade to the pay-as-you-go Blaze pricing plan, which you can ', + children: [ + WidgetSpan( + baseline: TextBaseline.ideographic, + alignment: PlaceholderAlignment.top, + child: Link( + uri: Uri.parse( + 'https://console.firebase.google.com/project/$%7BDefaultFirebaseOptions.currentPlatform.projectId%7D/overview?purchaseBillingPlan=metered', + ), + target: LinkTarget.blank, + builder: (context, followLink) => GestureDetector( + onTap: followLink, + child: Text( + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + height: 1.15, + decoration: TextDecoration.underline, + color: Theme.of(context).colorScheme.primary, + ), + 'set up in the Firebase console', + ), + ), + ), + ), + TextSpan(text: '.'), + ], + ), + ), + ), + ); + } +} + +class BlazeFooter extends StatelessWidget { + const BlazeFooter({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 8.0), + child: Text.rich( + textAlign: TextAlign.center, + TextSpan( + style: Theme.of(context).textTheme.bodyMedium, + children: [ + TextSpan( + text: + '* This demo includes some features that require an upgrade to the ', + ), + WidgetSpan( + baseline: TextBaseline.ideographic, + alignment: PlaceholderAlignment.top, + child: Link( + uri: Uri.parse('https://firebase.google.com/pricing'), + target: LinkTarget.blank, + builder: (context, followLink) => GestureDetector( + onTap: followLink, + child: Text( + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + height: 1.15, + decoration: TextDecoration.underline, + color: Theme.of(context).colorScheme.primary, + ), + 'pay-as-you-go Blaze pricing plan', + ), + ), + ), + ), + TextSpan(text: ', which you can '), + WidgetSpan( + baseline: TextBaseline.ideographic, + alignment: PlaceholderAlignment.top, + child: Link( + uri: Uri.parse( + 'https://console.firebase.google.com/project/$%7BDefaultFirebaseOptions.currentPlatform.projectId%7D/overview?purchaseBillingPlan=metered', + ), + target: LinkTarget.blank, + builder: (context, followLink) => GestureDetector( + onTap: followLink, + child: Text( + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + height: 1.15, + decoration: TextDecoration.underline, + color: Theme.of(context).colorScheme.primary, + ), + 'set up in the Firebase console', + ), + ), + ), + ), + TextSpan(text: '.'), + ], + ), + ), + ); + } +} diff --git a/firebase_ai_logic_showcase/pubspec.yaml b/firebase_ai_logic_showcase/pubspec.yaml new file mode 100644 index 0000000..abf565e --- /dev/null +++ b/firebase_ai_logic_showcase/pubspec.yaml @@ -0,0 +1,40 @@ +name: flutter_firebase_ai_sample +description: "Flutter application demonstrates Firebase AI Logic capabilities through a series of interactive demos." +publish_to: 'none' +version: 1.0.0+1 + +environment: + sdk: ^3.8.1 + +dependencies: + flutter: + sdk: flutter + cupertino_icons: ^1.0.8 + record: ^6.1.1 + flutter_riverpod: ^2.6.1 + flutter_soloud: ^3.1.6 + firebase_core: ^4.0.0 + camera: ^0.11.1 + flutter_image_compress: ^2.4.0 + waveform_flutter: ^1.2.0 + flutter_animate: ^4.5.2 + firebase_ai: ^3.3.0 + image_picker: ^1.1.2 + permission_handler: ^12.0.1 + flutter_markdown: ^0.7.7+1 + file_picker: ^10.2.1 + thumbnailer: ^3.1.0 + cross_file: ^0.3.4+2 + mime: ^2.0.0 + url_launcher: ^6.3.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + +flutter: + uses-material-design: true + assets: + - assets/gemini-logo.png + - assets/firebase-ai-logic.png diff --git a/firebase_ai_logic_showcase/web/404.html b/firebase_ai_logic_showcase/web/404.html new file mode 100644 index 0000000..829eda8 --- /dev/null +++ b/firebase_ai_logic_showcase/web/404.html @@ -0,0 +1,33 @@ + + + + + + Page Not Found + + + + +
+

404

+

Page Not Found

+

The specified file was not found on this website. Please check the URL for mistakes and try again.

+

Why am I seeing this?

+

This page was generated by the Firebase Command-Line Interface. To modify it, edit the 404.html file in your project's configured public directory.

+
+ + diff --git a/firebase_ai_logic_showcase/web/favicon.png b/firebase_ai_logic_showcase/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/firebase_ai_logic_showcase/web/favicon.png differ diff --git a/firebase_ai_logic_showcase/web/icons/Icon-192.png b/firebase_ai_logic_showcase/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/firebase_ai_logic_showcase/web/icons/Icon-192.png differ diff --git a/firebase_ai_logic_showcase/web/icons/Icon-512.png b/firebase_ai_logic_showcase/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/firebase_ai_logic_showcase/web/icons/Icon-512.png differ diff --git a/firebase_ai_logic_showcase/web/icons/Icon-maskable-192.png b/firebase_ai_logic_showcase/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/firebase_ai_logic_showcase/web/icons/Icon-maskable-192.png differ diff --git a/firebase_ai_logic_showcase/web/icons/Icon-maskable-512.png b/firebase_ai_logic_showcase/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/firebase_ai_logic_showcase/web/icons/Icon-maskable-512.png differ diff --git a/firebase_ai_logic_showcase/web/index.html b/firebase_ai_logic_showcase/web/index.html new file mode 100644 index 0000000..a7eed38 --- /dev/null +++ b/firebase_ai_logic_showcase/web/index.html @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + flutter_firebase_ai_sample + + + + + + + + + + + + + \ No newline at end of file diff --git a/firebase_ai_logic_showcase/web/manifest.json b/firebase_ai_logic_showcase/web/manifest.json new file mode 100644 index 0000000..1edb532 --- /dev/null +++ b/firebase_ai_logic_showcase/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "flutter_firebase_ai_sample", + "short_name": "flutter_firebase_ai_sample", + "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" + } + ] +} \ No newline at end of file diff --git a/jnigen_db_native_interop/.gitignore b/jnigen_db_native_interop/.gitignore new file mode 100644 index 0000000..58ee391 --- /dev/null +++ b/jnigen_db_native_interop/.gitignore @@ -0,0 +1,5 @@ +/mvn_java +/.dart_tool +.DS_Store +/.idea +/build diff --git a/jnigen_db_native_interop/LICENSE b/jnigen_db_native_interop/LICENSE new file mode 100644 index 0000000..9748873 --- /dev/null +++ b/jnigen_db_native_interop/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2025, James Williams + +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. diff --git a/jnigen_db_native_interop/README.md b/jnigen_db_native_interop/README.md new file mode 100644 index 0000000..ea7b30a --- /dev/null +++ b/jnigen_db_native_interop/README.md @@ -0,0 +1,116 @@ +# jni_leveldb + +A sample command-line application demonstrating how to create an idiomatic Dart wrapper around a Java implementation of LevelDB, a fast key-value storage library. This project showcases the evolution of a LevelDB wrapper from raw JNI bindings to a safe, clean, and Dart-native API using `jnigen`. + +## Prerequisites + +- Java Development Kit (JDK) version 1.8 or later must be installed, and the `JAVA_HOME` environment variable must be set to its location. + +## Setup and Running + +1. **Generate Bindings and Build JNI code:** + + Run `jnigen` to download the required Java libraries, generate Dart bindings, and build the JNI glue code. + + ```bash + dart run jni:setup + dart run jnigen:setup + dart run jnigen --config jnigen.yaml + ``` + +2. **Run the application:** + + ```bash + dart run bin/jni_leveldb.dart + ``` + +## From Raw Bindings to an Idiomatic Dart API + +The initial output of `jnigen` provides a low-level, direct mapping of the Java API. Using this directly in application code can be verbose, unsafe, and unidiomatic. This project demonstrates how to build a better wrapper by addressing common pitfalls. + +### 1. Resource Management + +JNI objects are handles to resources in the JVM. Failing to release them causes memory leaks. + +**The Problem:** Forgetting to call `release()` on JNI objects. + +**The Solution:** The best practice is to use the `using(Arena arena)` block, which automatically manages releasing all objects allocated within it, making your code safer and cleaner. For objects that live longer, you must manually call `release()`. + +*Example from the wrapper:* +```dart +void putBytes(Uint8List key, Uint8List value) { + using((arena) { + final jKey = JByteArray.from(key)..releasedBy(arena); + final jValue = JByteArray.from(value)..releasedBy(arena); + _db.put(jKey, jValue); + }); +} +``` + +### 2. Idiomatic API Design and Type Handling + +Raw bindings expose Java's conventions and require manual, repetitive type conversions. A wrapper class should expose a clean, Dart-like API. + +**The Problem:** Java method names (`createIfMissing$1`) and types (`JString`, `JByteArray`) are exposed directly to the application code. + +**The Solution:** Create a wrapper class that exposes methods with named parameters and standard Dart types (`String`, `Uint8List`), handling all the JNI conversions internally. + +*The Improved API:* +```dart +static LevelDB open(String path, {bool createIfMissing = true}) { ... } +void put(String key, String value) { ... } +String? get(String key) { ... } +``` + +This allows for clean, simple iteration in the application code: +```dart +for (var entry in db.entries) { + print('${entry.key}, ${entry.value}'); +} +``` + +### 3. JVM Initialization + +The JVM is a process-level resource and should be initialized only once when the application starts. + +**The Problem:** Calling `Jni.spawn()` inside library code. + +**The Solution:** `Jni.spawn()` belongs in a locatio where it will be called once, like your application's `main()` function, not in the library. In this example, the library code should assume the JVM is already running. + +*Correct Usage in `bin/jni_leveldb.dart`:* +```dart +void main(List arguments) { + // ... find JARs ... + Jni.spawn(classPath: jars); // Spawn the JVM once. + db(); // Run the application logic. +} +``` + +### The Final Result + +By applying these principles, the application logic becomes simple, readable, and free of JNI-specific details. + +*Final Application Code:* +```dart +import 'package:jni_leveldb/src/leveldb.dart'; + +void db() { + final db = LevelDB.open('example.db'); + try { + db.put('Akron', 'Ohio'); + db.put('Tampa', 'Florida'); + db.put('Cleveland', 'Ohio'); + + print('Tampa is in ${db.get('Tampa')}'); + + db.delete('Akron'); + + print('\nEntries in database:'); + for (var entry in db.entries) { + print('${entry.key}, ${entry.value}'); + } + } finally { + db.close(); + } +} +``` \ No newline at end of file diff --git a/jnigen_db_native_interop/bin/jni_leveldb.dart b/jnigen_db_native_interop/bin/jni_leveldb.dart new file mode 100644 index 0000000..fd0bf38 --- /dev/null +++ b/jnigen_db_native_interop/bin/jni_leveldb.dart @@ -0,0 +1,30 @@ +import 'package:jni_leveldb/jni_leveldb.dart' as jni_leveldb; +import 'dart:io'; +import 'package:jni/jni.dart'; + +import 'package:path/path.dart'; + +const jarError = ''; + +void main(List arguments) { + +const jarDir = './mvn_jar/'; + List jars; + try { + jars = Directory(jarDir) + .listSync() + .map((e) => e.path) + .where((path) => path.endsWith('.jar')) + .toList(); + } on OSError catch (_) { + stderr.writeln(jarError); + return; + } + if (jars.isEmpty) { + stderr.writeln(jarError); + return; + } + Jni.spawn(classPath: jars); + + jni_leveldb.db(); +} diff --git a/jnigen_db_native_interop/jnigen.yaml b/jnigen_db_native_interop/jnigen.yaml new file mode 100644 index 0000000..741d27a --- /dev/null +++ b/jnigen_db_native_interop/jnigen.yaml @@ -0,0 +1,15 @@ +output: + dart: + path: 'lib/leveldb/' + +classes: + - 'org.iq80.leveldb.DB' + - 'org.iq80.leveldb.Options' + - 'org.iq80.leveldb.DBIterator' + - 'org.iq80.leveldb.impl.Iq80DBFactory' + - 'org.iq80.leveldb.impl.SeekingIteratorAdapter' + - 'java.io.File' + +maven_downloads: + source_deps: + - 'org.iq80.leveldb:leveldb:0.12' diff --git a/jnigen_db_native_interop/lib/jni_leveldb.dart b/jnigen_db_native_interop/lib/jni_leveldb.dart new file mode 100644 index 0000000..a00fef8 --- /dev/null +++ b/jnigen_db_native_interop/lib/jni_leveldb.dart @@ -0,0 +1,22 @@ +import 'package:jni_leveldb/src/leveldb.dart'; + +void db() { + final db = LevelDB.open('example.db'); + try { + db.put('Akron', 'Ohio'); + db.put('Tampa', 'Florida'); + db.put('Cleveland', 'Ohio'); + db.put('Sunnyvale', 'California'); + + print('Tampa is in ${db.get('Tampa')}'); + + db.delete('Akron'); + + print('\nEntries in database:'); + for (var entry in db.entries) { + print('${entry.key}, ${entry.value}'); + } + } finally { + db.close(); + } +} \ No newline at end of file diff --git a/jnigen_db_native_interop/lib/src/leveldb.dart b/jnigen_db_native_interop/lib/src/leveldb.dart new file mode 100644 index 0000000..f4caafe --- /dev/null +++ b/jnigen_db_native_interop/lib/src/leveldb.dart @@ -0,0 +1,106 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:jni/jni.dart'; +import 'package:jni_leveldb/leveldb/java/io/File.dart' as java; +import 'package:jni_leveldb/leveldb/org/iq80/leveldb/DB.dart'; +import 'package:jni_leveldb/leveldb/org/iq80/leveldb/Options.dart'; +import 'package:jni_leveldb/leveldb/org/iq80/leveldb/impl/Iq80DBFactory.dart'; +import 'package:jni_leveldb/leveldb/org/iq80/leveldb/impl/SeekingIteratorAdapter.dart'; + +class LevelDB { + final DB _db; + + LevelDB._(this._db); + + static LevelDB open(String path, {bool createIfMissing = true}) { + final options = Options()..createIfMissing$1(createIfMissing); + final file = java.File(path.toJString()); + final db = Iq80DBFactory.factory!.open(file, options); + if (db == null) { + throw Exception('Failed to open database at $path'); + } + return LevelDB._(db); + } + + void put(String key, String value) { + putBytes(utf8.encode(key), utf8.encode(value)); + } + + void putBytes(Uint8List key, Uint8List value) { + using((arena) { + final jKey = JByteArray.from(key)..releasedBy(arena); + final jValue = JByteArray.from(value)..releasedBy(arena); + _db.put(jKey, jValue); + }); + } + + String? get(String key) { + final value = getBytes(utf8.encode(key)); + if (value == null) { + return null; + } + return utf8.decode(value); + } + + Uint8List? getBytes(Uint8List key) { + return using((arena) { + final jKey = JByteArray.from(key)..releasedBy(arena); + final value = _db.get(jKey); + if (value == null) { + return null; + } + final bytes = value.toList(); + value.release(); + return Uint8List.fromList(bytes); + }); + } + + void delete(String key) { + deleteBytes(utf8.encode(key)); + } + + void deleteBytes(Uint8List key) { + using((arena) { + final jKey = JByteArray.from(key)..releasedBy(arena); + _db.delete(jKey); + }); + } + + void close() { + _db.release(); + } + + Iterable> get entries sync* { + final iterator = _db.iterator()?.as(SeekingIteratorAdapter.type); + if (iterator == null) return; + try { + iterator.seekToFirst(); + while (iterator.hasNext()) { + final entry = iterator.next(); + if (entry == null) continue; + + final keyBytes = entry.getKey(); + final valueBytes = entry.getValue(); + + if (keyBytes == null || valueBytes == null) { + keyBytes?.release(); + valueBytes?.release(); + entry.release(); + continue; + } + + final key = utf8.decode(keyBytes.toList()); + final value = utf8.decode(valueBytes.toList()); + + keyBytes.release(); + valueBytes.release(); + entry.release(); + + yield MapEntry(key, value); + } + } finally { + iterator.release(); + } + } +} diff --git a/jnigen_db_native_interop/pubspec.yaml b/jnigen_db_native_interop/pubspec.yaml new file mode 100644 index 0000000..618b643 --- /dev/null +++ b/jnigen_db_native_interop/pubspec.yaml @@ -0,0 +1,19 @@ +name: jni_leveldb +description: A sample command-line application. +version: 1.0.0 +# repository: https://github.com/my_org/my_repo + +publish_to: none + +environment: + sdk: ^3.6.2 + +# Add regular dependencies here. +dependencies: + jnigen: + path: ../native-non-fork/pkgs/jnigen + jni: ^0.14.1 + +dev_dependencies: + lints: ^5.0.0 + test: ^1.24.0 diff --git a/tool/flutter_ci_script_beta.sh b/tool/flutter_ci_script_beta.sh deleted file mode 100755 index a091de0..0000000 --- a/tool/flutter_ci_script_beta.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -set -e - -DIR="${BASH_SOURCE%/*}" -source "$DIR/flutter_ci_script_shared.sh" - -flutter doctor -v - -declare -ar PROJECT_NAMES=( - "vertex_ai_firebase_flutter_app" -) - -ci_projects "beta" "${PROJECT_NAMES[@]}" - -echo "-- Success --" diff --git a/tool/flutter_ci_script_master.sh b/tool/flutter_ci_script_master.sh deleted file mode 100755 index ba65a53..0000000 --- a/tool/flutter_ci_script_master.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -set -e - -DIR="${BASH_SOURCE%/*}" -source "$DIR/flutter_ci_script_shared.sh" - -flutter doctor -v - -declare -ar PROJECT_NAMES=( - "vertex_ai_firebase_flutter_app" -) - -ci_projects "master" "${PROJECT_NAMES[@]}" - -echo "-- Success --" diff --git a/tool/flutter_ci_script_shared.sh b/tool/flutter_ci_script_shared.sh deleted file mode 100644 index f93be22..0000000 --- a/tool/flutter_ci_script_shared.sh +++ /dev/null @@ -1,33 +0,0 @@ -function ci_projects () { - local channel="$1" - - shift - local arr=("$@") - for PROJECT_NAME in "${arr[@]}" - do - echo "== Testing '${PROJECT_NAME}' on Flutter's $channel channel ==" - pushd "${PROJECT_NAME}" - - # Grab packages. - flutter pub get - - # Run the analyzer to find any static analysis issues. - dart analyze --fatal-infos --fatal-warnings - - # Run the formatter on all the dart files to make sure everything's linted. - dart format --output none --set-exit-if-changed . - - # Run the actual tests. - if [ -d "test" ] - then - if grep -q "flutter:" "pubspec.yaml"; then - flutter test - else - # If the project is not a Flutter project, use the Dart CLI. - dart test - fi - fi - - popd - done -} diff --git a/tool/flutter_ci_script_stable.sh b/tool/flutter_ci_script_stable.sh deleted file mode 100755 index e28a400..0000000 --- a/tool/flutter_ci_script_stable.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -set -e - -DIR="${BASH_SOURCE%/*}" -source "$DIR/flutter_ci_script_shared.sh" - -flutter doctor -v - -declare -ar PROJECT_NAMES=( - "vertex_ai_firebase_flutter_app" -) - -ci_projects "stable" "${PROJECT_NAMES[@]}" - -echo "-- Success --" diff --git a/vertex_ai_firebase_flutter_app/windows/.gitignore b/vertex_ai_firebase_flutter_app/windows/.gitignore deleted file mode 100644 index d492d0d..0000000 --- a/vertex_ai_firebase_flutter_app/windows/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -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/vertex_ai_firebase_flutter_app/windows/CMakeLists.txt b/vertex_ai_firebase_flutter_app/windows/CMakeLists.txt deleted file mode 100644 index 6e05810..0000000 --- a/vertex_ai_firebase_flutter_app/windows/CMakeLists.txt +++ /dev/null @@ -1,108 +0,0 @@ -# Project-level configuration. -cmake_minimum_required(VERSION 3.14) -project(colorist 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 "colorist") - -# 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/vertex_ai_firebase_flutter_app/windows/flutter/CMakeLists.txt b/vertex_ai_firebase_flutter_app/windows/flutter/CMakeLists.txt deleted file mode 100644 index 903f489..0000000 --- a/vertex_ai_firebase_flutter_app/windows/flutter/CMakeLists.txt +++ /dev/null @@ -1,109 +0,0 @@ -# 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/vertex_ai_firebase_flutter_app/windows/flutter/generated_plugin_registrant.cc b/vertex_ai_firebase_flutter_app/windows/flutter/generated_plugin_registrant.cc deleted file mode 100644 index d141b74..0000000 --- a/vertex_ai_firebase_flutter_app/windows/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,17 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - -#include -#include - -void RegisterPlugins(flutter::PluginRegistry* registry) { - FirebaseAuthPluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi")); - FirebaseCorePluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); -} diff --git a/vertex_ai_firebase_flutter_app/windows/flutter/generated_plugin_registrant.h b/vertex_ai_firebase_flutter_app/windows/flutter/generated_plugin_registrant.h deleted file mode 100644 index dc139d8..0000000 --- a/vertex_ai_firebase_flutter_app/windows/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// 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/vertex_ai_firebase_flutter_app/windows/flutter/generated_plugins.cmake b/vertex_ai_firebase_flutter_app/windows/flutter/generated_plugins.cmake deleted file mode 100644 index 29944d5..0000000 --- a/vertex_ai_firebase_flutter_app/windows/flutter/generated_plugins.cmake +++ /dev/null @@ -1,25 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST - firebase_auth - firebase_core -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST -) - -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/vertex_ai_firebase_flutter_app/windows/runner/CMakeLists.txt b/vertex_ai_firebase_flutter_app/windows/runner/CMakeLists.txt deleted file mode 100644 index 394917c..0000000 --- a/vertex_ai_firebase_flutter_app/windows/runner/CMakeLists.txt +++ /dev/null @@ -1,40 +0,0 @@ -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/vertex_ai_firebase_flutter_app/windows/runner/Runner.rc b/vertex_ai_firebase_flutter_app/windows/runner/Runner.rc deleted file mode 100644 index e0478fb..0000000 --- a/vertex_ai_firebase_flutter_app/windows/runner/Runner.rc +++ /dev/null @@ -1,121 +0,0 @@ -// 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", "colorist" "\0" - VALUE "FileVersion", VERSION_AS_STRING "\0" - VALUE "InternalName", "colorist" "\0" - VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0" - VALUE "OriginalFilename", "colorist.exe" "\0" - VALUE "ProductName", "colorist" "\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/vertex_ai_firebase_flutter_app/windows/runner/flutter_window.cpp b/vertex_ai_firebase_flutter_app/windows/runner/flutter_window.cpp deleted file mode 100644 index 955ee30..0000000 --- a/vertex_ai_firebase_flutter_app/windows/runner/flutter_window.cpp +++ /dev/null @@ -1,71 +0,0 @@ -#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/vertex_ai_firebase_flutter_app/windows/runner/flutter_window.h b/vertex_ai_firebase_flutter_app/windows/runner/flutter_window.h deleted file mode 100644 index 6da0652..0000000 --- a/vertex_ai_firebase_flutter_app/windows/runner/flutter_window.h +++ /dev/null @@ -1,33 +0,0 @@ -#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/vertex_ai_firebase_flutter_app/windows/runner/main.cpp b/vertex_ai_firebase_flutter_app/windows/runner/main.cpp deleted file mode 100644 index 021ad6b..0000000 --- a/vertex_ai_firebase_flutter_app/windows/runner/main.cpp +++ /dev/null @@ -1,43 +0,0 @@ -#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"colorist", 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/vertex_ai_firebase_flutter_app/windows/runner/resource.h b/vertex_ai_firebase_flutter_app/windows/runner/resource.h deleted file mode 100644 index 66a65d1..0000000 --- a/vertex_ai_firebase_flutter_app/windows/runner/resource.h +++ /dev/null @@ -1,16 +0,0 @@ -//{{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/vertex_ai_firebase_flutter_app/windows/runner/resources/app_icon.ico b/vertex_ai_firebase_flutter_app/windows/runner/resources/app_icon.ico deleted file mode 100644 index c04e20c..0000000 Binary files a/vertex_ai_firebase_flutter_app/windows/runner/resources/app_icon.ico and /dev/null differ diff --git a/vertex_ai_firebase_flutter_app/windows/runner/runner.exe.manifest b/vertex_ai_firebase_flutter_app/windows/runner/runner.exe.manifest deleted file mode 100644 index 153653e..0000000 --- a/vertex_ai_firebase_flutter_app/windows/runner/runner.exe.manifest +++ /dev/null @@ -1,14 +0,0 @@ - - - - - PerMonitorV2 - - - - - - - - - diff --git a/vertex_ai_firebase_flutter_app/windows/runner/utils.cpp b/vertex_ai_firebase_flutter_app/windows/runner/utils.cpp deleted file mode 100644 index 3a0b465..0000000 --- a/vertex_ai_firebase_flutter_app/windows/runner/utils.cpp +++ /dev/null @@ -1,65 +0,0 @@ -#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/vertex_ai_firebase_flutter_app/windows/runner/utils.h b/vertex_ai_firebase_flutter_app/windows/runner/utils.h deleted file mode 100644 index 3879d54..0000000 --- a/vertex_ai_firebase_flutter_app/windows/runner/utils.h +++ /dev/null @@ -1,19 +0,0 @@ -#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/vertex_ai_firebase_flutter_app/windows/runner/win32_window.cpp b/vertex_ai_firebase_flutter_app/windows/runner/win32_window.cpp deleted file mode 100644 index 60608d0..0000000 --- a/vertex_ai_firebase_flutter_app/windows/runner/win32_window.cpp +++ /dev/null @@ -1,288 +0,0 @@ -#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/vertex_ai_firebase_flutter_app/windows/runner/win32_window.h b/vertex_ai_firebase_flutter_app/windows/runner/win32_window.h deleted file mode 100644 index e901dde..0000000 --- a/vertex_ai_firebase_flutter_app/windows/runner/win32_window.h +++ /dev/null @@ -1,102 +0,0 @@ -#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_